feat(spawner): expose a timed-out sub-session's partial output via session.partial (k64 prereq 2/2, PRODUCER) - #297
Conversation
…ssion.partial PRODUCER half of the per-delegate timeout partial-result path. The CONSUMER shipped in amplifier-foundation f42f48c (PR #353): on a wall-clock timeout the delegate now RETURNS rather than raises, and reads an optional app-layer `session.partial` capability. Nothing offered one, so `partial_available` was false for every real timeout. This is what offers one. A sub-session cancelled by its per-delegate timeout previously had everything it had produced discarded: `child_session.execute()` is cancelled, the post-run block never runs, the child is cleaned up. This accumulates the agent's own assistant text (content_block:end) and publishes it under the sub_session_id, so the delegate hands the caller INCOMPLETE-with-partial instead of an empty failure. The record is published EAGERLY and updated in place, not sealed from the child's unwind. That is not a stylistic choice. tool-delegate's `_await_child_with_deadline` deliberately does not wait for a slow unwind: it cancels the child, DETACHES it, and reads `session.partial` immediately. A partial published from the child's `except BaseException:` handler arrives after the consumer has already reported `partial_available: false` -- measured cross-repo, with both halves' unit tests passing. `test_partial_is_readable_before_the_child_has_unwound` pins the ordering in-repo, without needing foundation on the path. Normal completions are untouched: the returned dict gains no key and is byte-identical (identical sha256 of the serialized result, parent vs patched), and the registry is cleared on the success path. Absent/None/raising degrades to `partial_available: false`, never to an error -- raising out of the timeout path would discard the completed siblings that path exists to protect. Evidence, round-trip check and probe under docs/lanes/9w0-delegate-timeout-partial-producer/.
Manager verification — FIX, and this is the commit that unblocks k64's $60 evalHead Full suite on the head: The gate that actually mattered — reproduced by me, not read off the evidence filek64's G-D4 stops the whole eval with and the lane's own captured contrast holds on the same wiring:
So the producer/consumer pair is closed end to end. Normal completions byte-identical — sha256 match, diff empty. Shown, not asserted. The correction I want on the record, because it is the honest halfThe goal told this lane to expect two named pre-existing failures (37n's The lane reported that as a correction rather than manufacturing the failures the goal predicted. That is exactly right — a stale expectation in a goal is not licence to produce matching output, and 37n's baseline had simply moved on. Fail-before for the new test file is collection-level on the parent (new symbols), which is weak on its own — the behavioural parent/patched contrast above is the real evidence, same pattern as FIX with the decisive gate reproduced → merging. Squash + |
What this is
The PRODUCER half of the per-delegate-timeout partial-result path.
It completes the hard prerequisite of
model_performance-k64.The CONSUMER half is already merged: amplifier-foundation
f42f48c(
model_performance-bp0, foundation PR #353). That side makes a timed-outdelegate return rather than raise, and read an optional app-layer
session.partialcapability:Nothing in the app registered one, so
partial_availablewas false for everyreal timeout. This PR is what offers one.
k64's gate G-D4 stops the whole eval with
PARTIAL-PATH-NOT-EXERCISEDunless at least one timeout carries
partial_available: true. It now can.The non-obvious part — do not "simplify" this back
The designed patch published the partial from an
except BaseException:handleraround
child_session.execute(). Against the real merged consumer that stillreports
partial_available: false, and it does so silently — both halves' unittests pass.
tool-delegate's_await_child_with_deadlinedeliberately does not wait fora child that is slow to unwind: at the deadline it cancels the child, detaches
it, and raises. Its handler then calls
session.partialimmediately — before thecancelled child has been scheduled to run any handler of its own. Observed in one
run:
The producer preserved the work one line after the consumer gave up on it.
So the record is published eagerly and updated in place, readable at any
instant, with no dependence on cancellation ordering — and removed when the
sub-session completes normally.
test_partial_is_readable_before_the_child_has_unwoundpins that in-repo, without needing foundation on the path.
Evidence
All under
docs/lanes/9w0-delegate-timeout-partial-producer/evidence/.Fail-before — same cross-repo round trip, parent app-cli × real foundation
f42f48c:Counter-evidence — the originally designed shape (publish only from the
child's unwind), everything else identical, still
false:Pass-after — patched app-cli × real foundation
f42f48c, overlaid copies(
cp -rL), neither repo mutated: 2 passed.G-D4 reached — same probe, parent vs patched:
session.partialregisteredFalseTruepartial_availablefalsetruepartial_responsenull"anchor A1 confirmed. anchor A2 confirmed. "partial_segments02partial_source"none""spawn-accumulator"partial_chars_total042ToolResult.successFalseFalsestatustimeouttimeoutNormal completions byte-identical — shown, not asserted. Same probe, same
fixed sub_session_id, only
amplifier_app_clidiffers:The returned dict is exactly
{output, session_id, status, turn_count, metadata}— no new key, no shape change — and the registry is cleared on the success path.
Suites
ab47608tests/ruff checkA correction to the item's premise, stated rather than manufactured: the two
TestSpawnEnrichment::test_{spawn,resume}_result_includes_status_and_turn_countfailures reported at
f16375fcdo not exist atab47608— both PASS, onthe parent and on this branch. The baseline improved between those commits.
Ordering
The consumer half is already merged (foundation
f42f48c), so the orderingconstraint from the design (
DESIGN.md §1e— enabling the producer before theconsumer is the data-loss configuration) is satisfied. This PR is safe to merge
now.
settings.timeoutstill defaults toNone; this PR ships mechanism only anddoes not enable, sweep, or bank a timeout value.
Scope
amplifier_app_cli/session_spawner.py,amplifier_app_cli/session_runner.py,one new test file, and lane artifacts under
docs/lanes/. No other repo touched.Full write-up:
docs/lanes/9w0-delegate-timeout-partial-producer/DONE-NOTE.md.