fix(run): the FinalGuard nudge round can never replace a non-blank answer
executus CI / test (pull_request) Successful in 49s
executus CI / test (pull_request) Successful in 49s
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]>
This commit is contained in:
+24
-1
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.stevedudenhoeffer.com/steve/majordomo/agent"
|
||||
@@ -500,6 +501,15 @@ func (e *Executor) Run(ctx context.Context, ra RunnableAgent, inv tool.Invocatio
|
||||
// small fixed step cap so a misread nudge can't start a new work spree.
|
||||
// The nudge round's failure is non-fatal — the original successful result
|
||||
// stands (any attachments it queued before failing still deliver).
|
||||
//
|
||||
// The nudge round's job is to QUEUE deliveries (tool calls); its TEXT is
|
||||
// meta-commentary ("answer above stands", "sent!") that must never replace
|
||||
// the answer the user asked for — hosts deliver Output text + separately
|
||||
// drained attachments, so replacing Output with the nudge round's closer
|
||||
// LOSES the real answer (live regression: run d5ec39f4, a research run's
|
||||
// page-cache false positive whose nudge reply "my answer above stands
|
||||
// as-is" was delivered INSTEAD of the answer). Keep the original Output
|
||||
// unless it was blank; log what was discarded so hosts can audit.
|
||||
if runErr == nil && runRes != nil && e.cfg.Ports.FinalGuard != nil {
|
||||
state := FinalState{
|
||||
Output: runRes.Output,
|
||||
@@ -529,7 +539,20 @@ func (e *Executor) Run(ctx context.Context, ra RunnableAgent, inv tool.Invocatio
|
||||
slog.Warn("run: final-nudge round failed; keeping original result",
|
||||
"run_id", inv.RunID, "error", nudgeErr)
|
||||
} else if nudgeRes != nil {
|
||||
runRes.Output = nudgeRes.Output
|
||||
if strings.TrimSpace(runRes.Output) == "" {
|
||||
// Degenerate original (blank stop turn): the nudge round's
|
||||
// text is the only answer there is.
|
||||
runRes.Output = nudgeRes.Output
|
||||
} else if rec != nil {
|
||||
// Discarded meta-text stays observable: a nudge round that
|
||||
// DECLINED to queue while the original claims a send shows
|
||||
// up here, not in the user's reply.
|
||||
rec.LogEvent("final_nudge_result", map[string]any{
|
||||
"discarded_output": nudgeRes.Output,
|
||||
})
|
||||
}
|
||||
// The transcript keeps the nudge round either way (audit,
|
||||
// PostRun, checkpoint continuity).
|
||||
runRes.Messages = nudgeRes.Messages
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user