Repository navigation
Managed sessions: fix slow session-host startup and mismatched time limits - #861
Merged
Merged
Conversation
A managed terminal or fresh-agent dispatch that outran the supervisor's reply bound left no trace of where the time went once its container was gone. Every grant now records a host.grant_timing event (severity, grant id, outcome, refusal code, total and per-stage milliseconds) in the incarnation's host-lifecycle.jsonl and on the host's stderr, for accepted and refused grants alike. This is what showed that a 4,200-file selected provider environment spent 10.5-12.8 s of a grant inside prepare_provider_state while every other stage took milliseconds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…onments dispatch The provider projection makes every staged node durable before it is published, and it flushed them one at a time inside the execution grant. Each flush is its own journal commit, so a selected plugin with a 4,200-file node_modules spent 8-13 s of the grant flushing on the qualification host (about 2 ms per file, far more under I/O pressure), past the supervisor's grant reply bound. The host then consumed the grant after the supervisor had already declared it unreachable. Flushes of independent nodes now overlap on a bounded worker set so the filesystem journal commits them together (4,200 files: 13.4 s serial, 1.4 s at 16 workers). Every node is still flushed exactly once with a full fsync, symlinks are still not followed, the barrier still completes before the transaction advances, and the calling thread always takes part so a refused thread spawn cannot stall it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dget The supervisor bounded a terminal GrantExecution reply by the 10 s control round-trip budget, the same bound as hello, status and stop. A grant is not a control round trip: the host answers only after it has prepared the selected provider inputs and started the provider, and those steps carry their own inner bounds inside the host (Codex app-server start 45 s, Claude final source reader 20 + 5 s, OpenCode native-auth reader 20 + 5 s). An outer bound shorter than the host's own inner bounds abandoned healthy dispatches half-way: the host consumed the grant and started the provider at 10.6-12.9 s while the supervisor had already recorded deliver_execution_grant HOST_UNREACHABLE, torn the incarnation down and blocked recovery as PROVIDER_UNAVAILABLE. Fresh-agent grants already used a 60 s dispatch bound for the same reason. Reply budgets are now resolved once into HostCommandBudgets and chosen by what the command makes the host do: control round trips keep FRESHELL_RUNTIME_HOST_COMMAND_TIMEOUT_MS (10 s), terminal grants use FRESHELL_RUNTIME_TERMINAL_DISPATCH_TIMEOUT_MS (60 s, documented against the inner bounds above) and fresh-agent grants and turn operations keep FRESHELL_RUNTIME_FRESH_AGENT_COMMAND_TIMEOUT_MS. A silent host is still released at the dispatch bound. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Claude creation or recovery whose native source observation failed
surfaced only the fixed sanitized refusal ("evidence unavailable") to the
web server, and the supervisor has no tracing subscriber, so the exact
check that stopped it was lost. A blocked Claude recovery showed nothing
but CAPABILITY_PENDING.
The observation now records supervisor.claude_source_observation.failed
in the supervisor lifecycle log (severity, soul, provider, authority kind,
candidate flag, error code and the specific refused check or reader
failure). The web caller still receives only the fixed refusal.
The candidate scratch absence probe also stops calling every non-404 a
name that is "already present": only an answered lookup proves presence,
and any other refusal is reported as absence that could not be confirmed.
Nothing is created in either case.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in browser fixtures The restricted Docker broker that fronts browser runtime fixtures could qualify a controller-published Claude candidate only for initial creation. A recovery candidate (registry attempt, prior generation, no initial reservation) was refused at owner_capability, so its scratch absence probe got a 403 and the supervisor blocked every managed Claude terminal recovery as CAPABILITY_PENDING. The provider parity suite timed out waiting for the Claude replacement incarnation. Initial and recovery publications now share one qualifier for both the absence probe and the later volume registration. A recovery candidate must name a well-formed registry attempt and a different prior generation, its capability must be the pending replacement of exactly that generation, and its retained profile (reservation, binds and resource units) comes from the frozen prior stage, the same profile the closed launch fixture hands off explicitly. The registration also now requires the exact candidate labels and, for a creation candidate, its reservation authority digest, which the absence probe already required. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve send_authenticated_host_command: keep the resolved HostCommandBudgets selection and route its fresh-agent class through main's uses_fresh_agent_command_budget, so FreshAgentConfigure keeps the fresh-agent budget and terminal grants keep the dispatch budget. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The budget tests gave terminal grants and fresh-agent operations the same configured value, so routing a terminal grant to the fresh-agent budget (or reading the wrong variable for it) still passed. Give control, terminal dispatch, and fresh-agent budgets distinct values and assert that each command class times out at its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…upervisor's wait The 60s terminal grant wait was documented as longer than the host's inner limits, but it is not: a terminal Codex start retries its 45s app-server start up to five times (about 251s), and an exact resume runs the 25s provider-owned refresh (plus up to 32s of recovery) first. A slow but healthy dispatch could still be abandoned by the supervisor while the host kept working on it. No fixed wait covers every such sum, so the grant now carries its own dispatch budget (`dispatch_budget_ms`): the supervisor's wait less a 5s reply allowance, never less than half the wait. The host runs every awaited dispatch stage inside it and cuts the stage at the deadline, never crosses the native dispatch boundary once it is spent (the grant stays unconsumed), and refuses with a typed HostUnreachable naming the stage the budget ran out before. `host.grant_timing` records `budgetMs` and `budgetExhaustedBefore`. The wait and the budget now have accurate docs, including that a stop issued during activation waits for it to settle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…observations Recovery's provider-preparation callback to the web server waited 35s and its recovery scope expired after 30s, yet a Claude recovery's callback makes two source observations back to back through the supervisor before it answers, each bounded at 35s on its own. An observation that finished inside its own bound could still be cut off by the callback around it, or find its scope expired, and the recovery blocked as CAPABILITY_PENDING. In the failing parity run on the previous head the candidate reader hit its own 25s limit 32.3s after the recovery was scheduled, inside a callback allowed 35s, under a load average near 600. A Claude recovery callback now waits 105s (the 35s callback wait plus two 35s observation bounds); every other callback keeps 35s. The recovery scope lives exactly as long as the callback wait it serves. The timeout message names the wait it hit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ETXTBSY The pre-push workspace run failed provider_config_two_stage_script_is_runnable with "Text file busy": another test thread forked while the fixture was still open for writing, its child held a copy of that write descriptor until it exec'd, and exec of the fixture was refused. Run freshly written fixtures through a helper that retries only that transient refusal for up to 500ms (the platform crate's fixtures already do), with a test that holds a write descriptor open to prove it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The provider parity suite (
test/integration/server/provider-parity-contract.test.ts) was failing on main. Managed terminal sessions timed out while starting ("timed out waiting for claude/amplifier replacement incarnation"). The supervisor'sdeliver_execution_grantgave up after 10 seconds, and Claude recovery got stuck asCAPABILITY_PENDING.Root causes (with evidence)
host.grant_timing) showed that nearly all of a 10.5–12.9 s grant was spent inprepare_provider_state. That stage flushed each of the selected plugin's roughly 4,200 files to disk one at a time, at about 2 ms per file. Run one at a time, the flushes took 13.4 s; run 16 at a time, they took 1.4 s.CAPABILITY_PENDINGand nothing logged why.Fix
PROVIDER_PROJECTION_SYNC_WORKERS), and every grant records how long each stage took.FRESHELL_RUNTIME_TERMINAL_DISPATCH_TIMEOUT_MS.dispatch_budget_ms). The budget is the supervisor's wait minus 5 s, and never less than half the wait (55 s by default). The host runs each awaited step inside that budget and stops it at the deadline. Once the budget is spent, it does not start the provider, so the grant can still be retried. It answers with a typedHostUnreachableerror that names the step it was on.host.grant_timingnow also recordsbudgetMsandbudgetExhaustedBefore. As a result, the supervisor never gives up on a dispatch the host is still working on.supervisor.claude_source_observation.failed), and the test Docker proxy now allows recovery scratch-volume checks under the same strict rules as the launch fixture.docs/development/managed-runtime.mddocuments each limit and where it comes from. It also notes that a stop issued while a session is starting waits until the start finishes or fails, up to the grant limit plus the callback limit during a recovery.Tests run
cargo test -p freshell-runtime-protocol -p freshell-session-host -p freshell-supervisor: all passed (protocol 66; session host 164 + 4 + 29 + 5; supervisor 195).6af75b199(cargo test --workspace --exclude freshell-tauri --locked, clippy, fmt, typecheck): passed.budgetMs.cargo clippy --tests -D warningsandcargo fmt --checkon the changed crates: clean.cargo check -p freshell-server --tests: clean.pnpm run test:vitest run test/unit/tooling/testing/runtime-test-broker.test.ts --config config/vitest/vitest.config.ts: 61/61 passed.pnpm run test:vitest run test/integration/server/provider-parity-contract.test.ts --config config/vitest/vitest.config.ts, run 6 times locally:dd309cb40dd309cb40dd309cb404d6cd8f8f(after merging main)4d6cd8f8f4d6cd8f8fNo Playwright spec was changed; the browser specs that use the test Docker proxy are local-only.
Remaining problem (not fixed here)
Runs 3 and 4 failed for a different reason from the ones above: creating a new freshclaude session failed with
FRESH_AGENT_CREATE_FAILED(marked retryable). The supervisor loggedClaude candidate reader timed outin one run andClaude candidate scratch cleanup failedin the other.Before that failure, a single Docker
POST /containers/createfor a short-lived helper container (freshell-mountpoints-*orfreshell-claude-source-*) took 18–31 s. That used up the Claude source reader's own 25 s limit. In a reproduction with timing added to the proxy, one create took 20,010 ms while other Docker calls kept answering in milliseconds and the test process was not stalled. A Docker stack dump taken during a slow create showed dockerd insidecreateContainerVolumesOS, mounting and unmounting the new container's root filesystem, while containerd was idle. On this shared host, the median time for these helper creates ranged from 52 ms to 501 ms depending on the run, with occasional spikes over 5 s. Session-host container creates never went above 0.4 s.This is not caused by the grant or callback limits fixed here, and the exact cause inside Docker is still unknown. Fixing it needs a decision: either give the Claude source reader a budget that can absorb slow Docker creates, or retry the retryable create and recovery failures.
🤖 Generated with Claude Code