Plan 003: Complete request cancellation for duration waits and snapshot runtimes
Executor instructions: Follow this plan step by step. Run every
verification command and confirm the expected result before moving to the
next step. If anything in the "STOP conditions" section occurs, stop and
report — do not improvise. A reviewer maintains the plan index for this
execution; do not create or edit plans/README.md.
Drift check (run first):
git diff --stat 13bc70f24..HEAD -- src/commands/interaction/runtime/selector-wait.ts src/commands/interaction/runtime/wait-polling.ts src/commands/interaction/runtime/selector-wait-cancellation.test.ts src/daemon/snapshot-runtime.ts src/daemon/__tests__/snapshot-runtime-cancellation.test.ts
If any in-scope file changed since this plan was written, compare the
"Current state" excerpts against the live code before proceeding; on a
mismatch, treat it as a STOP condition.
Status
- Priority: P1
- Effort: M
- Risk: MED
- Depends on: none
- Category: bug
- Planned at: commit
13bc70f24, 2026-08-08
Why this matters
The daemon already creates one AbortSignal per tracked request, and most
selector polling and platform snapshot implementations know how to consume it.
Two runtime boundaries drop that authority: a duration-only wait sleeps
without either available signal, and the snapshot/diff snapshot runtime
does not install or forward the registered request signal. A canceled request
can therefore keep doing work and retain its session/device execution lock
until the sleep or backend operation finishes. This plan completes the existing
contract without changing CLI syntax, timeout budgets, response shapes, or
platform fallback policy.
Current state
Relevant files and their roles:
src/commands/interaction/runtime/selector-wait.ts — implements duration,
selector, text, ref, and stable waits. Its context already permits command-
and runtime-level signals, but its duration branch does not use them.
src/commands/interaction/runtime/wait-polling.ts — owns the existing
cancellation-aware wait sleep used between selector/text polls.
src/daemon/selector-runtime-backend.ts — exemplar for installing the
registered daemon request signal on an AgentDeviceRuntime.
src/daemon/snapshot-runtime.ts — shared runtime path for both snapshot
and diff snapshot; currently creates a signal-less runtime and discards the
backend command context.
src/daemon/handlers/snapshot-capture.ts — lower capture layer. It already
accepts signal and forwards it to Linux, the macOS helper, and dispatched
platform snapshots; it should not need modification.
src/request/cancel.ts — sole request-cancellation registry. This plan must
consume its signal, not create a second controller or cancellation registry.
The duration branch has both signal authorities in its types, but calls a
signal-blind helper:
// src/commands/interaction/runtime/selector-wait.ts:59-64,119-129,176-183
type WaitCommandContext = {
session?: string;
requestId?: string;
signal?: AbortSignal;
metadata?: Record<string, unknown>;
};
type SelectorWaitRuntime = {
// ...
signal?: AbortSignal;
};
if (options.target.kind === 'sleep') {
await sleep(runtime, options.target.durationMs);
return { kind: 'sleep', waitedMs: options.target.durationMs };
}
// src/commands/interaction/runtime/selector-wait.ts:451-454
async function sleep(runtime: SelectorWaitRuntime, durationMs: number): Promise<void> {
if (runtime.clock) await runtime.clock.sleep(durationMs);
else await new Promise((resolve) => setTimeout(resolve, durationMs));
}
The polling path already has the behavior to reuse. It checks both authorities,
preserves injected-clock behavior, clears the real timer on abort, removes the
listener, and unrefs the timer:
// src/commands/interaction/runtime/wait-polling.ts:165-196
async function sleepWithinWait(
runtime: WaitPollingRuntime,
options: WaitPollingOptions,
durationMs: number,
): Promise<boolean> {
const parentSignals = [options.signal, runtime.signal].filter(
(signal): signal is AbortSignal => signal !== undefined,
);
for (const signal of parentSignals) signal.throwIfAborted();
// ... injected clock branch ...
const signal = parentSignals.length > 0 ? AbortSignal.any(parentSignals) : undefined;
// ... clearTimeout, listener cleanup, timer.unref() ...
}
The selector runtime shows the intended daemon wiring:
// src/daemon/selector-runtime-backend.ts:62-77
export function createSelectorRuntimeForDevice(params: SelectorRuntimeDeviceParams) {
return createAgentDevice({
backend: createSelectorBackend(params),
// ... policy and sessions ...
signal: params.signal ?? getRequestSignal(params.req.meta?.requestId),
});
}
The snapshot runtime omits that field, and its backend ignores the context that
captureRuntimeSnapshot supplies:
// src/daemon/snapshot-runtime.ts:266-311
function createSnapshotRuntime(params: { /* ... */ }) {
const { req, sessionName, logPath, sessionStore, session, device, snapshotScope } = params;
return createAgentDevice({
backend: createDaemonSnapshotBackend({ /* ... */ }),
...createDaemonRuntimePolicy('snapshot'),
sessions: createDaemonRuntimeSessionStore({ /* ... */ }),
});
}
// src/daemon/snapshot-runtime.ts:386-405
return {
platform: publicPlatformString(device),
captureSnapshot: async (_context, options): Promise<BackendSnapshotResult> => {
const capture = await captureSnapshot({
device,
session,
flags: req.flags,
outPath: options?.outPath ?? req.flags?.out,
logPath,
snapshotScope,
});
No platform-specific implementation work is needed. The lower seam is already
signal-aware:
// src/daemon/handlers/snapshot-capture.ts:39-49,89-122
type CaptureSnapshotParams = {
// ...
signal?: AbortSignal;
};
if (device.platform === 'linux') {
const linuxResult = await snapshotLinux(session?.surface, params.signal);
// ...
}
// macOS helper receives signal: params.signal
// dispatchCommand context receives signal: params.signal
Design and repository constraints to preserve:
docs/adr/0018-unified-event-journal.md:216-224 defines client-disconnect
tracking as transport-owned behavior that feeds request cancellation. Do not
move cancellation ownership into wait or snapshot modules.
src/request/cancel.ts:58-81,112-115 owns registrations and exposes
getRequestSignal; consume that exact signal.
docs/agents/testing.md:68-75 requires every regression test to be observed
red against the pre-fix production code and the failing assertion recorded.
docs/agents/testing.md:481-493 forbids real-time sleeps in unit tests and
test-only dependency-injection seams. Use a controlled fake clock/promise and
the existing production request-signal registry.
- Optional snapshot optimizations and capture fallbacks remain best-effort.
Cancellation is request authority, not a new reason to remove those fallbacks.
Commands you will need
| Purpose |
Command |
Expected on success |
| Inspect shared impact |
pnpm depgraph affected src/commands/interaction/runtime/wait-polling.ts --limit 25 |
exit 0; bounded dependents, gates, and command owners are printed |
| Inspect snapshot impact |
pnpm depgraph affected src/daemon/snapshot-runtime.ts --limit 25 |
exit 0; bounded dependents, gates, and command owners are printed |
| Focused tests |
pnpm exec vitest run src/commands/interaction/runtime/selector-wait-cancellation.test.ts src/commands/interaction/runtime/wait-polling.test.ts src/daemon/__tests__/snapshot-runtime-cancellation.test.ts src/daemon/selector-runtime-backend.test.ts src/daemon/__tests__/snapshot-quality-latch.test.ts |
all selected files pass |
| Fast static gate |
pnpm check:quick |
exit 0; lint and typecheck pass |
| Format |
pnpm format |
exit 0; only intended in-scope source/test formatting changes remain |
| Affected gate |
pnpm check:affected --run |
exit 0; selected local gates pass and CI-owned gates are reported |
| Broad deterministic gate |
pnpm check |
exit 0; tooling, fallow, unit, and smoke aggregates pass |
The repository requires pnpm 11.17.0 and Node >=22.12. Do not create or
restore package-lock.json. No Apple target build is required because this
plan changes TypeScript runtime wiring only; pnpm check:affected --run remains
the authority if the live diff selects additional gates.
Scope
In scope (the only implementation/test files to modify or create):
src/commands/interaction/runtime/selector-wait.ts
src/commands/interaction/runtime/wait-polling.ts
src/commands/interaction/runtime/selector-wait-cancellation.test.ts (create)
src/daemon/snapshot-runtime.ts
src/daemon/__tests__/snapshot-runtime-cancellation.test.ts (create)
Out of scope (do NOT touch, even though they are related):
src/request/cancel.ts and daemon HTTP/socket transports — cancellation
registration and disconnect detection already work and are not being redesigned.
src/daemon/handlers/snapshot-capture.ts, platform interactors, Android
helpers, Linux/macOS helpers, and provider adapters — these already accept a
signal; the missing seam is above them.
- Apple runner cancellation code — Apple already merges an explicit signal
with the request-id registration; do not add an Apple-specific fallback.
- Timeout-policy descriptors, daemon reset policy, wait positional parsing,
public error normalization, CLI/MCP schemas, response shapes, and ref-frame rules.
- Snapshot freshness, quality, fallback, deferred-outcome, or session mutation semantics.
- README/website/CLI-help/skill behavior guidance. The public command surface is
unchanged and the transport cancellation contract is already documented.
plans/README.md and every other plan file.
Git workflow
- Suggested branch:
advisor/003-complete-request-cancellation
- Keep test-first evidence and the production fix as logical commits if the
operator wants multiple commits. Suggested final commit message:
fix: complete request cancellation propagation
- Commit messages follow the repository's conventional prefixes (
fix:,
perf:, refactor:, test:), as shown by recent history.
- Do not push or open a PR unless the operator explicitly instructs it.
Steps
Step 1: Add deterministic red regression tests
Create src/commands/interaction/runtime/selector-wait-cancellation.test.ts.
Use the public runtime command (device.selectors.wait), not the private sleep
helper. Model setup after selector-wait.test.ts: construct a local
createAgentDevice with a minimal backend, memory session store, local policy,
and an injected clock. The clock's sleep must return a manually controlled
promise rather than waiting real time.
Cover both cancellation authorities in a table/loop:
- runtime signal (
createAgentDevice({ signal })), and
- command signal (
device.selectors.wait({ signal, target: ... })).
For each authority, start a duration wait, wait until the fake clock reports
that sleep began, abort with a unique Error reason, release the fake sleep,
and assert the wait rejects with that exact reason instead of returning
{ kind: 'sleep' }. This exercises abort-during-sleep deterministically; it
also proves the injected-clock post-sleep check remains load-bearing. Do not
use setTimeout, fake global timers, or a test-only production parameter.
Create src/daemon/__tests__/snapshot-runtime-cancellation.test.ts. Model its
module mock and scenario setup after
src/daemon/__tests__/snapshot-quality-latch.test.ts, but use
makeAndroidSession, makeSessionStore/mkdtempForTestSync, and the request
cancellation helpers from their existing production modules. Partially mock
dispatchCommand while keeping the real module's other exports.
Use a table covering both dispatchSnapshotViaRuntime and
dispatchSnapshotDiffViaRuntime. For each command:
- register a request ID with
registerRequestAbort and put it in req.meta;
- have the mocked snapshot dispatch expose the received
context.signal, wait
on a manually controlled promise, and call context.signal?.throwIfAborted()
immediately after that promise resolves;
- start the runtime command and wait until the mock has been entered;
- call
markRequestCanceled(requestId), then release the mock so the test can
never hang even when the signal is absent;
- assert the observed signal is the registration's controller signal and the
command rejects from abort rather than completing successfully; and
- clear the exact registration in
finally so module-global state cannot leak.
Run the focused command before changing production. At commit 13bc70f24, the
duration tests should resolve instead of reject after the fake clock is
released, and snapshot/diff should observe no dispatch signal and complete when
the mock is released. Record the failing test names/assertions in the PR or
handoff; this is the required red proof.
Verify (red, before production edits):
pnpm exec vitest run src/commands/interaction/runtime/selector-wait-cancellation.test.ts src/daemon/__tests__/snapshot-runtime-cancellation.test.ts
→ exits nonzero only on the new cancellation assertions, with no timeout or
unhandled-promise failure.
Step 2: Route duration waits through the existing cancellable sleep
In src/commands/interaction/runtime/wait-polling.ts, export the existing
sleepWithinWait production helper (a durable name such as
sleepWithWaitCancellation is also acceptable if every existing call is
updated). Do not duplicate its logic. Preserve all existing semantics:
- inspect both
options.signal and runtime.signal;
- throw immediately for an already-aborted authority;
- for an injected clock, check before and after
clock.sleep;
- for a real timer, combine multiple authorities, clear the timer on abort,
remove the listener on both paths, reject with the signal's reason, and
unref the timer; and
- preserve its current zero/negative-duration return behavior.
In src/commands/interaction/runtime/selector-wait.ts, import that helper and
pass the full options object from the duration branch so command-level and
runtime-level signals are both visible. Remove the private signal-blind
sleep function at the bottom of the file. Preserve the successful result
exactly: { kind: 'sleep', waitedMs: options.target.durationMs }.
Verify:
pnpm exec vitest run src/commands/interaction/runtime/selector-wait-cancellation.test.ts src/commands/interaction/runtime/wait-polling.test.ts src/commands/interaction/runtime/selector-wait.test.ts
→ all pass; no test waits on wall-clock duration.
Step 3: Install and forward the request signal in snapshot/diff runtime
In src/daemon/snapshot-runtime.ts:
- import
getRequestSignal from src/request/cancel.ts using the correct
relative path;
- add
signal: getRequestSignal(req.meta?.requestId) to the object passed to
createAgentDevice in createSnapshotRuntime, matching
createSelectorRuntimeForDevice;
- rename the backend callback parameter from
_context to context; and
- pass
signal: context.signal into captureSnapshot.
Do not query the registry again inside the backend and do not add platform
conditionals. The runtime command already selects options.signal ?? runtime.signal when it builds the backend context, so this one seam covers
both snapshot and diff snapshot. The lower capture layer then forwards the
same signal to non-Apple backends; Apple may merge it with the same registered
signal and must retain its existing behavior.
Verify:
pnpm exec vitest run src/daemon/__tests__/snapshot-runtime-cancellation.test.ts src/daemon/__tests__/snapshot-quality-latch.test.ts src/daemon/handlers/__tests__/snapshot-handler.test.ts
→ all pass, including both new command cases and existing quality/session behavior.
Step 4: Tighten and run repository gates
Review the diff for duplicated cancellation logic, stale imports, or a second
request controller. Confirm the snapshot fix is only signal plumbing and did
not alter capture options, annotations, session writes, error conversion, or
fallback selection. Confirm duration wait success output is unchanged.
Run formatting, the focused suite, fast static checks, the affected selector,
and the broad deterministic gate in that order. If formatting modifies any
file outside Scope, inspect why and revert only the unrelated formatter drift;
do not sweep it into this change.
Verify:
pnpm format
pnpm exec vitest run src/commands/interaction/runtime/selector-wait-cancellation.test.ts src/commands/interaction/runtime/wait-polling.test.ts src/commands/interaction/runtime/selector-wait.test.ts src/daemon/__tests__/snapshot-runtime-cancellation.test.ts src/daemon/selector-runtime-backend.test.ts src/daemon/__tests__/snapshot-quality-latch.test.ts
pnpm check:quick
pnpm check:affected --run
pnpm check
→ every command exits 0. git diff --name-only lists only the in-scope
implementation/test files plus an authorized plan-status update.
Test plan
src/commands/interaction/runtime/selector-wait-cancellation.test.ts:
- duration wait observes runtime cancellation;
- duration wait observes command cancellation;
- cancellation happens while the controlled clock sleep is pending;
- neither case uses real time.
src/daemon/__tests__/snapshot-runtime-cancellation.test.ts:
snapshot forwards the exact registered request signal and aborts;
diff snapshot forwards the same authority and aborts;
- controlled dispatch completion prevents a missing-signal regression from
hanging the test;
- every request registration is cleared in
finally.
- Existing regression coverage to retain:
wait-polling.test.ts already proves real-timer cancellation for runtime
and command authorities;
selector-runtime-backend.test.ts is the runtime signal-wiring exemplar;
snapshot-quality-latch.test.ts and snapshot-handler.test.ts protect
snapshot/diff response and session semantics.
- Red-before-fix proof: run the two new test files before production edits and
record the specific failing assertions. A timeout is not acceptable red evidence.
- Final verification: the focused command in Step 4 passes all selected files.
Done criteria
ALL must hold:
STOP conditions
Stop and report back; do not improvise if:
- Any in-scope current-state excerpt no longer matches after the drift check.
- Either new regression requires waiting real time, a live daemon/device, or a
test-only production parameter to fail before the fix.
- Reusing the polling sleep helper would change selector/text polling cadence,
wait timeout accounting, injected-clock semantics, or successful duration output.
- Snapshot cancellation requires modifying
snapshot-capture.ts, a platform
interactor/helper, transport registration, or public error normalization;
those layers already expose the needed signal seam at this commit.
- A proposed fix creates another
AbortController, registry, message sniff, or
platform-specific cancellation fallback instead of using getRequestSignal.
- Apple behavior changes when the explicit signal and request-id-derived signal
are the same authority.
- A verification command fails twice after one reasonable, in-scope correction.
- Completion appears to require any source/test file outside Scope.
Maintenance notes
- Future daemon runtimes should follow the same ownership chain:
transport registration → getRequestSignal(requestId) at runtime creation →
CommandContext.signal → backend operation. Review new runtimes for all four
links rather than teaching individual platforms to query daemon globals.
- Keep one cancellation-aware wait sleep implementation. Any new wait mode that
pauses should reuse it so runtime and command signals cannot drift again.
- Reviewers should scrutinize listener/timer cleanup and registration cleanup;
a green cancellation test that leaves a pending timer or global request entry
is incomplete.
- Snapshot capture fallback and quality semantics are deliberately deferred from
this plan. Cancellation must stop the request; ordinary optional-optimization
failure may continue to use the documented fallback path.
- No docs or skills update is expected: this restores an existing internal
request-lifecycle guarantee and does not change user-facing command behavior.
Plan 003: Complete request cancellation for duration waits and snapshot runtimes
Status
13bc70f24, 2026-08-08Why this matters
The daemon already creates one
AbortSignalper tracked request, and mostselector polling and platform snapshot implementations know how to consume it.
Two runtime boundaries drop that authority: a duration-only
waitsleepswithout either available signal, and the
snapshot/diff snapshotruntimedoes not install or forward the registered request signal. A canceled request
can therefore keep doing work and retain its session/device execution lock
until the sleep or backend operation finishes. This plan completes the existing
contract without changing CLI syntax, timeout budgets, response shapes, or
platform fallback policy.
Current state
Relevant files and their roles:
src/commands/interaction/runtime/selector-wait.ts— implements duration,selector, text, ref, and stable waits. Its context already permits command-
and runtime-level signals, but its duration branch does not use them.
src/commands/interaction/runtime/wait-polling.ts— owns the existingcancellation-aware wait sleep used between selector/text polls.
src/daemon/selector-runtime-backend.ts— exemplar for installing theregistered daemon request signal on an
AgentDeviceRuntime.src/daemon/snapshot-runtime.ts— shared runtime path for bothsnapshotand
diff snapshot; currently creates a signal-less runtime and discards thebackend command context.
src/daemon/handlers/snapshot-capture.ts— lower capture layer. It alreadyaccepts
signaland forwards it to Linux, the macOS helper, and dispatchedplatform snapshots; it should not need modification.
src/request/cancel.ts— sole request-cancellation registry. This plan mustconsume its signal, not create a second controller or cancellation registry.
The duration branch has both signal authorities in its types, but calls a
signal-blind helper:
The polling path already has the behavior to reuse. It checks both authorities,
preserves injected-clock behavior, clears the real timer on abort, removes the
listener, and
unrefs the timer:The selector runtime shows the intended daemon wiring:
The snapshot runtime omits that field, and its backend ignores the context that
captureRuntimeSnapshotsupplies:No platform-specific implementation work is needed. The lower seam is already
signal-aware:
Design and repository constraints to preserve:
docs/adr/0018-unified-event-journal.md:216-224defines client-disconnecttracking as transport-owned behavior that feeds request cancellation. Do not
move cancellation ownership into wait or snapshot modules.
src/request/cancel.ts:58-81,112-115owns registrations and exposesgetRequestSignal; consume that exact signal.docs/agents/testing.md:68-75requires every regression test to be observedred against the pre-fix production code and the failing assertion recorded.
docs/agents/testing.md:481-493forbids real-time sleeps in unit tests andtest-only dependency-injection seams. Use a controlled fake clock/promise and
the existing production request-signal registry.
Cancellation is request authority, not a new reason to remove those fallbacks.
Commands you will need
pnpm depgraph affected src/commands/interaction/runtime/wait-polling.ts --limit 25pnpm depgraph affected src/daemon/snapshot-runtime.ts --limit 25pnpm exec vitest run src/commands/interaction/runtime/selector-wait-cancellation.test.ts src/commands/interaction/runtime/wait-polling.test.ts src/daemon/__tests__/snapshot-runtime-cancellation.test.ts src/daemon/selector-runtime-backend.test.ts src/daemon/__tests__/snapshot-quality-latch.test.tspnpm check:quickpnpm formatpnpm check:affected --runpnpm checkThe repository requires pnpm 11.17.0 and Node >=22.12. Do not create or
restore
package-lock.json. No Apple target build is required because thisplan changes TypeScript runtime wiring only;
pnpm check:affected --runremainsthe authority if the live diff selects additional gates.
Scope
In scope (the only implementation/test files to modify or create):
src/commands/interaction/runtime/selector-wait.tssrc/commands/interaction/runtime/wait-polling.tssrc/commands/interaction/runtime/selector-wait-cancellation.test.ts(create)src/daemon/snapshot-runtime.tssrc/daemon/__tests__/snapshot-runtime-cancellation.test.ts(create)Out of scope (do NOT touch, even though they are related):
src/request/cancel.tsand daemon HTTP/socket transports — cancellationregistration and disconnect detection already work and are not being redesigned.
src/daemon/handlers/snapshot-capture.ts, platform interactors, Androidhelpers, Linux/macOS helpers, and provider adapters — these already accept a
signal; the missing seam is above them.
with the request-id registration; do not add an Apple-specific fallback.
public error normalization, CLI/MCP schemas, response shapes, and ref-frame rules.
unchanged and the transport cancellation contract is already documented.
plans/README.mdand every other plan file.Git workflow
advisor/003-complete-request-cancellationoperator wants multiple commits. Suggested final commit message:
fix: complete request cancellation propagationfix:,perf:,refactor:,test:), as shown by recent history.Steps
Step 1: Add deterministic red regression tests
Create
src/commands/interaction/runtime/selector-wait-cancellation.test.ts.Use the public runtime command (
device.selectors.wait), not the private sleephelper. Model setup after
selector-wait.test.ts: construct a localcreateAgentDevicewith a minimal backend, memory session store, local policy,and an injected clock. The clock's
sleepmust return a manually controlledpromise rather than waiting real time.
Cover both cancellation authorities in a table/loop:
createAgentDevice({ signal })), anddevice.selectors.wait({ signal, target: ... })).For each authority, start a duration wait, wait until the fake clock reports
that sleep began, abort with a unique
Errorreason, release the fake sleep,and assert the wait rejects with that exact reason instead of returning
{ kind: 'sleep' }. This exercises abort-during-sleep deterministically; italso proves the injected-clock post-sleep check remains load-bearing. Do not
use
setTimeout, fake global timers, or a test-only production parameter.Create
src/daemon/__tests__/snapshot-runtime-cancellation.test.ts. Model itsmodule mock and scenario setup after
src/daemon/__tests__/snapshot-quality-latch.test.ts, but usemakeAndroidSession,makeSessionStore/mkdtempForTestSync, and the requestcancellation helpers from their existing production modules. Partially mock
dispatchCommandwhile keeping the real module's other exports.Use a table covering both
dispatchSnapshotViaRuntimeanddispatchSnapshotDiffViaRuntime. For each command:registerRequestAbortand put it inreq.meta;context.signal, waiton a manually controlled promise, and call
context.signal?.throwIfAborted()immediately after that promise resolves;
markRequestCanceled(requestId), then release the mock so the test cannever hang even when the signal is absent;
command rejects from abort rather than completing successfully; and
finallyso module-global state cannot leak.Run the focused command before changing production. At commit
13bc70f24, theduration tests should resolve instead of reject after the fake clock is
released, and snapshot/diff should observe no dispatch signal and complete when
the mock is released. Record the failing test names/assertions in the PR or
handoff; this is the required red proof.
Verify (red, before production edits):
pnpm exec vitest run src/commands/interaction/runtime/selector-wait-cancellation.test.ts src/daemon/__tests__/snapshot-runtime-cancellation.test.ts→ exits nonzero only on the new cancellation assertions, with no timeout or
unhandled-promise failure.
Step 2: Route duration waits through the existing cancellable sleep
In
src/commands/interaction/runtime/wait-polling.ts, export the existingsleepWithinWaitproduction helper (a durable name such assleepWithWaitCancellationis also acceptable if every existing call isupdated). Do not duplicate its logic. Preserve all existing semantics:
options.signalandruntime.signal;clock.sleep;remove the listener on both paths, reject with the signal's reason, and
unrefthe timer; andIn
src/commands/interaction/runtime/selector-wait.ts, import that helper andpass the full
optionsobject from the duration branch so command-level andruntime-level signals are both visible. Remove the private signal-blind
sleepfunction at the bottom of the file. Preserve the successful resultexactly:
{ kind: 'sleep', waitedMs: options.target.durationMs }.Verify:
pnpm exec vitest run src/commands/interaction/runtime/selector-wait-cancellation.test.ts src/commands/interaction/runtime/wait-polling.test.ts src/commands/interaction/runtime/selector-wait.test.ts→ all pass; no test waits on wall-clock duration.
Step 3: Install and forward the request signal in snapshot/diff runtime
In
src/daemon/snapshot-runtime.ts:getRequestSignalfromsrc/request/cancel.tsusing the correctrelative path;
signal: getRequestSignal(req.meta?.requestId)to the object passed tocreateAgentDeviceincreateSnapshotRuntime, matchingcreateSelectorRuntimeForDevice;_contexttocontext; andsignal: context.signalintocaptureSnapshot.Do not query the registry again inside the backend and do not add platform
conditionals. The runtime command already selects
options.signal ?? runtime.signalwhen it builds the backend context, so this one seam coversboth
snapshotanddiff snapshot. The lower capture layer then forwards thesame signal to non-Apple backends; Apple may merge it with the same registered
signal and must retain its existing behavior.
Verify:
pnpm exec vitest run src/daemon/__tests__/snapshot-runtime-cancellation.test.ts src/daemon/__tests__/snapshot-quality-latch.test.ts src/daemon/handlers/__tests__/snapshot-handler.test.ts→ all pass, including both new command cases and existing quality/session behavior.
Step 4: Tighten and run repository gates
Review the diff for duplicated cancellation logic, stale imports, or a second
request controller. Confirm the snapshot fix is only signal plumbing and did
not alter capture options, annotations, session writes, error conversion, or
fallback selection. Confirm duration wait success output is unchanged.
Run formatting, the focused suite, fast static checks, the affected selector,
and the broad deterministic gate in that order. If formatting modifies any
file outside Scope, inspect why and revert only the unrelated formatter drift;
do not sweep it into this change.
Verify:
pnpm format pnpm exec vitest run src/commands/interaction/runtime/selector-wait-cancellation.test.ts src/commands/interaction/runtime/wait-polling.test.ts src/commands/interaction/runtime/selector-wait.test.ts src/daemon/__tests__/snapshot-runtime-cancellation.test.ts src/daemon/selector-runtime-backend.test.ts src/daemon/__tests__/snapshot-quality-latch.test.ts pnpm check:quick pnpm check:affected --run pnpm check→ every command exits 0.
git diff --name-onlylists only the in-scopeimplementation/test files plus an authorized plan-status update.
Test plan
src/commands/interaction/runtime/selector-wait-cancellation.test.ts:src/daemon/__tests__/snapshot-runtime-cancellation.test.ts:snapshotforwards the exact registered request signal and aborts;diff snapshotforwards the same authority and aborts;hanging the test;
finally.wait-polling.test.tsalready proves real-timer cancellation for runtimeand command authorities;
selector-runtime-backend.test.tsis the runtime signal-wiring exemplar;snapshot-quality-latch.test.tsandsnapshot-handler.test.tsprotectsnapshot/diff response and session semantics.
record the specific failing assertions. A timeout is not acceptable red evidence.
Done criteria
ALL must hold:
for the expected signal/cancellation assertions, and that red output is
recorded in the PR or handoff.
real-time test sleeps.
snapshotanddiff snapshotforward the exact registered request signalthrough the shared runtime backend.
rg -n "async function sleep\(" src/commands/interaction/runtime/selector-wait.tsreturns no matches.
rg -n "captureSnapshot: async \(_context" src/daemon/snapshot-runtime.tsreturns no matches.
pnpm check:quickexits 0.pnpm check:affected --runexits 0.pnpm checkexits 0.docs/skills are intentionally unchanged because the public contract and
command surface did not change.
plans/README.mdand every other plan file are unchanged by the executor.STOP conditions
Stop and report back; do not improvise if:
test-only production parameter to fail before the fix.
wait timeout accounting, injected-clock semantics, or successful duration output.
snapshot-capture.ts, a platforminteractor/helper, transport registration, or public error normalization;
those layers already expose the needed signal seam at this commit.
AbortController, registry, message sniff, orplatform-specific cancellation fallback instead of using
getRequestSignal.are the same authority.
Maintenance notes
transport registration →
getRequestSignal(requestId)at runtime creation →CommandContext.signal→ backend operation. Review new runtimes for all fourlinks rather than teaching individual platforms to query daemon globals.
pauses should reuse it so runtime and command signals cannot drift again.
a green cancellation test that leaves a pending timer or global request entry
is incomplete.
this plan. Cancellation must stop the request; ordinary optional-optimization
failure may continue to use the documented fallback path.
request-lifecycle guarantee and does not change user-facing command behavior.