SQL access is growing through large store modules and object facades. Prefer feature-local Drizzle against tables. Keep helpers only for real invariants or shared non-trivial mapping.
Intent
Agreed direction from design review:
- Drizzle + table schema is the data access layer.
- Default: query tables in the owning feature or plugin module.
- Extract a function only when a write rule is load-bearing, or the same non-trivial mapping repeats.
- Do not invent CRUD facades (
get/list/create/save/delete) that only wrap one query.
- Keep object
*Store factories only for a real edge (today: conversation DI). Do not grow new ones for normal features.
- No global
repositories/, DAO framework, or new persistence vocabulary.
src/db/ stays connection + schema. Feature/plugin folders own ops next to their tools and runtime code.
- Split large files by domain concern only after the facade is gone, and only when the file is still large.
This matches existing healthy code (event-tasks, workspaces, github outcomes, many api/ readers) and existing policy (policies/interface-design.md, policies/correctness-complexity.md, chat README ownership by feature).
Audit
store.ts inventory (current main)
| Lines |
Backend |
Path |
| 2008 |
SQL + legacy state |
packages/junior/src/chat/scheduled-tasks/store.ts |
| 1617 |
SQL |
packages/junior-memory/src/store.ts |
| 898 |
SQL class/ConversationStore |
packages/junior/src/chat/conversations/sql/store.ts |
| 579 |
state |
packages/junior/src/chat/agent-dispatch/store.ts |
| 572 |
state |
packages/junior/src/chat/resource-events/store.ts |
| 558 |
SQL |
packages/junior/src/chat/attachments/store.ts |
| 556 |
mailbox/lease (not SQL facade) |
packages/junior/src/chat/task-execution/store.ts |
| 439 |
SQL |
packages/junior/src/chat/agent-invocations/store.ts |
| 360 |
SQL plain functions |
packages/junior/src/chat/workspaces/store.ts |
| 304 |
SQL |
packages/junior/src/chat/artifacts/store.ts |
| 299 |
SQL plain functions |
packages/junior-github/src/pull-request-outcomes/store.ts |
| 205 |
SQL plain functions |
packages/junior/src/chat/event-tasks/store.ts |
| 131 |
SQL |
packages/junior/src/personal-tokens/store.ts |
| 128 |
SQL plain functions |
packages/junior-github/src/issue-outcomes/store.ts |
| 145 |
interface only |
packages/junior/src/chat/conversations/store.ts |
File-length exceptions already call out the two primary outliers and say “split by storage concern”:
scripts/file-length-exceptions.mjs → scheduled-tasks/store.ts, junior-memory/src/store.ts
Object facades vs plain functions
Object/create*Store edges still present:
ConversationStore / createSqlStore / event + message-search store factories — real DI across task-execution
SchedulerStore / createSchedulerSqlStore / createSchedulerStore — dual backend leftover
MemoryStore / createMemoryStore — convenience bag over closed-over context, not a second backend
UserTokenStore — credential edge, not SQL table CRUD
Plain-function SQL modules already in good shape:
event-tasks/store.ts
workspaces/store.ts
- github
pull-request-outcomes/store.ts, issue-outcomes/store.ts
- many direct table readers outside stores (
api/people/*, chat/tasks/read.ts, execution-stats.ts, etc.; ~230 direct schema references outside store/sql modules)
Scheduler dual backend
createSchedulerStore(state) appears production-dead; only packages/junior/tests/unit/scheduler-state-index.test.ts calls it.
- Production/eval paths use
createSchedulerSqlStore(getDb()) from heartbeat, tools, tasks read, evals.
- File still contains both state and SQL implementations plus shared claim/run logic (~2k lines).
Why thin helpers keep appearing
Several tables store a JSON document plus a few indexed columns:
- scheduler:
record jsonb + title/status/nextRunAtMs (db/schema/scheduled-tasks.ts)
- event tasks:
task jsonb + title (db/schema/event-tasks.ts)
That encourages getX wrappers that only do select … → parseRow. Those wrappers are not free architecture; keep them only when many callers share the same parse/filter rule.
Out of scope for this cleanup
- Redis/state stores that are not SQL facades (
agent-dispatch, resource-events, mailbox/lease task-execution/store.ts) unless a later pass wants rename-only consistency
- Broad schema normalization of all jsonb document columns (helpful later; not required to delete facades)
- Rewriting conversation DI (
ConversationStore) just to avoid the word “store”
Pointed suggestions
P0 — scheduled tasks
- Delete or fully isolate the PluginState backend (
createSchedulerStore / operational state store) if still test-only.
- Remove
SchedulerStore as the default call shape. Heartbeat/tools/read should not do createSchedulerSqlStore(db).getTask(...).
- Inline trivial task/run reads and writes at call sites with Drizzle against
juniorSchedulerTasks / juniorSchedulerRuns.
- Keep one named function for the real multi-step rule: due-run claim under lock (today
claimDueRun). Same for any terminal run transition that must stay atomic.
- Share
parseSqlTaskRow / title+record merge only if multiple modules need the exact same decode.
- Drop the file-length exception when the god file is gone.
P1 — memory plugin
- Stop routing tools/recall/process-session through
createMemoryStore(...).method() as the main API.
- Keep extracted functions only where domain rules live:
- create path: idempotency, dedupe, preference supersession, embedding write
- search/recall path: hybrid retrieval + ranking gates
- Simple list/get/archive paths can be plain queries in the caller if they stay simple.
- Leave plugin schema ownership in
junior-memory/src/db/schema.ts; do not move memory SQL into core src/db/.
- Split the 1.6k file by those domain concerns after the object bag is gone; remove the exception entry.
P2 — thin SQL modules / naming
- Revisit one-liner getters like
getEventTask / getWorkspace only if a caller needs a different projection; do not mass-delete working plain functions just to inline everywhere.
- Prefer feature filenames that name the domain (
tasks.ts, claim.ts) over mechanical store.ts when touching a file anyway (policies/interface-design.md).
- Do not add an
sql/ subdirectory unless a feature accumulates many SQL modules the way conversations already has.
P3 — conversations SQL class
- Keep
ConversationStore while task-execution DI needs it.
- Continue shrinking
conversations/sql/store.ts by moving distinct concerns into existing sibling modules (bindings, participants, history, etc.).
- Do not use conversations as the template for every feature.
Guardrails while cleaning
- Hard cutover for internal APIs; search every consumer (
createSchedulerSqlStore, createMemoryStore, SchedulerStore, MemoryStore).
- Prove behavior at existing integration/component edges; do not add one unit test per former store method.
- Avoid new abstraction names (“repository”, “transaction script”, “unit of work”).
- Prefer deleting code over relocating the same CRUD surface into more files.
Suggested sequence
- Scheduler: remove dead state backend +
SchedulerStore object API.
- Scheduler: inline trivial SQL; keep claim/atomic run transitions as named functions.
- Memory: replace
createMemoryStore bag with direct ops for create/search; inline simple paths.
- Opportunistic renames / thin-wrapper cleanup in event-tasks, workspaces, github outcomes only when already touching those files.
- Optional later: reduce jsonb-document reliance so fewer parse helpers are needed.
Done when
- No production feature requires
createXStore(db) for ordinary SQL CRUD.
scheduled-tasks/store.ts and junior-memory/src/store.ts are gone or under the 1,000-line limit without exceptions for “storage concern” bags.
- New SQL code defaults to feature-local Drizzle; helpers exist only for invariants or shared mapping.
- Conversation DI remains explicit and unbroken.
Requested by David Cramer.
--
View Junior Session [Sentry]
SQL access is growing through large
storemodules and object facades. Prefer feature-local Drizzle against tables. Keep helpers only for real invariants or shared non-trivial mapping.Intent
Agreed direction from design review:
get/list/create/save/delete) that only wrap one query.*Storefactories only for a real edge (today: conversation DI). Do not grow new ones for normal features.repositories/, DAO framework, or new persistence vocabulary.src/db/stays connection + schema. Feature/plugin folders own ops next to their tools and runtime code.This matches existing healthy code (
event-tasks,workspaces, github outcomes, manyapi/readers) and existing policy (policies/interface-design.md,policies/correctness-complexity.md, chat README ownership by feature).Audit
store.tsinventory (current main)packages/junior/src/chat/scheduled-tasks/store.tspackages/junior-memory/src/store.tsConversationStorepackages/junior/src/chat/conversations/sql/store.tspackages/junior/src/chat/agent-dispatch/store.tspackages/junior/src/chat/resource-events/store.tspackages/junior/src/chat/attachments/store.tspackages/junior/src/chat/task-execution/store.tspackages/junior/src/chat/agent-invocations/store.tspackages/junior/src/chat/workspaces/store.tspackages/junior/src/chat/artifacts/store.tspackages/junior-github/src/pull-request-outcomes/store.tspackages/junior/src/chat/event-tasks/store.tspackages/junior/src/personal-tokens/store.tspackages/junior-github/src/issue-outcomes/store.tspackages/junior/src/chat/conversations/store.tsFile-length exceptions already call out the two primary outliers and say “split by storage concern”:
scripts/file-length-exceptions.mjs→scheduled-tasks/store.ts,junior-memory/src/store.tsObject facades vs plain functions
Object/
create*Storeedges still present:ConversationStore/createSqlStore/ event + message-search store factories — real DI across task-executionSchedulerStore/createSchedulerSqlStore/createSchedulerStore— dual backend leftoverMemoryStore/createMemoryStore— convenience bag over closed-over context, not a second backendUserTokenStore— credential edge, not SQL table CRUDPlain-function SQL modules already in good shape:
event-tasks/store.tsworkspaces/store.tspull-request-outcomes/store.ts,issue-outcomes/store.tsapi/people/*,chat/tasks/read.ts,execution-stats.ts, etc.; ~230 direct schema references outside store/sql modules)Scheduler dual backend
createSchedulerStore(state)appears production-dead; onlypackages/junior/tests/unit/scheduler-state-index.test.tscalls it.createSchedulerSqlStore(getDb())from heartbeat, tools, tasks read, evals.Why thin helpers keep appearing
Several tables store a JSON document plus a few indexed columns:
recordjsonb +title/status/nextRunAtMs(db/schema/scheduled-tasks.ts)taskjsonb +title(db/schema/event-tasks.ts)That encourages
getXwrappers that only doselect … → parseRow. Those wrappers are not free architecture; keep them only when many callers share the same parse/filter rule.Out of scope for this cleanup
agent-dispatch,resource-events, mailbox/leasetask-execution/store.ts) unless a later pass wants rename-only consistencyConversationStore) just to avoid the word “store”Pointed suggestions
P0 — scheduled tasks
createSchedulerStore/ operational state store) if still test-only.SchedulerStoreas the default call shape. Heartbeat/tools/read should not docreateSchedulerSqlStore(db).getTask(...).juniorSchedulerTasks/juniorSchedulerRuns.claimDueRun). Same for any terminal run transition that must stay atomic.parseSqlTaskRow/ title+record merge only if multiple modules need the exact same decode.P1 — memory plugin
createMemoryStore(...).method()as the main API.junior-memory/src/db/schema.ts; do not move memory SQL into coresrc/db/.P2 — thin SQL modules / naming
getEventTask/getWorkspaceonly if a caller needs a different projection; do not mass-delete working plain functions just to inline everywhere.tasks.ts,claim.ts) over mechanicalstore.tswhen touching a file anyway (policies/interface-design.md).sql/subdirectory unless a feature accumulates many SQL modules the way conversations already has.P3 — conversations SQL class
ConversationStorewhile task-execution DI needs it.conversations/sql/store.tsby moving distinct concerns into existing sibling modules (bindings,participants,history, etc.).Guardrails while cleaning
createSchedulerSqlStore,createMemoryStore,SchedulerStore,MemoryStore).Suggested sequence
SchedulerStoreobject API.createMemoryStorebag with direct ops for create/search; inline simple paths.Done when
createXStore(db)for ordinary SQL CRUD.scheduled-tasks/store.tsandjunior-memory/src/store.tsare gone or under the 1,000-line limit without exceptions for “storage concern” bags.Requested by David Cramer.
--
View Junior Session [Sentry]