PLA-51: give storage its own owner instead of the Sessions registry - #113
Merged
Conversation
The process-wide SQLite bootstrap lived inside sessions/registry.ts, so every non-session module reached its database handle through Sessions. Pure code motion, no behaviour change: - shared/db.ts owns the connection, upgrade safety and initDb(). It is a composition root: it sequences each module's migrations in the exact order the old initialize transaction used, and holds no DDL of its own. - sessions/migrate.ts owns the Sessions DDL, its idempotent migrations and the callback_deliveries row model those migrations validate against. - shared/sanitize.ts owns stripControlChars/hasControlBytes. Being dependency-free, it also retires the duplicate hasControlBytes that mcp/knowledge-tools.ts kept only to avoid importing better-sqlite3. - Every initDb importer now imports from shared/db.js; registry.ts is one of them. The Todo-doctrine prose block is gone. The busy-retry helpers stay with the migrations rather than being injected into migrateCallbackDeliveriesSchema, which keeps every moved signature and body unchanged and leaves no module-scope cycle among the migrate modules. Verified: initDb() against a fresh throwaway home produces a byte-identical sqlite_master dump at base and at HEAD.
Three test files landed on main after this branch's base and still reach initDb()/__closeDbForTest() through sessions/registry.js, which no longer re-exports them. git merges them clean, tsc does not. - gateway/__tests__/budgets.test.ts (#116) — 2 call sites - gateway/__tests__/dispatch-route.test.ts (Todo Dispatcher) — 1 call site - sessions/__tests__/activity-schema-drop.test.ts (#112) — 2 call sites Import-path only: every call site just opens and seeds the database, with no mocking or spying on the registry module, so the behaviour under test is unchanged. Also carries #112's dropActivityLedgerSchema() into shared/db.ts, the new home of the boot migration transaction it runs inside.
hristo2612
force-pushed
the
simplify/PLA-51-storage-ownership
branch
from
August 3, 2026 09:36
491e160 to
fe358e3
Compare
hristo2612
added a commit
that referenced
this pull request
Aug 3, 2026
#113 relocated stripControlChars/hasControlBytes out of sessions/registry.ts into shared/sanitize.ts and left no re-export, so the absorbed knowledge code in notes/store.ts must import from the new owner. Also retarget the stale knowledge/store.ts reference in the mcp/knowledge-tools.ts header.
hristo2612
added a commit
that referenced
this pull request
Aug 3, 2026
knowledge/store.ts is deleted and its behaviour absorbed into notes/store.ts, leaving one owner for reads under knowledge/ and docs/. Path handling is not loosened. readKnowledgeFile keeps the whole battery: 300-char cap, control-byte reject rather than strip, POSIX and Win32 absolute-path reject, backslash reject, empty/dot/dotdot segment reject, realpath-into-realpath-home containment, isFile. The containment predicate moved to the Notes-side isRealpathContained, which differs from the old startsWith form only when candidate equals root, and that case is refused explicitly. Search is strictly stricter than before: walkMarkdown skips symlinks via lstat and openRegularFile uses O_NOFOLLOW, replacing a realpath-follows-then-check. One behaviour change the original description did not declare, verified by running both implementations against the same fixture: the old search gated filenames through a charset regex, and the new walker does not. Files with spaces, non-ASCII names, or names longer than 250 characters are now searchable. This is a widening inside the same two allowlisted roots, those files were already reachable through list_notes and read_note via the same walker, and it arrives paired with the stronger symlink regime, so it is acceptable. It is stated here so it is a decision rather than a side effect. One cosmetic consequence: a deeply nested hit path can now exceed readKnowledgeFile's 300-char cap, making that hit un-readable. Follow-up commit retargets stripControlChars and hasControlBytes to shared/sanitize.ts, where #113 moved them without leaving a re-export. The packages/web suite was not green during gating, but bare main fails at the same rate with a barely-overlapping set of waitFor timeouts, and this PR changes zero web files. Tracked as #123.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Selected area
packages/jinn/src/sessions/registry.ts— the Sessions registry, which also owned theprocess-wide SQLite bootstrap for every other module in the gateway.
Evidence of over-engineering / blended concerns (measured at base
23df25ed)registry.tswas 3,985 lines and held three unrelated responsibilities:dbsingleton,initDb(), diskpreflight, pre-migration backup/prune, DB-version sidecar, and the SQLite
busy-retry helpers.
initializeimmediatetransaction created and migrated Sessions, Messages,
messages_fts,queue_items, callback deliveries, Files and chat pins, and calledmigrateWorkItemsSchema(Todos) andmigrateActivitySchema(Activity).Consequences of the blend:
initDb. Every non-session module — work-items (×9),activity (×2), gateway, cron, cli, knowledge — reached its database handle by
importing from the Sessions module. Storage ownership was expressed as a
dependency on an unrelated domain.
knowledge/store.ts,mcp/knowledge-tools.ts,gateway/files.tsandgateway/api.tspulledstripControlChars/hasControlBytesout of a3,985-line session-CRUD file.
registry, documenting a domain the file does not own.
following it in the third:
work-items/migrate.tsandactivity/migrate.tsalready exist, and
workflows/repository-migrations.tsalready exportsopenWorkflowDatabase()owning its own store.Fixed constraint budget
Frozen in
PLAN.mdbefore implementation:netLineDeltamaxFilesTouchedmaxNewFilesmaxFileLinesMeasured budget output
Command run verbatim from
PLAN.mdat the pushed head:All four inside budget.
What was deleted or clarified
Three new files, each owning one concern:
packages/jinn/src/shared/db.tsdbsingleton andinitDb(), plus disk preflight, pre-migration backup/prune, version sidecar and the busy-retry helpers. A composition root: it sequences each owner's migration but contains no DDL and no domain logic.packages/jinn/src/sessions/migrate.tsCREATE_*constants andmigrateMessagesSchema/migrateSessionsSchema/migrateFtsSchema/migrateQueueItemsSchema/migrateCallbackDeliveriesSchema.packages/jinn/src/shared/sanitize.tsstripControlCharsandhasControlBytes, moved verbatim.Removed from
registry.ts: 1,174 lines deleted, 4 added — 3,985 → 2,815 lines.initDbis now declared in exactly one place (shared/db.ts:136).registry.tsimports it like every other module (
registry.ts:5) and does not re-export it,so nothing reaches storage through Sessions any more.
migrateWorkItemsSchemaandmigrateActivitySchemano longer appear inregistry.ts;shared/db.tssequences them from their owning modules'migrate.ts.initDbimporters were re-pointed mechanically — same exported name,one import line changed per file.
Import-cycle hazard, resolved without a cycle.
migrateCallbackDeliveriesSchemacalled
runImmediateMigrationWithRetry, which would have created ashared/db.ts↔sessions/migrate.tscycle. The transaction runner is passed inas a plain function parameter from the composition root instead. Verified: none of
sessions/migrate.ts,work-items/migrate.ts,activity/migrate.tsimportsshared/db.jsorsessions/registry.js.Deliberately not done (the constraint budget forbade it): no registration or
plugin framework, no
registerMigration()indirection, no renamed public functions,no new dependencies or config options.
shared/db.tscalls the three owners directly.Behaviour: pure code motion plus import re-pointing. No SQL text changed, no
migration-order changed — the
initializetransaction runs the same steps in thesame sequence inside one
runImmediateMigrationWithRetrycall.Acceptance greps at the pushed head
Test results
Turbo replays cached logs across worktrees, so every gate below was run with the
cache bypassed (
Cached: 0 cached) — these are real executions at the pushed head,after the final commit.
pnpm typecheck --forcepnpm test --forcepnpm build— the root script isturbo build && node scripts/sync-web-dist.mjs;appending
--forcetopnpm buildpasses the flag to the second command, so thechain was expanded to force the build itself:
No test was deleted; tests were only re-pointed at the new import paths.
Todo PLA-51. Base
23df25ed62acfa162a39923638eb14dbbf1d8bea, head491e160e137bd6e759b69c3358658c354db76b26. For operator review — not merged.