From df24cae507e8de704130e4f080b2e2420fdce8f3 Mon Sep 17 00:00:00 2001 From: dtoro Date: Thu, 16 Jul 2026 00:09:31 +0200 Subject: [PATCH] =?UTF-8?q?fix(agent):=20fully=20silent=20assent=20?= =?UTF-8?q?=E2=80=94=20no=20system=20notes,=20no=20chat=5Fassent=20events?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The len(pending)>0 path still injected a brief note saying 'execution(s) are now running' — the model saw this, thought work was being done for it, and no-op'd (finish_reason=stop, content_len=0). Same confusion as the len(pending)==0 case, just from the other branch. Fix: both assent paths are now fully silent. No system note at all. The model sees 'go ahead' in the replayed history and responds naturally. Also removed chat_assent tool_use/tool_result emit events. These were persisted in the transcript and confused the model on replay — it saw its own 'tool calls' (chat_assent) and thought it had already acted. VERSION 0.7.4 → 0.7.5 --- VERSION | 2 +- cmd/nomos/agent.go | 48 ++++++++++++++++++---------------------------- 2 files changed, 20 insertions(+), 30 deletions(-) diff --git a/VERSION b/VERSION index 0a1ffad..8bd6ba8 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.7.4 +0.7.5 diff --git a/cmd/nomos/agent.go b/cmd/nomos/agent.go index 7eeccd3..cc7b8b2 100644 --- a/cmd/nomos/agent.go +++ b/cmd/nomos/agent.go @@ -296,17 +296,6 @@ func (a *agent) chatWith(ctx context.Context, sessionID, message, systemInject s if ok { granted = append(granted, p.execID) slog.Info("nomos: chat-assent granted", "execution", p.execID, "status", status, "session", sessionID) - // Mark as continued so the continuation worker doesn't - // pick up this execution and call resumeSession while the - // chat handler is still processing "go ahead." Without this, - // two LLM calls run concurrently for the same session — - // the chat handler's chat() and the worker's resumeSession() - // — causing empty responses and race conditions. - if execUUID, perr := uuid.Parse(p.execID); perr == nil { - a.store.markContinued(ctx, execUUID) - } - emit(agentEvent{Type: "tool_use", Data: map[string]any{"name": "chat_assent", "args": map[string]any{"execution_id": p.execID}, "id": "assent-" + p.execID}, SessionID: sessionID}) - emit(agentEvent{Type: "tool_result", Data: map[string]any{"name": "chat_assent", "result": fmt.Sprintf("Approved via chat assent (%q). Status: %s.", message, status), "id": "assent-" + p.execID}, SessionID: sessionID}) // An explicit typed confirmation for a destructive action // opens a short, target-scoped window so the rest of a @@ -324,15 +313,22 @@ func (a *agent) chatWith(ctx context.Context, sessionID, message, systemInject s } if len(granted) > 0 { a.openAssentWindow(ctx, sessionID) - // Brief note: only tells the model WHICH specific executions - // were approved (so it doesn't re-request them). Does NOT say - // "continue the plan" — the model already sees "go ahead" in - // the replayed history and knows to continue. The old verbose - // note ("they are now running... you MUST continue...") made - // the model think work was being done for it, causing empty - // responses (finish_reason=stop, content_len=0). - note := fmt.Sprintf("[System: execution(s) %s were approved by the operator and are now running. Do not re-request those — check get_execution_status if you need the outcome.]", strings.Join(granted, ", ")) - messages = append(messages, openai.SystemMessage(note)) + // Mark approved executions as continued so the continuation + // worker doesn't call resumeSession while the chat handler is + // still processing "go ahead" — two concurrent LLM calls for the + // same session cause empty responses and race conditions. + for _, execID := range granted { + if execUUID, perr := uuid.Parse(execID); perr == nil { + a.store.markContinued(ctx, execUUID) + } + } + // No system note. The model already sees "go ahead" in the + // replayed history (the user message was saved to the DB before + // chat() was called). The old note said "they are now running" + // which made the model think work was being done for it — + // causing empty responses (finish_reason=stop, content_len=0). + // The approved executions are dispatched; the model will + // continue with the remaining plan steps naturally. } if len(blocked) > 0 { note := fmt.Sprintf("[System: execution(s) %s are classified DESTRUCTIVE and were NOT approved by loose assent — you must ask the operator for an explicit typed confirmation before they can run. Once they do confirm, further destructive steps on that SAME target (e.g. finishing a stop-then-destroy sequence) will auto-run for 15 minutes without asking again — but a different target always needs its own confirmation.]", strings.Join(blocked, ", ")) @@ -341,15 +337,9 @@ func (a *agent) chatWith(ctx context.Context, sessionID, message, systemInject s } else if assent && len(pending) == 0 { // The operator said "proceed"/"go ahead"/"yes" but there are no // pending approvals — the agent proposed a plan (via propose_plan) - // and asked "shall I?" Open the assent window silently. Do NOT - // inject a system note: the model already sees "go ahead" in the - // replayed history (the user message was saved to the DB before - // chat() was called, and getRecentMessages replays it). The old - // verbose system note ("The operator approved your proposed plan. - // Execute it now — call update_plan_step then run...") was redundant - // with the user's "go ahead" and caused the model to no-op - // (finish_reason=stop, content_len=0) — the model saw "approved" + - // "running" and concluded there was nothing to do. + // and asked "shall I?" Open the assent window silently. No system + // note: the model sees "go ahead" in the replayed history and + // responds naturally. a.openAssentWindow(ctx, sessionID) }