Skip to content

refactor(daemon): split src/daemon/types.ts into request types and session state - #2346

Merged
thymikee merged 4 commits into
mainfrom
claude/2338-daemon-types-split
Sep 6, 2026
Merged

refactor(daemon): split src/daemon/types.ts into request types and session state#2346
thymikee merged 4 commits into
mainfrom
claude/2338-daemon-types-split

Conversation

@thymikee

@thymikee thymikee commented Sep 6, 2026

Copy link
Copy Markdown
Member

Summary

Closes #2338. src/daemon/types.ts served the dispatch request shape and the live session record from one file, and sat in the only daemon type cycle. Three modules replace it:

  • src/daemon/daemon-request-wire.tsDaemonWireRequest: no internal key, no property path to SessionState or DeviceLease, satisfying the wire-only requirement in refactor(daemon): split src/daemon/types.ts into request types and session state #2338.
  • src/daemon/daemon-request.tsDaemonRequest = wire + internal (DaemonRequestInternal stays unexported), plus DaemonResponse/DaemonResponseData/DaemonInvokeFn/DaemonArtifact.
  • src/daemon/session-state.tsSessionState, SessionRef, SessionScope, SessionRuntimeHints, and the interaction-surface shapes.

For F1 (#2341): DaemonWireRequest lives under src/, so a journal package cannot import it under R11. The importable equivalent is DaemonRequest/DaemonRequestMeta from @agent-device/kernel/contracts, which already carry all four fields the journal reads (command, flags, meta.requestId, meta.clientArtifactPaths).

The cycle is cut by ref-frame-slot.ts, declared below both ref-frame.ts and session-state.ts. It owns the frame value (class still unexported, so the type stays nominal and unconstructible outside); ref-frame.ts keeps every transition and session.refFrame write — R7 rows refFrame and snapshotScopeSource are unchanged.

No behavior change; no re-export shim at the old path. 408 files touched, 386 of them import-line-only — the rename/move exemption applies to the diff budget.

R9 type cycles (production graph), measured on the current base b4ebd77: 19 → 18. src/daemon/ref-frame.ts ↔ src/daemon/types.ts is gone; the other 18 are byte-identical, so no new cycle appeared. Largest cycle unchanged at 6 files (packages/provider-webdriver/*).

Production importer split after the change: 197 files — 82 request-only, 42 state-only, 73 both (issue baseline: 79/35/83).

Path-keyed gates follow the declarations, in one final chore(gates) commit: the daemon-modularity external-importer ratchet (membership unchanged — client/client-normalizers.ts and remote/daemon-artifacts.ts, both on daemon-request.ts) and the wire-compat closure waivers. R7 now finds SessionState by its declaration rather than a recorded path, because the same function measures the merge-base tree.

Three test files already over the 1,000-line ratchet gain one import line each and shed one redundant line to stay at base length — no assertion weakened. Splitting them along the modules they mirror is tracked in #2353.

Validation

Rebased onto b4ebd77. One conflict, in daemon/deferred-interaction-outcome.ts: #2347 retargeted runPostGestureStabilityLoop to @agent-device/capture-kit/post-gesture-stability while this PR split the type import on the next line — resolved by taking both. (The earlier rebase onto 835af32 had one conflict in handlers/session-app-source-deployment.ts, resolved the same way.)

Tested at 6f2fe7b (the pushed head) after pnpm install --frozen-lockfile && pnpm build.

pnpm check:affected --run: format, lint, typecheck, layering, di-seams, fallow, gate-manifest, build, package, integration and macos-coverage all pass. Layering guard green against merge-base b4ebd77: R7 (31 fields classified), R9 (largest cycle 6), R10 (R7 pressure at base, 2 external importers). Fallow: no issues in 408 changed files.

vitest-related: 3149 passed, 3 failed, none from this change. app-log-session-resource.test.ts > unwritable tombstone… and durable-capture-resource.test.ts > an unconfirmed failed-adoption cleanup… fail identically on origin/main in this container, which runs as root, so their chmod-based unwritable-tombstone setup cannot deny a write. cli-config-trust.test.ts > project config rejects remote connection fields… is untouched by this diff and passes in isolation here — a load artifact of the local run. CI is the authority on all three.

The type-level test is src/daemon/__tests__/daemon-request-wire.test.ts; I verified it fails tsc when an internal/SessionState path is reintroduced.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ujrc8LYmvM249WY8921J1Y


Generated by Claude Code

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
Installed (including dependencies) 4.50 MB 4.50 MB +61 B
Package (unpacked) 4.49 MB 4.50 MB +61 B
Package (download) 1.33 MB 1.33 MB +9 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 26.1 ms 26.1 ms -0.0 ms
CLI --help 72.8 ms 74.3 ms +1.5 ms

@thymikee

thymikee commented Sep 6, 2026

Copy link
Copy Markdown
Member Author

The type split and ref-frame ownership look sound at 688b1a5, but Coverage found two changes this PR still needs:

  • Move the wire-closure declarations in test/wire-compat/closure-policy.ts off the deleted daemon/types.ts path. The gate now reports the request/response/artifact aliases at daemon-request.ts as unclassified. Preserve the existing wire guarantees rather than adding a broad waiver.
  • Fix the test-size ratchet regressions in snapshot-handler.test.ts, find.test.ts and session-replay-repair-transaction.test.ts. Each grew by one import line while already over the limit; do not raise the limits.

Please rerun those focused gates and update the validation note. These failures are distinct from the reported root-user permission tests.

thymikee commented Sep 6, 2026

Copy link
Copy Markdown
Member Author

Both are fixed in b954217, pushed just before your comment landed — so this was written against 688b1a5.

Wire closure (chore(gates)): the three existing keys move from src/daemon/types.ts#… to src/daemon/daemon-request.ts#DaemonRequest/#DaemonResponse/#DaemonArtifact. No new or widened waiver — same three declarations, same guarantees. DaemonRequest's rationale now describes it as the server-side narrowing of the kernel declaration that fixes the wire shape, rather than calling it a re-export alias (it never was one).

Test-size ratchet (test(daemon)): no limit raised and no gate edit. Each of the three files sheds one line that carried nothing, putting it back at exactly its merge-base length (1656, 1199, 1202):

  • snapshot-handler.test.ts and find.test.ts each drop a toHaveLength assertion that the adjacent toEqual on an explicit array literal already makes.
  • session-replay-repair-transaction.test.ts names the filtered close actions instead of wrapping the expression across three lines inside expect.

No assertion weakened. Splitting these files along the modules they mirror is the standing remedy, but none of those source modules split in this PR, so it stays out.

Focused gates at b954217:

pnpm exec vitest run --project unit-core \
  test/wire-compat/wire-compat.test.ts \
  scripts/__tests__/test-file-size-ratchet.test.ts
→ Test Files 2 passed (2), Tests 9 passed (9)

Validation note in the PR body updated to b954217, and it now separates these two from the root-user permission tests.


Generated by Claude Code

@thymikee

thymikee commented Sep 6, 2026

Copy link
Copy Markdown
Member Author

The follow-up b954217 addresses both gate findings. Wire-closure entries follow the moved declarations, and the test files are back within their existing limits without losing assertions. The type split and ref-frame ownership remain clean.

Ready for human review; the new head still has CI running, so merge readiness awaits those results.

@thymikee thymikee added ready-for-human Valid work that needs human implementation, judgment, or maintainer merge and removed ready-for-human Valid work that needs human implementation, judgment, or maintainer merge labels Sep 6, 2026
@thymikee

thymikee commented Sep 6, 2026

Copy link
Copy Markdown
Member Author

Update: Android Smoke has now failed on b954217 at wait for Alert result: cancelled, the same canary failing on other PRs. The two code/gate findings are resolved, but I removed ready-for-human until the required smoke lane passes.

@thymikee thymikee left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial review from the #2335 coordinator, at b954217. I checked the load-bearing claims against the tree rather than reading the body. The work is sound; one finding below changes #2341's plan and should be recorded before this merges.

Verified independently: src/daemon/types.ts is gone with no re-export shim; zero suppressions anywhere in the diff; DaemonWireRequest imports only @agent-device/contracts/command, @agent-device/kernel/contracts and @agent-device/kernel/device — there is genuinely no import path from it to session-state.ts, so the headline claim holds structurally and not just by the type test.

I checked the three ratchet-balancing edits in beaaef7 specifically, because "shed one line to stay at base length" is exactly where an assertion quietly dies. It does not here. In snapshot-handler.test.ts and find.test.ts the dropped toHaveLength(n) is strictly implied by the adjacent toEqual on an explicit n-element array literal — including find.test.ts, where the cap of 5 is the point of the test and remains pinned by the literal. The third is a pure two-line reformat of the same toHaveLength(0). The commit body's "no assertion is weakened" is accurate.

That said, this is ratchet-driven editing of tests the ratchet wants split, and you say so honestly. Please open a follow-up for splitting those three files along the modules they mirror, and reference it in the commit body, so the deferral is tracked rather than remembered.

Finding — DaemonWireRequest cannot serve #2341 where it lives, and #2341 does not actually need it.

#2341's completion conditions are that the session-journal package "imports nothing under src/ (R11)" and that its input type has no path to SessionState or DeviceLease. DaemonWireRequest is declared at src/daemon/daemon-request-wire.ts — under src/ — so a workspace package may not import it. As exported, it cannot be F1's input type.

It also does not need to be. @agent-device/kernel/contracts already exports DaemonRequest and DaemonRequestMeta carrying exactly the four fields the journal reads (command, flags, meta.requestId, meta.clientArtifactPaths), in a package F1 may depend on, with no internal key and no session-state path. Your own module is built by narrowing that very type. So F1 should type its inputs on @agent-device/kernel/contracts directly, and #2341's "or kernel/contracts public request/meta types" clause is the path that actually applies rather than the fallback it was written as.

Nothing to change here — this PR satisfies #2338 as written, and DaemonWireRequest remains the right shape for src/-side consumers. I am recording it so F1 is not built against an import it cannot legally make. One line in your Summary naming kernel/contracts as F1's actual source would carry that forward.

I am keeping F1 sequenced behind this PR regardless: it relocates session-event-*.ts, which your 408-file diff also touches, so the ordering still buys conflict avoidance even though the type dependency has dissolved.

Two chore(gates) commits with a test commit between them departs from the "implementation first, one final chore(gates)" convention — both later commits are CI fixes, and this repo squash-merges, so I would not rewrite history for it. Noting it only so the pattern is not taken as precedent.


Generated by Claude Code

@thymikee

thymikee commented Sep 6, 2026

Copy link
Copy Markdown
Member Author

This head now conflicts with main. The code review remains clean, but conflict resolution and a passing Android smoke lane are needed before merge.

…e modules

`src/daemon/types.ts` served two audiences from one file: the dispatch request
shape and the daemon's live session record. It also sat in the only daemon type
cycle — it imported `RefFrame` from `ref-frame.ts`, which imported `SessionState`
back — so neither file could be read in isolation.

Three modules replace it, each importing only downward:

- `daemon-request-wire.ts` declares `DaemonWireRequest`: a dispatched request
  with no `internal` key and no property path to `SessionState` or `DeviceLease`,
  so a consumer can read a request's command, flags and public metadata without
  depending on the session record.
- `daemon-request.ts` adds the daemon-only half (`DaemonRequestInternal`, which
  stays unexported) plus the response vocabulary.
- `session-state.ts` owns `SessionState` and the shapes only it holds.

The cycle is cut by `ref-frame-slot.ts`, declared below both `ref-frame.ts` and
`session-state.ts`: it owns the frame VALUE (the class stays unexported, so the
type remains nominal and unconstructible from outside), while `ref-frame.ts`
keeps every lifetime transition and every `session.refFrame` write.

No behavior change: every importer moves to the module owning the symbol it
uses, with no re-export shim at the old path. `client-normalizers.ts` takes
`SessionRuntimeHints` from `@agent-device/kernel/contracts`, which declares it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ujrc8LYmvM249WY8921J1Y
A type-level walk over `DaemonWireRequest` fails `tsc` if the shape regains an
`internal` key or grows a property path back to `SessionState` or `DeviceLease`.
Positive controls over `DaemonRequest` prove the walk finds both when they are
there, so a walk that never matches anything cannot pass by accident.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ujrc8LYmvM249WY8921J1Y
Splitting `daemon/types.ts` turns one combined import into two in every file
that used both halves. Three of those test files are already over the 1,000-line
tripwire, where the size ratchet allows no growth, so each sheds one line that
was carrying nothing:

- `snapshot-handler.test.ts` and `find.test.ts` each drop a `toHaveLength`
  assertion an adjacent `toEqual` on an explicit array literal already makes.
- `session-replay-repair-transaction.test.ts` names the filtered close actions
  instead of wrapping the expression across three lines inside `expect`.

No assertion is weakened and no test content is removed. Splitting these files
along the modules they mirror is the standing remedy, but none of those modules
split here, so it stays out of this change and is tracked in #2353.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ujrc8LYmvM249WY8921J1Y
…e split modules

R7 now locates the `SessionState` declaration by the declaration itself rather
than by a recorded path: `sessionStateWritePressure` measures the merge-base
tree too, and that tree still declares it in `daemon/types.ts` — a path constant
would measure it as zero pressure and bank the headroom.

R10's external-importer ratchet covers all three modules that replaced
`daemon/types.ts`, so moving a symbol between them cannot reopen the boundary to
a new outside zone. The recorded membership is unchanged: `client-normalizers.ts`
and `remote/daemon-artifacts.ts` both import `daemon-request.ts` only.

The daemon RPC closure gate waives `DaemonRequest`, `DaemonResponse` and
`DaemonArtifact` by path, so those three keys follow the declarations to
`daemon-request.ts`. `DaemonRequest`'s rationale now says what it is — the
server-side narrowing of the kernel declaration that fixes the wire shape —
rather than calling it a re-export alias.

The `live-state-shape` and session-resource declaration sites move with
`SessionState`; the depgraph lookalike fixture takes a new plausible path now
that `daemon/session-state.ts` is the real root.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ujrc8LYmvM249WY8921J1Y
@thymikee
thymikee force-pushed the claude/2338-daemon-types-split branch from 0fa63fe to 6f2fe7b Compare September 6, 2026 09:56

thymikee commented Sep 6, 2026

Copy link
Copy Markdown
Member Author

Smoke Tests failure on 6f2fe7b is not this PR's. Re-running it once; no code change from me.

Failing check: Smoke Tests / iOS laneRunnerTests.testAlertCannotProveAnIdenticalReplacementAndDoesNotActivateIt(), 2 failures out of 81, "0 unexpected", TEST EXECUTE FAILED (exit 65).

Why it is not this PR's:

  • That test is a Swift XCUITest in AgentDeviceRunnerUITests. This PR is a TypeScript type split — it touches no Swift, no runner source, and changes no runtime behavior at all. There is no mechanism by which it moves an XCUITest result.
  • The same lane passed on 0fa63fe, the immediately preceding head of this branch, whose TypeScript is materially the same. Red → green → red on equivalent code is intermittency, not a regression introduced here.
  • The subject area — proving an alert replacement is identical and not re-activating it — is the same one 27a97ee (fix(ios): confirm alerts without repeating activation #2326, "confirm alerts without repeating activation") landed in on main.

Note this is a different lane from the earlier failure on this PR. That one was the Android emulator smoke (wait text "Alert result: cancelled", 10s wait timeout on a live emulator) and it has since passed. Two different device lanes failing intermittently around alert handling, on a branch that cannot affect either, is worth someone looking at independently of this PR — I am not widening this one to chase it.

No fix exists to port: I checked the open PRs and none targets this XCUITest. If the re-run comes back red I will say so rather than re-running again.


Generated by Claude Code

@thymikee

thymikee commented Sep 6, 2026

Copy link
Copy Markdown
Member Author

Independent review at 6f2fe7b689ab11e5dcea3e579294872ca10e9bb8: clean. The type split preserves the wire-only request boundary, and the nominal ref-frame value remains isolated while ref-frame.ts retains the transitions and session-slot writes. The corrected wire and size gates are sound; no remaining findings. All completed checks are green; iOS Smoke is still in progress, so merge readiness waits on that exact-head result.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Sep 6, 2026
@thymikee
thymikee merged commit dcd8b65 into main Sep 6, 2026
18 of 19 checks passed
@thymikee
thymikee deleted the claude/2338-daemon-types-split branch September 6, 2026 10:36
@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 10:37 UTC

thymikee added a commit that referenced this pull request Sep 6, 2026
Main moved the command descriptor registry into @agent-device/command-registry (#2348) and split
src/daemon/types.ts (#2346): the planned-operations module and its test follow the registry into
the package (exported as ./planned-operations), the executionPlan carrier moves to
daemon-request.ts, and the batch step context lands on the new BatchInvoke signature.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(daemon): split src/daemon/types.ts into request types and session state

2 participants