okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

Aly Raffaufrefactor(links): remove obsolete mutations510b062

main
16.3 KiB235 linesraw

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 dataImplementation ownerCoverage
Older core and extension schemas, inline images, and session preferencesCore migrations, extension-contributed migrations, and preference importstartup scenarios, migration ordering
Older transcript metadata, titles, and timestampsConversation columns and transcript classificationconversation tests, metadata backfill
Inline image records and TEXT image payloadsImage-store and cell-image migrationsimage-store tests, cell image readers
Legacy item-count compaction stateRolling's lazy compatibility readerrolling tests
Older Python namespace snapshotsKernel snapshot readerkernel state scenarios
Flat provider configuration and relocated credentialsProvider configuration and credential migrationsettings scenarios, startup integration, credential migration
Older layouts inspected offlineDaemon storage helper compatibility readerstorage 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:

  1. conversation.initialise creates the core tables, then calls conversation_columns.apply: the existing session columns first, transcript columns second. The same core initialization creates operations, pending_inputs, submission_events, and continuation_markers through operations.schema. operations.initialise upgrades 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 creates sessions_activity.
  2. quota, mail, and family initialise their tables, unchanged.
  3. migrations.run(ledger, backup) runs the core image_store.run, which first externalizes legacy transcript images, then converts image-table TEXT data to decoded BLOB. It then runs transcript_classes.run to classify old transcript rows. runtime.migrate(host, backup) then collects installed extensions' DataMigration callbacks in registry/plugin order and applies them with the same backup path. python contributes cell_images.run, which requires the core image and marker tables. callbacks report named row counts; the server logs nonzero extension results as migration <name>: <n> rows. a failed callback stops startup and prevents later migrations from running.
  4. 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.apply adds nullable transcript.row_class, checked against user, image_fit, and other. Live writes set it in the same transaction as the payload; forks copy it with the retained prefix. transcript_classes.run classifies only NULL rows in 128-row transactions; completed batches survive interruption. An unreadable payload, here or in conversation's startup indexing, is healed by conversation.heal_in: its bytes move to transcript_quarantine (one row per sequence ID, deleted with its session), and the row becomes a user-role system note saying it was set aside, classed user. 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.settle is the one rule. conversation applies it when it commits results, session_history applies 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 remaps transcript_calls, submission_events, continuation_markers, provider_requests, operations.committed_seq, and transcript_quarantine with them. LCM source ranges are not remapped.
  • the request builder pairs the same way at the request boundary: tool_results.pair gives 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 gets tool_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 a tool_use without its tool_result and a tool_result without its tool_use alike. 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_class indexes unclassified sequence IDs. transcript_users indexes (session,seq) for user and image-fit rows; transcript_fits indexes (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 are image_store and cell_images, recorded with unixepoch(). 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 the image_store marker 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.gleam and python's cell migration preserve the two existing backup call patterns: transcript and BLOB migration check for an existing file before directory creation and VACUUM INTO, retaining the image 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 shared home/backups/albedo-before-image-store-<timestamp>.sqlite path. 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 implementationcurrent implementation
conversation.initialise: session/transcript add_columnsmigrations/conversation_columns.apply
transcript row classificationmigrations/transcript_classes.run, after core image upgrades
images.migrate / migrate_legacy / transcript pagesmigrations/image_store.run / migrate_legacy / transcript pages
images.migrate_blobs / BLOB pagesmigrations/image_store.migrate_blobs / BLOB pages
images.backup_before_migrationmigrations/backup.image_store
python/cells.migrate_images / cell pagespython/migrations/cell_images.run / cell pages
python/cells.backup_before_migrationpython/migrations/cell_images.backup_before_migration
work/ledger.initialise: cwd alteration and indexwork/migrations/cwd.apply, contributed by work
paperclips/ledger.initialise: title additionpaperclips/migrations/title.apply, contributed by paperclips
paperclips/ledger.initialise: paperclips_cwd index creationdropped from the schema; paperclips/migrations/scope.apply removes it from existing stores
paperclips/ledger.initialise: reply columnpaperclips/migrations/reply.apply, added with the global ledger
rolling.initialise: rolling_compaction_observation creationdropped 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`.
822. quota, mail, and family initialise their tables, unchanged.
833. `migrations.run(ledger, backup)` runs the core `image_store.run`, which first
84   externalizes 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.
924. the existing best-effort filesystem credential migration runs; then provider
93   assignment from legacy configuration runs, unchanged.
94
95schema 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
111  against `user`, `image_fit`, and `other`. Live writes set it in the same
112  transaction as the payload; forks copy it with the retained prefix.
113  `transcript_classes.run` classifies only NULL rows in 128-row transactions;
114  completed batches survive interruption. An unreadable payload, here or in
115  `conversation`'s startup indexing, is healed by `conversation.heal_in`: its
116  bytes move to `transcript_quarantine` (one row per sequence ID, deleted with
117  its session), and the row becomes a user-role system note saying it was set
118  aside, classed `user`. Startup continues. The backfill adds no migration
119  marker. It runs after the image migration.
120- a tool result saved after rows that are neither calls nor results (a
121  capability note written while the call ran, then the result recovered after a
122  restart) moves back beside its call: providers reject a call whose result does
123  not follow it. `tool_results.settle` is the one rule. `conversation` applies it
124  when it commits results, `session_history` applies it to the live history and to
125  a transcript when it loads, and saves the moves from that load. A move keeps
126  every 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
129  ranges 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
132  one tool result right after the block that made it. A late result moves
133  there, a call no result answers gets `tool_results.unpaired_result`
134  ("interrupted, no output"), and a result whose call nothing carries — one a
135  projection dropped, or a second answer to a call already answered — stays
136  out of the request: providers reject a `tool_use` without its `tool_result`
137  and a `tool_result` without its `tool_use` alike. The transcript is left
138  as it is: the pairing is applied again to every request, so a session an
139  older 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
143  databases have no pending entries, so subsequent startups do not decode
144  transcript payloads for classification.
145- the existing `migrations(name,applied_at)` table remains unchanged. the exact
146  markers are `image_store` and `cell_images`, recorded with `unixepoch()`.
147  transcript migration skips when its marker exists; cell migration skips when
148  its 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
152  replacing that row.
153- transcript and cell scans retain their original cursor order and 16-row pages;
154  BLOB conversion retains its 16-row limit. each page uses the existing
155  transaction helper. there is no new all-migrations transaction or transaction
156  policy change. markers are written at the original completion points, and
157  interrupted runs recheck remaining rows.
158- core `backup.gleam` and python's cell migration preserve the two existing
159  backup 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
161  migration ensures the directory first and copies on the first page containing
162  inline images, outside its transaction. the server still supplies one shared
163  `home/backups/albedo-before-image-store-<timestamp>.sqlite` path. no backup is
164  overwritten and backup triggering is unchanged (the transcript candidate
165  check can include tool outputs without inline images).
166- live writes and transcript migration share `image_payloads.insert`, the
167  unchanged decoded-BLOB insertion helper. image hashes, serialized formats,
168  image validation, and legacy inline fallback reads are unchanged. the existing
169  Erlang 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
196  decoding/title helpers and still runs at exactly the same point. core schema
197  bootstrap 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,
200  validates the old fingerprint (including legacy image payload fingerprints),
201  and only writes a cut when the history matches and its tail begins with a user
202  message. treating this as an unconditional startup migration would change its
203  timing and semantics; its projections are untouched.
204- credentials migration is filesystem/auth, not SQLite. legacy configuration's
205  provider assignment likewise remains in the existing server startup path.
206- no zstd, compression, schema-version, offline storage upgrade,
207  new 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.