diff --git a/internal/agent/execute_one.go b/internal/agent/execute_one.go index 0ca218a779..88b83d3378 100644 --- a/internal/agent/execute_one.go +++ b/internal/agent/execute_one.go @@ -634,7 +634,7 @@ func (a *Agent) prepareToolExecution(ctx context.Context, plan *toolCallPlan) (t cctx = WithSubagentDepth(cctx, a.subagentDepth) if a.evidence != nil { cctx = evidence.WithLedger(cctx, a.evidence) - cctx = evidence.WithSessionMessages(cctx, a.session.Snapshot()) + cctx = evidence.WithSessionMessages(cctx, a.session.Snapshot) if a.deliveryProfile { cctx = evidence.WithDeliveryProfile(cctx) } diff --git a/internal/evidence/evidence.go b/internal/evidence/evidence.go index 0526b4bf63..fd38d40555 100644 --- a/internal/evidence/evidence.go +++ b/internal/evidence/evidence.go @@ -1511,18 +1511,24 @@ func DeliveryProfileFromContext(ctx context.Context) bool { return enabled } -// WithSessionMessages attaches the full conversation history so verifyStepEvidence -// can fall back to scanning the transcript when the per-turn ledger misses a -// command (cross-turn references, non-bash tool calls, truncated command strings). -func WithSessionMessages(ctx context.Context, msgs []provider.Message) context.Context { - return context.WithValue(ctx, sessionMessagesKey{}, msgs) +// WithSessionMessages attaches a lazy transcript accessor so verifyStepEvidence +// can fall back to scanning the conversation when the per-turn ledger misses a +// command (cross-turn references, non-bash tool calls, truncated command +// strings). The context carries the capability, not the data: snapshot is +// called only when a consumer (complete_step) actually needs the history, so +// ordinary tool calls never pay for a full transcript copy. +func WithSessionMessages(ctx context.Context, snapshot func() []provider.Message) context.Context { + return context.WithValue(ctx, sessionMessagesKey{}, snapshot) } -// SessionMessagesFromContext retrieves the conversation history attached by -// WithSessionMessages. +// SessionMessagesFromContext resolves the transcript accessor attached by +// WithSessionMessages, taking the snapshot at call time. func SessionMessagesFromContext(ctx context.Context) ([]provider.Message, bool) { - msgs, ok := ctx.Value(sessionMessagesKey{}).([]provider.Message) - return msgs, ok + snapshot, ok := ctx.Value(sessionMessagesKey{}).(func() []provider.Message) + if !ok || snapshot == nil { + return nil, false + } + return snapshot(), true } // WithTodoState attaches the host's canonical task list to a tool call. The diff --git a/internal/tool/builtin/completestep_test.go b/internal/tool/builtin/completestep_test.go index a003fae386..1b2861e30c 100644 --- a/internal/tool/builtin/completestep_test.go +++ b/internal/tool/builtin/completestep_test.go @@ -637,7 +637,7 @@ func TestCompleteStepSessionFallbackUsesNormalizedMatching(t *testing.T) { {Role: provider.RoleTool, ToolCallID: "c1", Name: "bash", Content: "ok\nPASS"}, } ctx := evidence.WithLedger(context.Background(), evidence.NewLedger()) - ctx = evidence.WithSessionMessages(ctx, msgs) + ctx = evidence.WithSessionMessages(ctx, func() []provider.Message { return msgs }) if _, err := (completeStep{}).Execute(ctx, json.RawMessage(`{ "step":"x","result":"y", @@ -654,7 +654,7 @@ func TestCompleteStepSessionFallbackSkipsFailedCalls(t *testing.T) { {Role: provider.RoleTool, ToolCallID: "c1", Name: "bash", Content: "error: command exited: exit status 1\nFAIL"}, } ctx := evidence.WithLedger(context.Background(), evidence.NewLedger()) - ctx = evidence.WithSessionMessages(ctx, msgs) + ctx = evidence.WithSessionMessages(ctx, func() []provider.Message { return msgs }) if _, err := (completeStep{}).Execute(ctx, json.RawMessage(`{ "step":"x","result":"y", @@ -695,7 +695,7 @@ func TestCompleteStepSessionFallbackResolvesDiffPaths(t *testing.T) { {Role: provider.RoleTool, ToolCallID: "w1", Name: "write_file", Content: "wrote 10 lines"}, } ctx := evidence.WithLedger(context.Background(), evidence.NewLedger()) - ctx = evidence.WithSessionMessages(ctx, msgs) + ctx = evidence.WithSessionMessages(ctx, func() []provider.Message { return msgs }) if _, err := (completeStep{}).Execute(ctx, json.RawMessage(`{ "step":"x","result":"y", @@ -712,7 +712,7 @@ func TestCompleteStepSessionFallbackSkipsFailedWrite(t *testing.T) { {Role: provider.RoleTool, ToolCallID: "w1", Name: "write_file", Content: "error: permission denied"}, } ctx := evidence.WithLedger(context.Background(), evidence.NewLedger()) - ctx = evidence.WithSessionMessages(ctx, msgs) + ctx = evidence.WithSessionMessages(ctx, func() []provider.Message { return msgs }) if _, err := (completeStep{}).Execute(ctx, json.RawMessage(`{ "step":"x","result":"y",