Skip to content

refactor(capture-kit): move durable-capture resource mechanics out of the daemon - #2320

Merged
thymikee merged 8 commits into
mainfrom
refactor/durable-capture-mechanics
Sep 6, 2026
Merged

refactor(capture-kit): move durable-capture resource mechanics out of the daemon#2320
thymikee merged 8 commits into
mainfrom
refactor/durable-capture-mechanics

Conversation

@thymikee

@thymikee thymikee commented Sep 5, 2026

Copy link
Copy Markdown
Member

Closes #2317.

The daemon held two halves of one mechanism. @agent-device/capture-kit already owned the durable-resource envelope, JSON, and descriptor codec; the fence, transition, adoption, and recovery mechanics that operate on that envelope still lived in src/daemon.

The four seams, and what replaced them

Seam Replacement
SessionStore.set / .resolveSessionDir DurableCaptureSessionStore<S> — a two-member port
SessionState a session type parameter S; only a definition's own sessionSlot looks inside it
safeSessionName recovery takes resolveSessionDir(sessionId) from its caller
DurableCaptureAdmissionLedger DurableCaptureCleanupOutcome, reported by the mechanics; the daemon decides admission

recoverFailedAdoption computes confirmed exactly as before — (!persisted && cleanupError === undefined) || transition.confirmed — and reports it. createDurableCaptureResource maps confirmed: true to clearUndurableCleanup and confirmed: false to blockUndurableCleanup(device, reason) with the same reason string, still applied before the primary error is rethrown. The admission ledger is never named inside capture-kit.

Splitting DurableCaptureRecordDefinition out of DurableCaptureResourceDefinition says which half needs a session at all: recovery, finish-recovered, and start preflight terminalize a persisted record with no session in hand, so they name the record definition and carry no session type. That also drops the handle parameter from those signatures, which was only ever there to reach sessionSlot.

What moved, and what stayed

Eight files (1,390 lines) left src/daemon for packages/capture-kit/src/durable-capture/, behind one new @agent-device/capture-kit/durable-capture subpath. It is deliberately not on the . index — that index is the eager closure every platform runtime imports.

The daemon keeps five durable-capture-* files, 297 lines: the admission ledger, start preflight, runtime binding, the closed kind set, and the composition root that wires the moved mechanics to the ledger and the session store's naming rule. All sixteen kind stamps stay.

git show -M90% reads every production move as a rename (90–100% similarity); git show -M90% 50f9a5d has the summary.

Tests

The mechanics tests moved beside their sources and now run against a resource kind and a session type of their own, so what they prove is that the mechanics need neither the daemon's closed kind set nor SessionState. Their fixture handle reproduces the shipped live-handle contract — finish and cleanup run once, and disposing an unconfirmed cleanup throws — because that throw is the signal a failed adoption reports.

The admission mapping the adoption test used to assert moved to durable-capture-resource.test.ts, where the ledger lives: one test for the clear, one for the block.

Planted-red proof

No new structural rule, so the proof is that the existing ones bite.

  1. Skipping the re-validation inside withDurableCaptureResourceFencefence.test.ts > rejects a stale fence before its side effect fails by name.
  2. Importing src/daemon/types.ts from the moved adoption module → R11 red ('../../../../src/daemon/types.ts' escapes packages/capture-kit/), plus R10 on the daemon/types.ts importer count.
  3. Importing durable-capture-admission-ledger.ts from the moved adoption module → R11 red the same way.

All three removed.

Validation

Green: pnpm check:tooling (format, lint, typecheck, layering, depgraph, gate-manifest, production-exports, tmpdir-leaks, xctest-selection, mcp-metadata, build, bundle-owner-files, package), full vitest --project unit-core (1200 files, 8980 tests), src/__tests__/cli-startup-import-closure.test.ts, scripts/__tests__/eager-closure-budgets.test.ts.

Two things worth a reviewer's eye:

  • APPROVED_OVER_CEILING row. The new subpath is a first-introduced domain-facade entry evaluating 30 modules against a ceiling of 20. The entry surface is new; the weight is not — the store publishes through host-kit/file and adoption validates through the envelope codec, and both edges came along unchanged from src/daemon. The gate itself confirms no existing entry grew.
  • check:affected fails on one unrelated test. session-open-runtime.test.ts > open --metro-port alone stays host-ambiguous on a physical Android device trips the hermetic-signal guard when vitest related schedules it alongside this file set. It passes alone and in the full unit-core run, and it fails identically on main at 80997b6 with a comparable vitest related selection, so it is pre-existing and not caused by this change.

