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_execfiles_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.
## 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)
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]>
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 main2026-07-15 16:23:42 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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:
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.e9e7bf40(2026-07-14) — the top-level run produced an 11 MB MP4 viacode_execfiles_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:""→ run finishes as-is (the common path; zero extra model calls).finalNudgeMaxSteps = 6steps so a misread nudge can't start a new work spree. The nudge round's own stop is final (no re-guard).final_nudgeaudit event and appended to the durable checkpoint transcript, so a shutdown-resume replays it.FinalStategives the host what it needs to decide cheaply: the finalOutput, the run'sToolNames(skip runs with no delivery tool), andStagedFileIDs—stageInputFilesnow 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_attachmentsavailable → 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
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 of007b753(mort main pins that commit as a pseudo-version; this branch must never be rewritten). Co-Authored-By: Claude Fable 5 <[email protected]>