okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
510b062
main
existing storage migrations
migration test inventory
Keep these tests while the corresponding old format remains supported. A fresh-install scenario cannot exercise an upgrade from that format. Retire a test only together with its migration or compatibility reader and an explicit decision to stop supporting that upgrade path.
| Supported data | Implementation owner | Coverage |
|---|---|---|
| Older core and extension schemas, inline images, and session preferences | Core migrations, extension-contributed migrations, and preference import | startup scenarios, migration ordering |
| Older transcript metadata, titles, and timestamps | Conversation columns and transcript classification | conversation tests, metadata backfill |
| Inline image records and TEXT image payloads | Image-store and cell-image migrations | image-store tests, cell image readers |
| Legacy item-count compaction state | Rolling's lazy compatibility reader | rolling tests |
| Older Python namespace snapshots | Kernel snapshot reader | kernel state scenarios |
| Flat provider configuration and relocated credentials | Provider configuration and credential migration | settings scenarios, startup integration, credential migration |
| Older layouts inspected offline | Daemon storage helper compatibility reader | storage compatibility scenarios |
These are groups within feature suites, not a separate test lane. The normal gate runs them. Compatibility with another running daemon build and provider response shapes has its own contracts; those tests are not retired merely because a storage migration is removed.
core implementations live under src/albedo/daemon/migrations/;
extension implementations live under their own migrations/ directories and
contribute MigrationPlugin values. the host applies them, without importing
extension-specific migration modules.
startup order
server.main first claims ALBEDO_HOME/daemon.lock with BEGIN EXCLUSIVE.
Offline cleanup in cli/internal/storage takes the same lock after confirmation,
before it revalidates candidates. Its Gleam helper holds ownership through file
deletion and vacuum. Daemon startup refuses while maintenance owns the home,
before it touches application storage. Neither owner commits the lock transaction,
changes its journal mode, or replaces the lock file. Cleanup checks health for
diagnostics; only exclusive ownership permits offline mutation. CLI cancellation
or death stops the helper and releases ownership. Completed deletions remain
deleted if later deletion or vacuum fails.
The client invokes albedo-daemon storage inspect <database> <scratch-directory>
and albedo-daemon storage maintain <home> through the same local executable
selection as daemon startup. daemon/storage_cli owns both helper modes and
dispatches before server startup. Inspection copies the database and WAL into
the client's private scratch directory, compares native file identities before
and after copying, then reads the snapshot without schema initialization or
migrations. It supports older layouts independently of the live projection.
The client removes scratch files and retains its filesystem accounting and
cleanup revalidation. Helper metadata uses the native timestamp resolution.
Maintenance exchanges newline-delimited JSON with the client. It captures candidate identities under the home lock, reports readiness, and waits for the client's authorization after revalidation. An EOF watcher terminates the helper VM during authorized work, including vacuum. Go keeps the pipe open until the helper exits and accepts a result only with a successful exit. These helper modes do not require Python or create a daemon discovery record.
runtime.start installs the extension registry before server.prepare_storage.
for each installed extension, extension.install first calls its table
initialiser, then applies its contributed SchemaMigration callbacks on the
store-owned SQLite connection, in plugin order. work contributes cwd.apply;
paperclips contributes title.apply, scope.apply, and reply.apply.
The links extension's ledger.initialise calls links.apply to create
workspace_links, then creates its revision tables and triggers. See
linked workspaces. The mcp extension's initialiser creates mcp_catalogues (see mcp); python contributes kernel_links.apply, the detached kernel's link tables (see kernel), which link.create also ensures for embedded runtimes that boot kernels without the extension. their ledgers create tables but do not run
upgrades themselves. all installed owners upgrade, even when disabled for
sessions; session selection and live reload never rerun migrations.
server.prepare_storage then runs before sessions start:
conversation.initialisecreates the core tables, then callsconversation_columns.apply: the existing session columns first, transcript columns second. The same core initialization createsoperations,pending_inputs,submission_events, andcontinuation_markersthroughoperations.schema.operations.initialiseupgrades historical display and continuation metadata to unique(session,operation_id)keys in one transaction, preserving their rowids. Admission receipts and pending inputs still require globally unique input identities. It then runs its existing domain-local session recovery (missing activity/title from transcript), then createssessions_activity.- quota, mail, and family initialise their tables, unchanged.
migrations.run(ledger, backup)runs the coreimage_store.run, which first externalizes legacy transcript images, then converts image-table TEXT data to decoded BLOB. It then runstranscript_classes.runto classify old transcript rows.runtime.migrate(host, backup)then collects installed extensions'DataMigrationcallbacks in registry/plugin order and applies them with the same backup path. python contributescell_images.run, which requires the core image and marker tables. callbacks report named row counts; the server logs nonzero extension results asmigration <name>: <n> rows. a failed callback stops startup and prevents later migrations from running.- the existing best-effort filesystem credential migration runs; then provider assignment from legacy configuration runs, unchanged.
schema creation is still owned by each subsystem. work/migrations/cwd.apply
checks PRAGMA table_info(work), adds the existing reserved __albedo_legacy__
cwd default when absent, then creates work_cwd_id;
paperclips/migrations/title.apply adds the existing non-null empty-default title
column, paperclips/migrations/scope.apply drops the now-unused workspace
index left from the cwd-scoped vent era, and
paperclips/migrations/reply.apply adds the non-null empty-default reply
column that makes a /paperclips answer durable. rolling/migrations/observation.apply
drops the observation table rolling used to write and read straight back. these upgrades run immediately
after their owner's table creation, so later extension initialisers can use
the upgraded schema. embedding hosts call
runtime.migrate after preparing core storage and before opening sessions.
compatibility and interruption
conversation_columns.applyadds nullabletranscript.row_class, checked againstuser,image_fit, andother. Live writes set it in the same transaction as the payload; forks copy it with the retained prefix.transcript_classes.runclassifies only NULL rows in 128-row transactions; completed batches survive interruption. An unreadable payload, here or inconversation's startup indexing, is healed byconversation.heal_in: its bytes move totranscript_quarantine(one row per sequence ID, deleted with its session), and the row becomes a user-role system note saying it was set aside, classeduser. Startup continues. The backfill adds no migration marker. It runs after the image migration.- a tool result saved after rows that are neither calls nor results (a
capability note written while the call ran, then the result recovered after a
restart) moves back beside its call: providers reject a call whose result does
not follow it.
tool_results.settleis the one rule.conversationapplies it when it commits results,session_historyapplies it to the live history and to a transcript when it loads, and saves the moves from that load. A move keeps every seq and moves row contents between them, and remapstranscript_calls,submission_events,continuation_markers,provider_requests,operations.committed_seq, andtranscript_quarantinewith them. LCM source ranges are not remapped. - the request builder pairs the same way at the request boundary:
tool_results.pairgives every call a request's provider items make exactly one tool result right after the block that made it. A late result moves there, a call no result answers getstool_results.unpaired_result("interrupted, no output"), and a result whose call nothing carries — one a projection dropped, or a second answer to a call already answered — stays out of the request: providers reject atool_usewithout itstool_resultand atool_resultwithout itstool_usealike. The transcript is left as it is: the pairing is applied again to every request, so a session an older fold left unpaired sends valid history without a repair. transcript_pending_classindexes unclassified sequence IDs.transcript_usersindexes(session,seq)for user and image-fit rows;transcript_fitsindexes(session,seq)for image-fit rows alone. Completed databases have no pending entries, so subsequent startups do not decode transcript payloads for classification.- the existing
migrations(name,applied_at)table remains unchanged. the exact markers areimage_storeandcell_images, recorded withunixepoch(). transcript migration skips when its marker exists; cell migration skips when its marker exists, and does nothing without a cells table. - TEXT-to-BLOB migration deliberately has no marker. it checks
typeof(data)='text'on every run and converts pending rows, even when theimage_storemarker already exists. invalid legacy base64 fails without replacing that row. - transcript and cell scans retain their original cursor order and 16-row pages; BLOB conversion retains its 16-row limit. each page uses the existing transaction helper. there is no new all-migrations transaction or transaction policy change. markers are written at the original completion points, and interrupted runs recheck remaining rows.
- core
backup.gleamand python's cell migration preserve the two existing backup call patterns: transcript and BLOB migration check for an existing file before directory creation andVACUUM INTO, retaining theimage store backup failed:error prefix; cell migration ensures the directory first and copies on the first page containing inline images, outside its transaction. the server still supplies one sharedhome/backups/albedo-before-image-store-<timestamp>.sqlitepath. no backup is overwritten and backup triggering is unchanged (the transcript candidate check can include tool outputs without inline images). - live writes and transcript migration share
image_payloads.insert, the unchanged decoded-BLOB insertion helper. image hashes, serialized formats, image validation, and legacy inline fallback reads are unchanged. the existing Erlang conversion and cell packing functions remain the FFI implementation.
migration inventory
| previous implementation | current implementation |
|---|---|
conversation.initialise: session/transcript add_columns | migrations/conversation_columns.apply |
| transcript row classification | migrations/transcript_classes.run, after core image upgrades |
images.migrate / migrate_legacy / transcript pages | migrations/image_store.run / migrate_legacy / transcript pages |
images.migrate_blobs / BLOB pages | migrations/image_store.migrate_blobs / BLOB pages |
images.backup_before_migration | migrations/backup.image_store |
python/cells.migrate_images / cell pages | python/migrations/cell_images.run / cell pages |
python/cells.backup_before_migration | python/migrations/cell_images.backup_before_migration |
work/ledger.initialise: cwd alteration and index | work/migrations/cwd.apply, contributed by work |
paperclips/ledger.initialise: title addition | paperclips/migrations/title.apply, contributed by paperclips |
paperclips/ledger.initialise: paperclips_cwd index creation | dropped from the schema; paperclips/migrations/scope.apply removes it from existing stores |
paperclips/ledger.initialise: reply column | paperclips/migrations/reply.apply, added with the global ledger |
rolling.initialise: rolling_compaction_observation creation | dropped from the schema; rolling/migrations/observation.apply removes it from existing stores |
images.migrate and cells.migrate_images are removed rather than wrapped:
the migration implementations are separate from the live domain readers and
writers. startup discovers extension upgrades through plugins, not direct calls
to their implementation modules.
intentionally not extracted
- session recovery stays in
conversation: it uses the domain's transcript decoding/title helpers and still runs at exactly the same point. core schema bootstrap and index creation remain with their owners. - rolling compaction's legacy item-count-to-user-cut conversion remains lazy in
rolling/extension.resume. it needs the current prepared history and source, validates the old fingerprint (including legacy image payload fingerprints), and only writes a cut when the history matches and its tail begins with a user message. treating this as an unconditional startup migration would change its timing and semantics; its projections are untouched. - credentials migration is filesystem/auth, not SQLite. legacy configuration's provider assignment likewise remains in the existing server startup path.
- no zstd, compression, schema-version, offline storage upgrade, new dependency, or unrelated transaction fix is part of this refactor.
durable operation admissions
operations owns receipt retention and pending delivery state on the shared
store connection. Session creation commits its session row and accepted receipt
in one immediate transaction. Submission admission commits its receipt and
pending input together. Transcript consumption moves pending metadata into
submission_events, records its committed position, and removes pending input
in the same transaction as the transcript rows. Failed statements roll back the
whole transaction, including a failed commit.
Receipts retain the original admission result. They do not reference the session
row, so deletion cancels pending inputs and leaves receipts available. Terminal
receipts expire seven days after acceptance for creation or rejection, or after
commit or cancellation for submissions. Pending receipts do not expire. Cleanup
removes at most 128 expired receipts per call. Durable display metadata in
submission_events remains until session deletion, including operation IDs,
display text, and echo identity. Continuations commit a continuation_markers
row in the same transaction without adding a user-text transcript row. These
markers also remain until session deletion and survive receipt cleanup.
Forks copy this historical metadata with its original input IDs, timestamps,
and turn IDs at the branch's remapped transcript positions. A continuation
follows its anchor row, so only markers anchored strictly before the selected
checkpoint belong to the fork. Forks copy no admission receipts or live runs.
Acceptance guarantees stored input or its committed or cancelled outcome. Commit means the input reached the transcript. Neither guarantee promises that assistant generation, tools, or external effects occur exactly once.
1# existing storage migrations 2 3## migration test inventory 4 5Keep these tests while the corresponding old format remains supported. A 6fresh-install scenario cannot exercise an upgrade from that format. Retire a 7test only together with its migration or compatibility reader and an explicit 8decision to stop supporting that upgrade path. 9 10| Supported data | Implementation owner | Coverage | 11| --- | --- | --- | 12| Older core and extension schemas, inline images, and session preferences | Core migrations, extension-contributed migrations, and preference import | [startup scenarios](../test/e2e/migrations_test.py), [migration ordering](../test/harness/migration_test.gleam) | 13| Older transcript metadata, titles, and timestamps | Conversation columns and transcript classification | [conversation tests](../test/daemon/conversation_test.gleam), [metadata backfill](../test/daemon/transcript_sources_test.gleam) | 14| Inline image records and TEXT image payloads | Image-store and cell-image migrations | [image-store tests](../test/daemon/image_store_test.gleam), [cell image readers](../test/harness/tool_image_test.gleam) | 15| Legacy item-count compaction state | Rolling's lazy compatibility reader | [rolling tests](../test/harness/rolling_test.gleam) | 16| Older Python namespace snapshots | Kernel snapshot reader | [kernel state scenarios](../test/e2e/kernel_state_test.py) | 17| Flat provider configuration and relocated credentials | Provider configuration and credential migration | [settings scenarios](../test/e2e/settings_test.py), [startup integration](../test/e2e/integration_test.py), [credential migration](../test/e2e/credentials_test.py) | 18| Older layouts inspected offline | Daemon storage helper compatibility reader | [storage compatibility scenarios](../test/e2e/storage_test.py) | 19 20These are groups within feature suites, not a separate test lane. The normal 21gate runs them. Compatibility with another running daemon build and provider 22response shapes has its own contracts; those tests are not retired merely 23because a storage migration is removed. 24 25core implementations live under `src/albedo/daemon/migrations/`; 26extension implementations live under their own `migrations/` directories and 27contribute `MigrationPlugin` values. the host applies them, without importing 28extension-specific migration modules. 29 30## startup order 31 32`server.main` first claims `ALBEDO_HOME/daemon.lock` with `BEGIN EXCLUSIVE`. 33Offline cleanup in `cli/internal/storage` takes the same lock after confirmation, 34before it revalidates candidates. Its Gleam helper holds ownership through file 35deletion and vacuum. Daemon startup refuses while maintenance owns the home, 36before it touches application storage. Neither owner commits the lock transaction, 37changes its journal mode, or replaces the lock file. Cleanup checks health for 38diagnostics; only exclusive ownership permits offline mutation. CLI cancellation 39or death stops the helper and releases ownership. Completed deletions remain 40deleted if later deletion or vacuum fails. 41 42The client invokes `albedo-daemon storage inspect <database> <scratch-directory>` 43and `albedo-daemon storage maintain <home>` through the same local executable 44selection as daemon startup. `daemon/storage_cli` owns both helper modes and 45dispatches before server startup. Inspection copies the database and WAL into 46the client's private scratch directory, compares native file identities before 47and after copying, then reads the snapshot without schema initialization or 48migrations. It supports older layouts independently of the live projection. 49The client removes scratch files and retains its filesystem accounting and 50cleanup revalidation. Helper metadata uses the native timestamp resolution. 51 52Maintenance exchanges newline-delimited JSON with the client. It captures 53candidate identities under the home lock, reports readiness, and waits for the 54client's authorization after revalidation. An EOF watcher terminates the helper 55VM during authorized work, including vacuum. Go keeps the pipe open until the 56helper exits and accepts a result only with a successful exit. These helper 57modes do not require Python or create a daemon discovery record. 58 59`runtime.start` installs the extension registry before `server.prepare_storage`. 60for each installed extension, `extension.install` first calls its table 61initialiser, then applies its contributed `SchemaMigration` callbacks on the 62store-owned SQLite connection, in plugin order. work contributes `cwd.apply`; 63paperclips contributes `title.apply`, `scope.apply`, and `reply.apply`. 64The links extension's `ledger.initialise` calls `links.apply` to create 65`workspace_links`, then creates its revision tables and triggers. See 66[linked workspaces](workspaces.md#linked-workspaces). The mcp extension's initialiser creates `mcp_catalogues` (see [mcp](mcp.md#saved-catalogues)); python contributes `kernel_links.apply`, the detached kernel's link tables (see [kernel](kernel.md)), which `link.create` also ensures for embedded runtimes that boot kernels without the extension. their ledgers create tables but do not run 67upgrades themselves. all installed owners upgrade, even when disabled for 68sessions; session selection and live reload never rerun migrations. 69 70`server.prepare_storage` then runs before sessions start: 71 721. `conversation.initialise` creates the core tables, then calls 73`conversation_columns.apply`: the existing session columns first, transcript 74 columns second. The same core initialization creates `operations`, 75 `pending_inputs`, `submission_events`, and `continuation_markers` through 76 `operations.schema`. 77 `operations.initialise` upgrades historical display and continuation metadata 78 to unique `(session,operation_id)` keys in one transaction, preserving their 79 rowids. Admission receipts and pending inputs still require globally unique 80 input identities. It then runs its existing domain-local session recovery 81 (missing activity/title from transcript), then creates `sessions_activity`. 82 2. quota, mail, and family initialise their tables, unchanged. 833. `migrations.run(ledger, backup)` runs the core `image_store.run`, which first 84externalizes legacy transcript images, then converts image-table TEXT data 85 to decoded BLOB. It then runs `transcript_classes.run` to classify old 86 transcript rows. `runtime.migrate(host, backup)` then collects installed 87 extensions' `DataMigration` callbacks in registry/plugin order and applies 88 them with the same backup path. python contributes `cell_images.run`, which 89 requires the core image and marker tables. callbacks report named row counts; 90 the server logs nonzero extension results as `migration <name>: <n> rows`. 91 a failed callback stops startup and prevents later migrations from running. 92 4. the existing best-effort filesystem credential migration runs; then provider 93assignment from legacy configuration runs, unchanged. 94 95 schema creation is still owned by each subsystem. `work/migrations/cwd.apply` 96checks `PRAGMA table_info(work)`, adds the existing reserved `__albedo_legacy__` 97cwd default when absent, then creates `work_cwd_id`; 98`paperclips/migrations/title.apply` adds the existing non-null empty-default title 99column, `paperclips/migrations/scope.apply` drops the now-unused workspace 100index left from the cwd-scoped vent era, and 101`paperclips/migrations/reply.apply` adds the non-null empty-default reply 102column that makes a /paperclips answer durable. `rolling/migrations/observation.apply` 103drops the observation table rolling used to write and read straight back. these upgrades run immediately 104after their owner's table creation, so later extension initialisers can use 105the upgraded schema. embedding hosts call 106`runtime.migrate` after preparing core storage and before opening sessions. 107 108## compatibility and interruption 109 110- `conversation_columns.apply` adds nullable `transcript.row_class`, checked 111against `user`, `image_fit`, and `other`. Live writes set it in the same 112transaction as the payload; forks copy it with the retained prefix. 113`transcript_classes.run` classifies only NULL rows in 128-row transactions; 114completed batches survive interruption. An unreadable payload, here or in 115`conversation`'s startup indexing, is healed by `conversation.heal_in`: its 116bytes move to `transcript_quarantine` (one row per sequence ID, deleted with 117its session), and the row becomes a user-role system note saying it was set 118aside, classed `user`. Startup continues. The backfill adds no migration 119marker. It runs after the image migration. 120- a tool result saved after rows that are neither calls nor results (a 121capability note written while the call ran, then the result recovered after a 122restart) moves back beside its call: providers reject a call whose result does 123not follow it. `tool_results.settle` is the one rule. `conversation` applies it 124when it commits results, `session_history` applies it to the live history and to 125a transcript when it loads, and saves the moves from that load. A move keeps 126every seq and moves row contents between them, and remaps `transcript_calls`, 127`submission_events`, `continuation_markers`, `provider_requests`, 128`operations.committed_seq`, and `transcript_quarantine` with them. LCM source 129ranges are not remapped. 130- the request builder pairs the same way at the request boundary: 131`tool_results.pair` gives every call a request's provider items make exactly 132one tool result right after the block that made it. A late result moves 133there, a call no result answers gets `tool_results.unpaired_result` 134("interrupted, no output"), and a result whose call nothing carries — one a 135projection dropped, or a second answer to a call already answered — stays 136out of the request: providers reject a `tool_use` without its `tool_result` 137and a `tool_result` without its `tool_use` alike. The transcript is left 138as it is: the pairing is applied again to every request, so a session an 139older fold left unpaired sends valid history without a repair. 140- `transcript_pending_class` indexes unclassified sequence IDs. 141`transcript_users` indexes `(session,seq)` for user and image-fit rows; 142`transcript_fits` indexes `(session,seq)` for image-fit rows alone. Completed 143databases have no pending entries, so subsequent startups do not decode 144transcript payloads for classification. 145- the existing `migrations(name,applied_at)` table remains unchanged. the exact 146markers are `image_store` and `cell_images`, recorded with `unixepoch()`. 147transcript migration skips when its marker exists; cell migration skips when 148its marker exists, and does nothing without a cells table. 149- TEXT-to-BLOB migration deliberately has no marker. it checks 150`typeof(data)='text'` on every run and converts pending rows, even when the 151`image_store` marker already exists. invalid legacy base64 fails without 152replacing that row. 153- transcript and cell scans retain their original cursor order and 16-row pages; 154BLOB conversion retains its 16-row limit. each page uses the existing 155transaction helper. there is no new all-migrations transaction or transaction 156policy change. markers are written at the original completion points, and 157interrupted runs recheck remaining rows. 158- core `backup.gleam` and python's cell migration preserve the two existing 159backup call patterns: transcript and BLOB migration check for an existing file before directory creation and 160`VACUUM INTO`, retaining the `image store backup failed: ` error prefix; cell 161migration ensures the directory first and copies on the first page containing 162inline images, outside its transaction. the server still supplies one shared 163`home/backups/albedo-before-image-store-<timestamp>.sqlite` path. no backup is 164overwritten and backup triggering is unchanged (the transcript candidate 165check can include tool outputs without inline images). 166- live writes and transcript migration share `image_payloads.insert`, the 167unchanged decoded-BLOB insertion helper. image hashes, serialized formats, 168image validation, and legacy inline fallback reads are unchanged. the existing 169Erlang conversion and cell packing functions remain the FFI implementation. 170 171## migration inventory 172 173| previous implementation | current implementation | 174| --- | --- | 175| `conversation.initialise`: session/transcript `add_columns` | `migrations/conversation_columns.apply` | 176| transcript row classification | `migrations/transcript_classes.run`, after core image upgrades | 177| `images.migrate` / `migrate_legacy` / transcript pages | `migrations/image_store.run` / `migrate_legacy` / transcript pages | 178| `images.migrate_blobs` / BLOB pages | `migrations/image_store.migrate_blobs` / BLOB pages | 179| `images.backup_before_migration` | `migrations/backup.image_store` | 180| `python/cells.migrate_images` / cell pages | `python/migrations/cell_images.run` / cell pages | 181| `python/cells.backup_before_migration` | `python/migrations/cell_images.backup_before_migration` | 182| `work/ledger.initialise`: cwd alteration and index | `work/migrations/cwd.apply`, contributed by work | 183| `paperclips/ledger.initialise`: title addition | `paperclips/migrations/title.apply`, contributed by paperclips | 184| `paperclips/ledger.initialise`: `paperclips_cwd` index creation | dropped from the schema; `paperclips/migrations/scope.apply` removes it from existing stores | 185| `paperclips/ledger.initialise`: reply column | `paperclips/migrations/reply.apply`, added with the global ledger | 186| `rolling.initialise`: `rolling_compaction_observation` creation | dropped from the schema; `rolling/migrations/observation.apply` removes it from existing stores | 187 188`images.migrate` and `cells.migrate_images` are removed rather than wrapped: 189the migration implementations are separate from the live domain readers and 190writers. startup discovers extension upgrades through plugins, not direct calls 191to their implementation modules. 192 193## intentionally not extracted 194 195- session recovery stays in `conversation`: it uses the domain's transcript 196decoding/title helpers and still runs at exactly the same point. core schema 197bootstrap and index creation remain with their owners. 198- rolling compaction's legacy item-count-to-user-cut conversion remains lazy in 199`rolling/extension.resume`. it needs the current prepared history and source, 200validates the old fingerprint (including legacy image payload fingerprints), 201and only writes a cut when the history matches and its tail begins with a user 202message. treating this as an unconditional startup migration would change its 203timing and semantics; its projections are untouched. 204- credentials migration is filesystem/auth, not SQLite. legacy configuration's 205provider assignment likewise remains in the existing server startup path. 206- no zstd, compression, schema-version, offline storage upgrade, 207new dependency, or unrelated transaction fix is part of this refactor. 208 209## durable operation admissions 210 211`operations` owns receipt retention and pending delivery state on the shared 212store connection. Session creation commits its session row and accepted receipt 213in one immediate transaction. Submission admission commits its receipt and 214pending input together. Transcript consumption moves pending metadata into 215`submission_events`, records its committed position, and removes pending input 216in the same transaction as the transcript rows. Failed statements roll back the 217whole transaction, including a failed commit. 218 219Receipts retain the original admission result. They do not reference the session 220row, so deletion cancels pending inputs and leaves receipts available. Terminal 221receipts expire seven days after acceptance for creation or rejection, or after 222commit or cancellation for submissions. Pending receipts do not expire. Cleanup 223removes at most 128 expired receipts per call. Durable display metadata in 224`submission_events` remains until session deletion, including operation IDs, 225display text, and echo identity. Continuations commit a `continuation_markers` 226row in the same transaction without adding a user-text transcript row. These 227markers also remain until session deletion and survive receipt cleanup. 228Forks copy this historical metadata with its original input IDs, timestamps, 229and turn IDs at the branch's remapped transcript positions. A continuation 230follows its anchor row, so only markers anchored strictly before the selected 231checkpoint belong to the fork. Forks copy no admission receipts or live runs. 232 233Acceptance guarantees stored input or its committed or cancelled outcome. 234Commit means the input reached the transcript. Neither guarantee promises that 235assistant generation, tools, or external effects occur exactly once.