Two small cleanups the move forced: decodeDurableResourceEnvelope left the capture-kit . index (the daemon's store and adoption modules were its only production consumers, and they now sit beside the encoder), and the four kind-stamp recovery files had their duplicated subpath imports merged.

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
Installed (including dependencies) 4.48 MB 4.48 MB +481 B
Package (unpacked) 4.48 MB 4.48 MB +481 B
Package (download) 1.33 MB 1.33 MB +336 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 30.3 ms 30.2 ms -0.2 ms
CLI --help 84.6 ms 83.8 ms -0.9 ms

@thymikee

thymikee commented Sep 5, 2026

Copy link
Copy Markdown
Member Author

Sentinel at efd5d86: coordinator + independent Terra/high review found one P2 regression-test gap. In src/daemon/tests/durable-capture-resource.test.ts, the new confirmed failed-adoption cleanup test starts with an unblocked ledger; deleting clearUndurableCleanup from the newly moved composition mapping still leaves assertStartAllowed green. Seed blockUndurableCleanup(context.device, ...) before adoption, then assert confirmed cleanup clears that block. Validate the test red with the clear mapping removed and green restored. This is a missing regression for the relocated policy seam, not a claimed production behavior bug. Otherwise the mechanics preserve fence/recovery/error/cleanup ordering; the 30-module subpath exception has evidence that actual consumer closures shrink, and package growth is only +481 B. Exact-head CI including Coverage and Integration is green. Hold readiness label until the mapping regression is effective. No fixes or merge performed.

@thymikee

thymikee commented Sep 6, 2026

Copy link
Copy Markdown
Member Author

Fixed in 85d1485.

The clear mapping is now effective: the test seeds blockUndurableCleanup(context.device, ...), asserts assertStartAllowed throws, runs the canceled adoption, and asserts the block is gone. Deleting clearUndurableCleanup from the composition mapping turns it red (× a confirmed failed-adoption cleanup lifts an earlier block on the device, the other six still green), and restoring it returns all seven to green.

…anup report

The durable-capture mechanics reached two daemon-owned authorities directly:
the concrete `SessionStore` class plus `SessionState`, and the admission
ledger, which `recoverFailedAdoption` called to block or clear a replacement
start. Both are daemon policy, so neither can travel with the mechanics.

Replace them with a two-member `DurableCaptureSessionStore<S>` port and a
session type parameter, and let the mechanics report what they observed —
`DurableCaptureCleanupOutcome` — while `createDurableCaptureResource` keeps
the clear/block decision and the reason text. Recovery takes the session
directory resolver from its caller instead of importing `safeSessionName`.

Splitting `DurableCaptureRecordDefinition` out of the definition says which
half needs a session at all: recovery, finish-recovered, and start preflight
terminalize a persisted record with no session in hand.
… the daemon

The daemon held two halves of one mechanism. capture-kit already owned the
durable-resource envelope, JSON, and descriptor codec; the fence, transition,
adoption, and recovery mechanics that operate on that envelope still sat in
`src/daemon` as eight files.

Move them behind the store port and cleanup report the previous commit
introduced, exposed through one new `@agent-device/capture-kit/durable-capture`
subpath — not the `.` index, which is the eager closure every platform runtime
imports. Admission, start preflight, runtime binding, the kind stamps, and the
composition root that wires the mechanics to the admission ledger stay daemon
policy.

The moved tests exercise the mechanics through a resource kind and session type
of their own, so what they prove is that the mechanics need neither the daemon's
closed kind set nor `SessionState`. The composition root keeps the admission
mapping the adoption test used to assert, now in
`durable-capture-resource.test.ts` where the ledger lives.
…export

The daemon's store and adoption modules were the `.` index's only production
consumers of `decodeDurableResourceEnvelope`; both now sit beside the encoder
inside capture-kit and import it directly.
…ed surface

`tsc -b` cannot name the fixture spy's inferred type across the package
boundary, and five re-exported vocabulary types had no consumer.
The confirmed-cleanup test started with an unblocked ledger, so deleting
`clearUndurableCleanup` from the relocated composition mapping still left
`assertStartAllowed` green. Seed a block first, so the assertion is that the
mapping lifted it.
@thymikee

thymikee commented Sep 6, 2026

Copy link
Copy Markdown
Member Author

The test gap is fixed at 85d1485. The test now starts with a blocked device and proves cleanup removes the block. All seven tests passed; removing the clear operation makes the intended test fail.

No new findings. The remaining blockers are conflicts with main and missing CI coverage on this commit. Please resolve the conflicts and run the required checks.

@thymikee
thymikee force-pushed the refactor/durable-capture-mechanics branch from 85d1485 to e186d72 Compare September 6, 2026 05:50
@thymikee

thymikee commented Sep 6, 2026

Copy link
Copy Markdown
Member Author

Rebased onto f30328d0 (head now e186d72). Three conflicts, all from #2316 and #2321 landing:

  • packages/capture-kit/package.json and the pinned export list in scripts/layering/package-boundaries.test.ts: main added ./durable-json, this branch adds ./durable-capture. Both kept.
  • atomic-publish-ownership.test.ts: the durable-publisher scan now names packages/capture-kit/src/durable-capture/store.ts and packages/managed-allocation/src/store-filesystem.ts — the two moves crossed on the same two lines.

Re-validated on the new base: pnpm check:layering (186 pass / 0 fail, R11 now 20 packages behind 217 subpaths), typecheck, lint, format, check:production-exports, scripts/__tests__/eager-closure-budgets.test.ts (the APPROVED_OVER_CEILING row is still live and not stale), cli-startup-import-closure, and the durable-capture tests on both sides of the boundary. Every production move still reads as a rename at -M90%.

One update to the pre-existing failure noted in the description: session-open-runtime.test.ts is no longer confined to vitest related selections. On main at f30328d086, the plain vitest run --project unit-core fails it outright (open --metro-port alone stays host-ambiguous on a physical Android device). On this branch the same file instead emits two unhandled @vitest/mocker errors around packages/platform-apple/src/runner/runner-session.ts with all 9003 tests green. Same unstable file either way, and it passes alone on both branches — still not this change.

@thymikee
thymikee merged commit bcb6c55 into main Sep 6, 2026
18 checks passed
@thymikee
thymikee deleted the refactor/durable-capture-mechanics branch September 6, 2026 05:57
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-09-06 05:57 UTC

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(capture-kit): move durable-capture resource mechanics out of the daemon

1 participant