feat(run): Ports.FinalGuard — nudge an announce-then-stop run to deliver its stranded artifact #23

Merged
steve merged 2 commits from feat/final-guard-nudge into main 2026-07-15 16:23:42 +00:00
Owner

Why

A run can render an artifact into host storage, end on a text-only stop turn claiming "Done — sent you the file", and never call the host's delivery tool — the artifact strands in run storage while the user is told it shipped. Observed live twice in mort:

  • gifsmith run 29170c93 (2026-07-12) — a finished GIF stranded; mort patched the palette (child-run) path with a tool-result note, with "executus-level nudge" recorded as the open follow-up.
  • general run e9e7bf40 (2026-07-14) — the top-level run produced an 11 MB MP4 via code_exec files_out, then stopped with "Done. Pure-Python music video …" and zero attachments. No guard exists on this path: majordomo's loop terminates immediately on a no-tool-calls turn, and the executor goes straight to audit + delivery.

What

New optional Ports.FinalGuard, consulted once when a single-loop run is about to finalize successfully:

  • Returns "" → run finishes as-is (the common path; zero extra model calls).
  • Returns a nudge → the nudge is appended to the SAME conversation as the next user turn and the agent runs ONE bounded extra round: same model/toolbox/observers (audit, steps, critic all see it), but capped at finalNudgeMaxSteps = 6 steps so a misread nudge can't start a new work spree. The nudge round's own stop is final (no re-guard).
  • Guard panics and nudge-round failures are isolated — the original successful result always survives (any attachments queued before a nudge-round failure still deliver).
  • The nudge is recorded as a final_nudge audit event and appended to the durable checkpoint transcript, so a shutdown-resume replays it.
  • Multi-phase runs are not guarded (their output contract is the pipeline's, not a delivery surface's); failed runs are never nudged.

FinalState gives the host what it needs to decide cheaply: the final Output, the run's ToolNames (skip runs with no delivery tool), and StagedFileIDsstageInputFiles now also returns the ids it staged, since staged inputs live under run scope but are the user's attachments, not run-produced artifacts.

Host side

mort wires this as executushost.UndeliveredArtifactGuard (zero pending attachments + run-scope files beyond the staged inputs + send_attachments available → nudge). See the companion mort PR.

Tests

run/final_guard_test.go: nudge runs one extra round and replaces output/audit roll-up; empty nudge adds zero model calls; nudge-round failure keeps the original result; failed runs aren't consulted; guard panic is isolated; ToolNames + StagedFileIDs reach the guard; phase runs skip the guard. go build ./... && go vet ./... && go test ./... green.

🤖 Generated with Claude Code

## Why A run can render an artifact into host storage, end on a text-only stop turn claiming "Done — sent you the file", and never call the host's delivery tool — the artifact strands in run storage while the user is told it shipped. Observed live twice in mort: - gifsmith run `29170c93` (2026-07-12) — a finished GIF stranded; mort patched the **palette (child-run) path** with a tool-result note, with "executus-level nudge" recorded as the open follow-up. - general run `e9e7bf40` (2026-07-14) — the **top-level** run produced an 11 MB MP4 via `code_exec` `files_out`, then stopped with "Done. Pure-Python music video …" and zero attachments. No guard exists on this path: majordomo's loop terminates immediately on a no-tool-calls turn, and the executor goes straight to audit + delivery. ## What New optional `Ports.FinalGuard`, consulted **once** when a single-loop run is about to finalize **successfully**: - Returns `""` → run finishes as-is (the common path; zero extra model calls). - Returns a nudge → the nudge is appended to the SAME conversation as the next user turn and the agent runs ONE bounded extra round: same model/toolbox/observers (audit, steps, critic all see it), but capped at `finalNudgeMaxSteps = 6` steps so a misread nudge can't start a new work spree. The nudge round's own stop is final (no re-guard). - Guard panics and nudge-round failures are isolated — the original successful result always survives (any attachments queued before a nudge-round failure still deliver). - The nudge is recorded as a `final_nudge` audit event and appended to the durable checkpoint transcript, so a shutdown-resume replays it. - Multi-phase runs are not guarded (their output contract is the pipeline's, not a delivery surface's); failed runs are never nudged. `FinalState` gives the host what it needs to decide cheaply: the final `Output`, the run's `ToolNames` (skip runs with no delivery tool), and `StagedFileIDs` — `stageInputFiles` now also returns the ids it staged, since staged inputs live under run scope but are the user's attachments, not run-produced artifacts. ## Host side mort wires this as `executushost.UndeliveredArtifactGuard` (zero pending attachments + run-scope files beyond the staged inputs + `send_attachments` available → nudge). See the companion mort PR. ## Tests `run/final_guard_test.go`: nudge runs one extra round and replaces output/audit roll-up; empty nudge adds zero model calls; nudge-round failure keeps the original result; failed runs aren't consulted; guard panic is isolated; ToolNames + StagedFileIDs reach the guard; phase runs skip the guard. `go build ./... && go vet ./... && go test ./...` green. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
steve added 1 commit 2026-07-15 03:14:33 +00:00
feat(run): Ports.FinalGuard — host seam that nudges an announce-then-stop run to deliver its artifact
Gadfly review (reusable) / review (pull_request) Successful in 29s
Adversarial Review (Gadfly) / review (pull_request) Successful in 28s
executus CI / test (pull_request) Successful in 3m1s
007b7539c2
A run can render an artifact into host storage, end on a text-only stop
turn claiming "Done — sent you the file", and never call the host's
delivery tool — the artifact strands while the user is told it shipped
(mort general run e9e7bf40 2026-07-14, gifsmith run 29170c93 2026-07-12).
The loop has no seam for the host to intervene at the stop turn.

Add Ports.FinalGuard: consulted once when a single-loop run is about to
finalize successfully. A non-empty nudge is appended to the SAME
conversation as the next user turn and the agent runs ONE bounded extra
round (same toolbox/observers, capped at finalNudgeMaxSteps=6) — enough
to deliver the stranded file or correct the final text, not enough to
start a new work spree. Guard panics and nudge-round failures are
isolated: the original successful result always survives. The nudge is
recorded as a final_nudge audit event and appended to the durable
checkpoint transcript.

stageInputFiles now also returns the staged file ids, surfaced to the
guard via FinalState.StagedFileIDs — staged inputs live under run scope
but are the user's attachments, not run-produced artifacts, so an
undelivered-artifact check must exclude them. FinalState.ToolNames lets
a host skip runs that have no delivery tool at all.

Co-Authored-By: Claude Fable 5 <[email protected]>
steve added 1 commit 2026-07-15 14:15:45 +00:00
Live regression (mort run d5ec39f4, 2026-07-15): a research run's page-cache
file false-positived the guard, and the nudge round's meta closer ("my
answer above stands as-is") REPLACED the real answer — hosts deliver
Output text plus separately-drained attachments, so the user received the
closer and never saw the answer.

New merge policy: the nudge round exists to make tool calls (queue the
delivery); its final text is discarded and recorded as a final_nudge_result
audit event, so a round that declined to queue stays observable without
reaching the user. The only exception is a blank original stop turn, where
the nudge round's text is the only answer there is. Messages/Usage still
merge (audit, PostRun, checkpoint continuity).

Additive on top of 007b753 (mort main pins that commit as a pseudo-version;
this branch must never be rewritten).

Co-Authored-By: Claude Fable 5 <[email protected]>
steve merged commit 42206f0af0 into main 2026-07-15 16:23:42 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: steve/executus#23