Skip to content

feat(spawner): expose a timed-out sub-session's partial output via session.partial (k64 prereq 2/2, PRODUCER) - #297

Merged
Brian Krabach (bkrabach) merged 2 commits into
mainfrom
lane/9w0-delegate-timeout-partial-producer
Sep 3, 2026
Merged

feat(spawner): expose a timed-out sub-session's partial output via session.partial (k64 prereq 2/2, PRODUCER)#297
Brian Krabach (bkrabach) merged 2 commits into
mainfrom
lane/9w0-delegate-timeout-partial-producer

Conversation

@bkrabach

Copy link
Copy Markdown
Collaborator

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-out
delegate return rather than raise, and read an optional app-layer
session.partial capability:

(sub_session_id: str) -> {"text": str, "segments": int, "source": str} | None

Nothing in the app registered one, so partial_available was false for every
real timeout
. This PR is what offers one.

k64's gate G-D4 stops the whole eval with PARTIAL-PATH-NOT-EXERCISED
unless 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: handler
around child_session.execute(). Against the real merged consumer that still
reports partial_available: false, and it does so silently — both halves' unit
tests pass.

tool-delegate's _await_child_with_deadline deliberately does not wait for
a child that is slow to unwind: at the deadline it cancels the child, detaches
it, and raises. Its handler then calls session.partial immediately — before the
cancelled child has been scheduled to run any handler of its own. Observed in one
run:

WARNING tool_delegate    Agent 'explorer' timed out after 1s … No partial output could be recovered.
WARNING session_spawner  Sub-session …_explorer did not complete; preserved 2 partial text segment(s), 42 chars

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_unwound
pins 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:

E  AssertionError: session.partial produced nothing -- k64's G-D4 would record PARTIAL-PATH-NOT-EXERCISED here
E  assert False is True
WARNING tool_delegate  Agent 'explorer' timed out after 1s … No partial output could be recovered.
2 failed in 3.29s

Counter-evidence — the originally designed shape (publish only from the
child's unwind), everything else identical, still false:

E  assert False is True
WARNING tool_delegate    … No partial output could be recovered.
WARNING session_spawner  … preserved 2 partial text segment(s), 42 chars      <-- too late
2 failed in 3.30s

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:

key parent patched
session.partial registered False True
partial_available false true
partial_response null "anchor A1 confirmed. anchor A2 confirmed. "
partial_segments 0 2
partial_source "none" "spawn-accumulator"
partial_chars_total 0 42
ToolResult.success False False
status timeout timeout

Normal completions byte-identical — shown, not asserted. Same probe, same
fixed sub_session_id, only amplifier_app_cli differs:

$ diff <(probe normal @parent) <(probe normal @patched)
(no differences)
ef8c86fdaf3b68e26e02e86e6a1bd59e0b638e4eb9b36f8f0a7f9f0138a35700  normal-parent.json
ef8c86fdaf3b68e26e02e86e6a1bd59e0b638e4eb9b36f8f0a7f9f0138a35700  normal-patched.json

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

parent ab47608 this branch
tests/ 1647 passed, 1 skipped, 1 xfailed 1659 passed, 1 skipped, 1 xfailed
named suites (spawner/runner/issue-233) 73 passed, 0 failed 73 passed, 0 failed
ruff check 14 pre-existing errors same 14, none in touched files

A correction to the item's premise, stated rather than manufactured: the two
TestSpawnEnrichment::test_{spawn,resume}_result_includes_status_and_turn_count
failures reported at f16375fc do not exist at ab47608 — both PASS, on
the parent and on this branch. The baseline improved between those commits.

Ordering

The consumer half is already merged (foundation f42f48c), so the ordering
constraint from the design (DESIGN.md §1e — enabling the producer before the
consumer is the data-loss configuration) is satisfied. This PR is safe to merge
now.

settings.timeout still defaults to None; this PR ships mechanism only and
does 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.

…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/.
@bkrabach
Brian Krabach (bkrabach) marked this pull request as ready for review September 3, 2026 10:34
@bkrabach

Copy link
Copy Markdown
Collaborator Author

Manager verification — FIX, and this is the commit that unblocks k64's $60 eval

Head 7f749c0, base ab47608 = current origin/main. CI green on all 9 jobs.

Full suite on the head: 1659 passed, 1 skipped, 1 xfailed.

The gate that actually mattered — reproduced by me, not read off the evidence file

k64's G-D4 stops the whole eval with PARTIAL-PATH-NOT-EXERCISED unless at least one timeout carries partial_available: true. I ran this lane's cross-repo round-trip against a fresh clone of merged amplifier-foundation (the real consumer half, #353), with this branch's producer on PYTHONPATH and neither repo mutated:

2 passed

and the lane's own captured contrast holds on the same wiring:

parent ab47608 this branch
session.partial registered False True
partial_available false true
partial_chars_total 0 42 (2 segments, spawn-accumulator)

So the producer/consumer pair is closed end to end. model_performance-k64 is no longer prerequisite-blocked.

Normal completions byte-identical — sha256 match, diff empty. Shown, not asserted.

The correction I want on the record, because it is the honest half

The goal told this lane to expect two named pre-existing failures (37n's 71 passed / 2 failed at f16375fc) and to reproduce them on the parent. They do not exist at ab47608. I checked independently:

tests/test_session_spawner.py + test_session_runner.py + test_session_spawner_issue_233.py
  -> 73 passed, 0 failed   (parent)

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 #353.

FIX with the decisive gate reproduced → merging. Squash + --admin per the base-branch policy.

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.

2 participants