Skip to content

fix(native): bound adoption and authored product clocks by the caller deadline - #499

Draft
roodboi wants to merge 3 commits into
nextfrom
wf/g106-product-clock-bounds
Draft

roodboi wants to merge 3 commits into
nextfrom
wf/g106-product-clock-bounds

Conversation

@roodboi

@roodboi roodboi commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Refs HACK-1458 (unit G106). Product fix for the four product clocks that kept HACK-1367's loaded runs from passing. Test-side budgets stay with #419 (G52/G105); this PR adds new cases at the end of the two shared test files and edits none of #419's hunks.

Root cause

  1. Public adoption dispatch. tryLegacyComposeAdoptedCommand opened its adoption store without a deadline, so every store inspection phase (one probe owner) got the default 15 s aggregate clock, whatever HACK_COMPOSE_STARTUP_TIMEOUT_MS allowed. A slow engine expired it as E_NATIVE_COMPOSE_PROBE_TIMEOUT; translate() turned that into E_LEGACY_ADOPTION_STATE, and the command reported E_CONFIG_INVALID ("state is invalid, unsafe or changed"). The command's own deadline only started after loadActive, so inspection before it was never bound by the caller.

  2. Authored compiler calls. acquireNativeExecutionInputs never passed a compiler timeout, so each compiler invocation (and the later assertFresh/value checks) kept the transport's fixed 10 s default, even with minutes of startup budget left.

  3. Source5 cancellation after readiness hang. Reproduced: after onReady aborts, the cancel's down.before re-acquires inputs with the hard signal. That acquisition also kept the fixed 10 s compiler clock. When one compiler call took longer, down.before failed at acquire (no hook ran), onStopFailure fired, and the start retained the live owner and host processes, waiting for a hard cancel that the case never sends. With a scratch copy whose compiler sleeps 11 s after readiness, the case hung for its whole 420 s budget, with hook events down.before:acquire then down.before:stop-operation and no down hook, the same symptoms as the loaded report.

  4. Failed authored cases leave host process groups running. This is a product path, two of them:

    • (i) Startup inputs check freshness under the startup signal. Once a cancel or the startup deadline (its timer aborts the same signal) ends startup after the host processes started and before reservation, stopUnstartedHostProcesses could never pass hostProcesses.stop()'s freshness check (it re-reads inputs with the aborted signal), so the groups stayed up. A loaded local run of a new case showed it directly: startup expired after host-process-ready.json was written, the start failed, and the mux window group was still alive afterwards.
    • (ii) createNativeAuthoredHostProcesses().start() had no failure path. A failure after the launch intent (cancel, deadline or refused check during the launch) left the mux session running behind an intent that recovery never converts into authority.

    Failures after reservation still retain host processes on purpose, with complete owner/ready/state records that hack down recovery stops (retireNativeAuthoredHostProcesses). Ending those in a test is test cleanup (test: derive load-sensitive harness budgets from named constants #419).

Fix

  • Adoption (native-compose-adoption-generation.ts, native-compose-adoption-command.ts):
    • The store takes an optional caller deadline. With one, each inspection phase gets legacyAdoptionProbePhaseMs: the remaining time, capped at LEGACY_ADOPTION_PROBE_PHASE_MAX_MS (60 s). An expired deadline refuses as E_LEGACY_ADOPTION_DEADLINE, and an unclassified failure after a probe timeout or past the deadline becomes E_LEGACY_ADOPTION_DEADLINE, not E_LEGACY_ADOPTION_STATE. A store without a deadline is unchanged.
    • The command starts one deadline (HACK_COMPOSE_STARTUP_TIMEOUT_MS) before it opens the store and uses it for inspection, the retained operation, the plain container runner and ps inspection. E_LEGACY_ADOPTION_DEADLINE maps to E_STARTUP_TIMEOUT. The message stays the store's fixed text, so the closed legacy-deadline diagnostic token is unchanged.
  • Compiler (native-config-compiler.ts, native-project-validation.ts, native-execution-inputs.ts): named NATIVE_CONFIG_COMPILER_MIN_TIMEOUT_MS (1) and NATIVE_CONFIG_COMPILER_MAX_TIMEOUT_MS (60 000), nativeConfigCompilerTimeoutWithin(remainingMs), and an optional compilerTimeoutMs getter that every compiler invocation of a selection reads, including later freshness checks.
  • Authored start (native-authored-project-start.ts):
    • Startup compiler calls use the remaining startup deadline (floor 1 ms, ceiling 60 s). Calls outside startup (down-hook acquisition, checks after ready) have no startup deadline and take the 60 s ceiling, so a slow host no longer fails a graceful stop.
    • Before stopping unstarted host processes, teardown re-acquires inputs under the hard signal and refuses a changed selection, like the down hooks do.
  • Host processes (native-authored-host-processes.ts): a start that fails after its launch intent calls the controller's abort(), which ends the session through the ownership token it created. Owner bookkeeping is cleared only when every launched group was recorded and is proven absent within 5 s. Otherwise it stays retained.
  • Docs: docs/reference/native-compose-adoption.md, docs/reference/native-authored-commands.md.

Tests

New cases, each proven by mutation (product change reverted in a scratch copy):

Case File Clock Mutation result
HACK-1458: public dispatch inspection outlives the former fixed 15 s phase clock while the caller deadline remains adoption generation injected Date.now jump of 16 s after the first engine probe, 120 s deadline base: E_CONFIG_INVALID "state is invalid, unsafe or changed"
HACK-1458: an expired public dispatch deadline ends inspection as a startup timeout, not invalid config adoption generation injected 6 s jump, 5 s deadline base: returns 0 (inspection ignored the expired deadline and started the container)
HACK-1458: a startup compiler call may outlast the compiler's 10 s default while the startup deadline remains authored start real: one compile sleeps 10.5 s wiring reverted: stage inputs (E_COMPILER_TIMEOUT) after 11.4 s
HACK-1458: Source5 cancellation after readiness settles when its down acquisition compiler is slow authored start real: first compiler call after readiness sleeps 10.5 s pending (running)
HACK-1458: cancellation after host processes start and before reservation stops them authored start real: second frontend-plan held until cancelled teardown refresh removed: process-order is started only and the host group stays alive
HACK-1458: cancellation during the host process launch ends the launched group authored start real: first list-panes delayed 1 s abandonLaunch made a no-op: the owned mux session (mux.json) stays

Evidence

In progress: 10-consecutive loaded runs of both whole files are running locally (M5, Bun 1.4.2, private HOME/HACK_HOME/DOCKER_CONFIG, keychain fallback disabled, unreachable DOCKER_HOST, added CPU load stopped in a trap), on this branch plus #419's test-side budgets. Hosted CI at the exact head is the full-suite evidence. This section will be updated with results.

Gates

  • gitleaks 8.30.1 dir over a git log -p origin/next..HEAD export: no leaks.
  • Root bun run check (includes privacy:check): passed. bun run --cwd packages/cli typecheck: 0 errors. Biome on the changed files: no new warnings (the 8 existing complexity warnings in native-compose-adoption-generation.ts remain).
  • The CLI surface and consumer agent guidance did not change, so docs:cli-reference and plugin regeneration do not apply.
  • origin/next merged at 246f1b4.

Gaps

To be completed with the evidence.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

roodboi and others added 2 commits October 24, 2026 16:33
… deadline (HACK-1458)

Public adoption dispatch now starts one command deadline before it opens the
store. Each store inspection phase takes the time left on that deadline,
capped at 60 s, instead of a fixed 15 s clock, and an expired deadline
returns E_STARTUP_TIMEOUT with the store's fixed deadline message instead of
E_CONFIG_INVALID.

Authored startup compiler calls are bounded by the remaining startup deadline
(floor 1 ms, ceiling 60 s) instead of a fixed 10 s each. Compiler calls outside
startup, including the down-hook acquisition after a cancel, take the 60 s
ceiling: the fixed 10 s clock failed the graceful stop under load, which then
retained the live owner and host processes until a hard cancel (the
"Source5 cancellation after readiness" hang).

A start that fails or is cancelled after host processes began and before
reservation now stops them: their teardown re-acquires inputs under the hard
signal, because the startup inputs refuse once the startup signal aborts. A
host process launch that fails part-way ends its owned session through the
token it created.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Oct 24, 2026

Copy link
Copy Markdown

HACK-1458

…nslation

An adopted store with a caller deadline still reports a held lock as
E_LEGACY_ADOPTION_BUSY; only an otherwise unclassified failure becomes the
deadline refusal (HACK-1458).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

1 participant