Skip to content

feat: clear reviewer context (fresh eyes) on each coder handoff - #2291

Open
lsm wants to merge 19 commits into
devfrom
space/clear-reviewer-context-fresh-eyes-on-each-coder-handoff-2
Open

feat: clear reviewer context (fresh eyes) on each coder handoff#2291
lsm wants to merge 19 commits into
devfrom
space/clear-reviewer-context-fresh-eyes-on-each-coder-handoff-2

Conversation

@lsm

@lsm lsm commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Adds a per-slot resetContextPerTurn flag (data-driven, no role-name lookup) that wipes an agent slot's SDK model context at the start of each node→node handoff, so each turn starts fresh while NeoKai keeps one continuous UI thread (sdk_messages are untouched). Enabled on the built-in Coding Workflow Reviewer; coder stays persistent.

How the clear works

AgentSession.clearConversationContext() issues the SDK's /clear command in-stream as an internal control message ahead of the triggering handoff. The SDK's pull-based streaming-input generator serializes /clear before the handoff (it won't pull the next message until the /clear turn completes), so the handoff always runs in a fresh conversation.

This replaces an earlier stop→wipe→restart approach and drops its most fragile machinery — there is no query stop, so no generation-bump idle-race to suppress, no provider-env restore, no message-queue/runner clears, and no manual restart. Verified empirically against the bundled CLI (2.1.179) that /clear in streaming-input mode clears context for the next message and rotates the SDK session id (online regression test in tests/online/lifecycle/).

The SDK rotates the SDK-internal sdkSessionId itself on /clear; handleSystemInit now captures the new id on every init where it changes (was: only when unset), so daemon-restart resume points at the live conversation, not a stale pre-clear one. The prior id is appended to a capped metadata.pastSdkSessionIds trace for audit. NeoKai keys UI threading on its own session id (not sdkSessionId), so the rotation is transparent to the UI.

Gating

Cleared only for task inputs (node→node handoffs). Never clears for: human input, system recovery nags, the first turn (no prior sdkSessionId), a busy session, or flag-less slots. SDK-only — /clear is a Claude-Code command, so the flag is a documented no-op on ACP (codex) slots until ACP grows an equivalent.

Cost is preserved across the rotation: the prior turn's lastSdkCost is rolled into costBaseline before /clear (the result handler's restart-detection is unreliable here — the post-/clear cost can be ≥ the prior).

Tests

AgentSession clear primitive (enqueues /clear, rolls cost, records audit trace, does not stop/restart), handleSystemInit session-id rotation, TaskAgentManager gating (handoff clears; human/recovery/first-turn/busy/no-flag do not), editor + serialization round-trip for the toggle, and an online test proving /clear clears context in streaming-input mode. bun run check clean; daemon shards pass.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 956fa7b315

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts Outdated
Comment thread packages/web/src/components/space/visual-editor/serialization.ts
Comment thread packages/daemon/src/lib/agent/agent-session.ts Outdated
Comment thread packages/daemon/src/lib/space/workflows/built-in-workflows.ts
Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts Outdated
Comment thread packages/shared/src/types/space.ts
Comment thread packages/web/src/components/space/WorkflowNodeCard.tsx

@lsm lsm left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Review by glm-5.1 (GLM)

Model: glm-5.1 | Client: NeoKai | Provider: GLM

Recommendation: REQUEST_CHANGES — 2 P1, 5 P2.

The core design is sound and well-documented. I independently verified the central decision: the SDK exposes no in-place clear (clear is only an EXIT_REASONS value in sdk.d.ts:621, plus compact-boundary hooks), so rotating the SDK-internal sdkSessionId to start a non-resuming conversation is the correct — and only viable — mechanism short of piping a /clear string (which the spec forbids). I also confirmed: resume is correctly skipped when sdkSessionId is undefined (query-options-builder.ts:729-735); ensureQueryStarted() is a safe no-op after the internal startStreamingQuery() (no double-start, query-lifecycle-manager.ts:589-611); the new session_id is captured without truncating sdk_messages (sdk-message-handler.ts:767); messageQueue.clear() only drops in-memory unconsumed input and is safe behind the !isBusy guard; and nothing keys UI threading on sdkSessionId (the sdk_messages schema has no sdk_session_id column). New unit/editor tests pass, bun run check is clean, and the 5-space-runtime-a/b + 1-core/agent shards pass.

However, I independently confirmed two P1 integration defects and several P2 propagation gaps. Several of these corroborate the unresolved chatgpt-codex-connector threads — I verified them against primary sources rather than trusting the prior review.

P1

1. Idle-state race on session reuse breaks every 2nd+ reviewer cycle. clearConversationContext() calls stateManager.setIdle() (agent-session.ts:800). setState publishes session.updated unconditionally — there is no same-state guard (processing-state-manager.ts:344-363) — so even an already-idle reused session re-fires the event, and publish awaits subscribers inline (internal-event-bus.ts:9, await Promise.all at :222). On cycle 2+, createSubSession's reuse branch registers a fresh completion callback (task-agent-manager.ts:1046-1051) before the kickoff is injected (:856). That callback fires on any idle with sdkCount > 0 (:2225-2231) and is one-shot (:2233-2243). So the clear's idle publication fires the callback during the clear — prematurely marking the NodeExecution idle (handleSubSessionComplete:2318) before the handoff is even enqueued, then self-unsubscribing. The agent's genuine completion of the new turn later has no callback → completion signaling is lost. This deterministically undermines the feature on every cycle after the first, and the existing unit tests don't catch it (they mock setIdle and don't register a completion callback). Fix: do not emit a client-visible idle during the clear — suppress/defer the publication, move the clear ahead of callback registration, or make the completion detector distinguish a clear-induced idle from a real end-of-turn (e.g. gate on a turn marker / message-count increase rather than raw idle).

2. Built-in workflow change doesn't reach existing spaces. computeWorkflowHash fingerprints only customPrompt.value per agent (template-hash.ts:187-189), not resetContextPerTurn. Adding the flag to the built-in Reviewer yields an identical hash, so the restamp drift gate (built-in-workflows.ts:2206-2207) skips already-installed spaces; and even if restamp ran, mergeNodeStructuralFieldsFromTemplate (:1350-1359) doesn't copy the field. Result: existing spaces with the Coding Workflow never get fresh-eyes on their Reviewer — only new spaces do. The headline use case silently fails for installed spaces. Fix: add resetContextPerTurn to the hash input and to the restamp merge field set.

P2

3. ACP-provider slots silently no-op. ACP sessions set acpSessionId, never sdkSessionId (acp-query-runner.ts), so the !!sdkSessionId gate (task-agent-manager.ts:3291) is always false → the clear never fires; and clearConversationContext only wipes sdk fields, so even if it ran it would not clear ACP context (the trailing startStreamingQuery would resume the same ACP session). Latent (the built-in Reviewer is SDK-based) but the flag is documented as generic/data-driven. At minimum document the SDK-only limitation; ideally extend the gate/primitive to ACP.

4. Export/import drops the flag. export-format.ts:370-384, ExportedWorkflowNodeAgent (space.ts:2458-2513), and the Zod schema enumerate sibling fields but omit resetContextPerTurn → cross-Space export/import loses it.

5. Template picker drops the flag. workflowToTemplate/buildTemplateNodes (workflow-templates.ts) copy field-by-field and omit it (this also drops timeoutMs/toolGuards for daemon templates) → template-created workflows silently lose fresh-eyes.

6. Editor single↔multi conversion drops the flag. NodeConfigPanel.tsx primary-slot construction (≈373-379) and removeAgent (≈273-282) don't carry resetContextPerTurn. The reverse direction is a fresh inconsistency: disabledSkillIds IS promoted but resetContextPerTurn is not.

7. Unguarded resolveNodeAgents on the delivery path. slotResetsContextForSession (task-agent-manager.ts:2597) calls resolveNodeAgents unguarded, and is itself called unguarded in the && at :3292 inside injectMessageIntoSession. resolveNodeAgents throws on a node with empty agents + no legacy agentId (space-utils.ts:49). A mid-flight-edited or corrupted node would drop the handoff instead of falling back to "deliver without clear" — inconsistent with the codebase's own guard pattern (node-agent-tools.ts:513, channel-router.ts:481) and with this PR's own try/catch on the clear invocation. Wrap the lookup (or the whole shouldClearContext evaluation) in try/catch → false.

Not a finding (matches spec intent)

Another reviewer flagged external-event digests being classified 'task' and clearing context. The task spec explicitly accepts this: "GitHub events would inherit the same clear behavior automatically if reviewers ever subscribe — that is a separate subscription decision, out of scope here." So that behavior is intended, not a defect.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2bc0b2625c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts
Comment thread packages/daemon/src/lib/space/workflows/template-hash.ts Outdated
Comment thread packages/daemon/src/lib/agent/agent-session.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 66f52c1782

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/daemon/src/lib/agent/agent-session.ts Outdated
Comment thread packages/daemon/src/lib/agent/agent-session.ts Outdated
Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6904eec801

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts Outdated
Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts Outdated
Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 12ddfebf3d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/daemon/src/lib/agent/agent-session.ts Outdated
Comment thread packages/web/src/components/space/visual-editor/NodeConfigPanel.tsx
Comment thread packages/web/src/components/space/visual-editor/NodeConfigPanel.tsx
Comment thread packages/daemon/src/lib/agent/agent-session.ts Outdated

@lsm lsm left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Review by glm-5.1 (GLM)

Model: glm-5.1 | Client: NeoKai | Provider: GLM

Recommendation: REQUEST_CHANGES — 1 P2, 1 P3 (down from 2 P1 + 5 P2 last round).

Round 2 is strong. I independently verified every prior finding addressed and all three pushbacks justified.

Verified fixed

  • P1 #2 (propagation): template-hash now fingerprints resetContextPerTurn (only when non-empty, so unrelated built-ins don't mass-restamp) and mergeNodeStructuralFieldsFromTemplate copies it → existing spaces restamp and receive the flag. Plus a boolean validation guard. ✅
  • P2 #3 (ACP): hasPriorContext now includes acpSessionId; clearConversationContext clears acpSessionId + acpInstructionsSent for the ACP provider; the ACP runner finally is also suppressed. ✅
  • P2 #4/#5/#6 (export/template/editor): the flag flows through export-format + Zod + import type, workflowToTemplate/buildTemplateNodes (both branches), and NodeConfigPanel single↔multi (both directions). ✅
  • P2 #7 (resolveNodeAgents guard): wrapped in try/catch → false. ✅
  • Input-kind scoping: external-event digests (agent.message.inject) and hook-failure notices (notifySourceSession) now pass 'system' → no longer clear. ✅
  • Inject lock: verified no deadlock/re-entrancy (clearConversationContext and handleSubSessionComplete don't re-inject); FIFO serialization is the intended fix. ✅
  • Cost preservation: byte-identical to the normal reset() path; the consumer at sdk-message-handler.ts:831-858 confirms no loss or double-count. ✅

Pushbacks (independently assessed) — all justified

(3c) shared-session slot resolution is clearly correct: getByAgentSessionId returns the in_progress/active execution (SQL-ordered), and the runtime routes handoffs to the active node. (3a) busy-handoff and (3b) post-restart FIFO are genuine narrow latent gaps but correctly out of scope here — 3a's proposed defer wouldn't even work (deferred replay bypasses the inject layer, verified at query-mode-handler.ts:82/119); 3b is a pre-existing shape made deterministic by the lock.

Remaining — the P1 idle-race fix is almost fully closed

P2 — The load-bearing idle-suppression is not tested end-to-end. I confirmed the suppression is load-bearing, not defensive: query() is called with prompt: createMessageGeneratorWrapper() (query-runner.ts:757-760) — an async generator that yields messages across turns — so the SDK subprocess stays alive between turns and queryPromise is non-null at clear time. Therefore lifecycleManager.stop() during the clear really does run the runQuery finally (query-runner.ts:1438), and the && !isClearingConversationContext() guard is what actually prevents the idle publish. But the "P1-1 regression" test (agent-session-clear-context.test.ts:146) mocks lifecycleManager.stop() — it only verifies the flag is armed around the mocked call, never that the real finally reads the flag and skips setIdle. query-runner.test.ts just adds the interface stub (isClearingConversationContext: () => false) without exercising the branch. A future refactor removing the guard at query-runner.ts:1438 / acp-query-runner.ts:787 would pass every test and silently re-open the race; the test's own comment describes the finally mechanism but never runs it. Fix: add a QueryRunner test that drives runQuery to its finally with isClearingConversationContext() === true and asserts stateManager.setIdle is NOT called (and IS called when false) — exercise the actual load-bearing branch, not just the flag lifecycle.

P3 — Boolean-flag suppression has a narrow timing window. lifecycleManager.stop() awaits queryPromise settlement raced against a ~5s timer (query-lifecycle-manager.ts:292). If the SDK subprocess takes >5s to exit after the clear's abort, stop() returns via the timer, clearConversationContext resets the flag, and the late-settling finally sees isClearingConversationContext() === false and publishes idle → the race re-opens. Narrow (an idle subprocess should exit well under 5s) but not impossible. restart() avoids this with a query-generation/stale-query guard; the same pattern here would be robust to timing. Low priority.

Tests pass, bun run check is clean, space-runtime shards pass.

Comment thread packages/daemon/tests/unit/1-core/agent/agent-session-clear-context.test.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 66a7010371

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/daemon/src/lib/agent/agent-session.ts Outdated

@lsm lsm left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Review by glm-5.1 (GLM)

Model: glm-5.1 | Client: NeoKai | Provider: GLM

Recommendation: APPROVE — posted as COMMENT only because the PR author is the authenticated user (GitHub rejects APPROVE/REQUEST_CHANGES on your own PR). The verdict is explicitly APPROVE, zero P0–P3 findings.

Round-3 verification complete. All previously raised findings (round-1 P1/P2, round-2 P2/P3) are resolved, and the three propagation follow-ups are correct.

Idle suppression: generation bump replaces the flag (round-2 P2 + P3 — resolved)

The boolean _isClearingConversationContext flag is fully removed (zero references remain across packages/). clearConversationContext now bumps the query generation before stop() (agent-session.ts:831). The old query's runQuery finally checks isStaleQuery first (query-runner.ts:1376, acp-query-runner.ts:756); when stale it skips the entire cleanup block — including stateManager.setIdle() — so the clear no longer publishes a completion-eligible idle that could prematurely fire the node-agent completion callback before the cleared handoff is enqueued.

This is strictly better than the flag:

  • Timing-robust (round-2 P3): a late-settling subprocess finally still sees the bumped generation → stale → idle suppressed. A flag would have been reset before a late finally ran.
  • Rides on an already-tested guard (round-2 P2): query-runner.test.ts:1217 forces isStaleQuery and asserts the finally skips cleanup. The new unit test now asserts the precondition (generation bumped before stop()), not a mocked suppression path.

The cleanup-ownership invariant holds: stop() still closes the subprocess via its queryObject === snapshot check (the stale finally skipped close()), and the fresh query from startStreamingQuery() — which bumps the generation again — owns the next finally. The stop()startStreamingQuery() sequence is unchanged from round 2.

Follow-ups

  1. NodeConfigPanel shorthand→explicit promotion now carries resetContextPerTurn (NodeConfigPanel.tsx:473) — toggling skills on a single-slot node no longer drops the flag.
  2. ACP usage estimate clearedacpContextUsageEstimate added to SessionMetadata (types.ts:607) and reset alongside acpInstructionsSent (agent-session.ts:859), so a fresh ACP turn doesn't inherit the prior token total.
  3. buildTemplateCanvasSignature includes resetContextPerTurn on both single- and multi-agent paths (VisualWorkflowEditor.tsx:102,110) — toggling the flag marks the canvas dirty.

Verification

  • bun run check clean (lint + typecheck + knip + session/db/test-quality guards).
  • Daemon: agent-session-clear-context (7), query-runner (150), acp-query-runner (28) pass.
  • Web: serialization, NodeConfigPanel, VisualWorkflowEditor, workflow-templates (233) pass.
  • 21/21 review threads resolved; PR OPEN, MERGEABLE, targets dev.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3b0d59d7f5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/daemon/src/lib/agent/agent-session.ts Outdated
Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts
Comment thread packages/daemon/src/lib/space/managers/space-workflow-manager.ts
Comment thread packages/daemon/src/lib/agent/agent-session.ts Outdated
Comment thread packages/daemon/src/lib/space/managers/space-workflow-manager.ts Outdated
Comment thread packages/daemon/src/lib/agent/agent-session.ts Outdated

@lsm lsm left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Review by glm-5.1 (GLM)

Model: glm-5.1 | Client: NeoKai | Provider: GLM

Recommendation: REQUEST_CHANGES (posted as COMMENT because the PR author is the authenticated user — GitHub rejects REQUEST_CHANGES on your own PR; the verdict is explicitly REQUEST_CHANGES). Two actionable findings (P2) + one optional defensive (P3).

Round-4 env-leak fix — verified correct ✅

The fix at agent-session.ts:833-842 is right: it restores this.originalEnvVars after stop(), mirroring the finally's logic (query-runner.ts:1420-1427) and running before startStreamingQuery() snapshots env. The leak was a genuine round-3 regression — clearConversationContext's "bump generation before stop" makes the old finally stale and skip restoreEnvVars, whereas restart() bumps after stop (old finally runs clean). The new test locks it in. agent-session-clear-context (8), query-runner+acp-query-runner (178) pass; bun run check clean.

I also did a full stale-finally blast-radius audit. stop() already covers startupTimer, abortController, processExitedPromise, queryObject.close, queryPromise/messageQueue (with comments documenting this exact hazard); setIdle is intentionally suppressed; restoreEnvVars is now handled. ACP client.close is covered via stop() closing the adapter (proxyBridge.close() runs unconditionally). restart() is immune (bumps after stop). Two gaps remain:

P2 — resetContextPerTurn: null round-trips into an import failure (anchored)

Validation exempts null, export copies it, but the import schema z.boolean().optional() rejects null. Drop the !== null exemption so validation matches the import schema. Narrow (UI never sends null) but real.

P2 — generation bump suppresses the finally but not the catch (anchored)

The catch runs before the finally; the retry branches (query-runner.ts:1012, 1070+; acp-query-runner.ts:826) call setIdle() with no generation check, guarded only by !isQueryInterrupted. For a clear at an idle turn boundary, stop() killing the subprocess may surface as a process-exit/connection error (not AbortError) → transient → premature idle → completion callback fires before the handoff is enqueued. Please confirm interrupt yields AbortError for the idle-kill case (+ test) or add a getQueryGeneration() guard at the retry setIdle sites.

P3 — _lastConsumedUserMessage is stale but not a live bug (no anchor)

After a completed turn it still references that turn; the stale finally skips its clear; startStreamingQuery reuses the same QueryRunner. Not a live bug — every retry/rate-limit path that reads it requires an API response, which requires the new handoff to be consumed first (by then it's updated). An optional defensive clear next to messageQueue.clear() (:822) would close the window; requires adding a small setter/method on QueryRunner, so feel free to skip if you agree it's unreachable.

Considered and rejected — kickoff injections vs the session lock

The codex-connector flagged spawnWorkflowNodeAgentForExecution (:865) and spawnPostApprovalSubSession (:4359) bypassing withSessionInjectLock. Not a bug for this feature: both inject into freshly-spawned sessions, and the clear gate requires hasPriorContext (!!sdkSessionId || !!acpSessionId, task-agent-manager.ts:3349) — a fresh session has neither, so no clear can occur and there's nothing to interleave. The reuse path (where prior context exists) correctly uses the lock (:1487).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5d3b3a72b7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/daemon/src/lib/agent/agent-session.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9bfc996573

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/daemon/src/lib/agent/agent-session.ts Outdated
Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts Outdated

@lsm lsm left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Review by glm-5.1 (GLM)

Model: glm-5.1 | Client: NeoKai | Provider: GLM

Recommendation: APPROVE — posted as COMMENT only because the PR author is the authenticated user (GitHub rejects APPROVE on your own PR). Verdict is explicitly APPROVE, zero P0–P3 code findings.

Round-5 fixes all verified. The catch-side idle race (my round-4 P2) is closed with a cleaner design than I'd suggested — a single early-return instead of scattered per-site guards.

Catch-side idle race — fixed ✅

A single if (getQueryGeneration() !== queryGeneration) return; at the top of both catch handlers (query-runner.ts:887, acp-query-runner.ts:818), placed after the isCleaningUp() check but before handleError / error classification / messageQueue.clear / setIdle / retry. A stale query (clear bumped the generation before stop()) now touches no shared state from the catch — no premature idle, no queue clear, no retry, no error broadcast; the finally's existing isStaleQuery guard handles cleanup uniformly. This subsumes the per-site guards and removes the dependence on whether stop()'s subprocess kill surfaces as AbortError vs. a transient error. New test forces a stale query + transient error and asserts no setIdle and no retry.

_lastConsumedUserMessage — fixed ✅

clearLastConsumedUserMessage() on both runners, called from clearConversationContext (agent-session.ts:826) via the active this.queryRunner instance (agent-session.ts:284/409). The fresh turn's retry can no longer replay the prior handoff. New test asserts the clear.

resetContextPerTurn: null — fixed ✅

The !== null validation exemption is removed (space-workflow-manager.ts:737); null is now rejected, matching the z.boolean().optional() import schema. (Minor nit, non-blocking: no dedicated test asserts nullWorkflowValidationError; the round-trip/export tests cover the behavior, but a one-line null-rejection assertion would lock it in.)

Two codex follow-ups — scope-out accepted

Both are narrow timing windows with no data loss that don't affect the built-in Coding Workflow reviewer (whose turns are spaced — the coder waits for feedback before the next handoff):

  • Turn-end context refresh: a clear landing between a turn-end result and the fire-and-forget context refresh could publish stale usage or enqueue /compact. Fix understood (guard the refresh with the query generation).
  • First-turn provider-init pointer: system:init landing during spawn-setup awaits could cause one wasted first-turn restart. Fix understood (gate on getSDKMessageCount() > 0).

Reasonable to defer with the documented commit to scope separately.

Verification

  • bun run check clean (lint + typecheck + knip + session/db/test-quality guards).
  • Daemon: agent-session-clear-context + query-runner + acp-query-runner (188) and the space suite (503, incl. export-format, export-import-round-trip, reset-context-per-turn, template-hash, built-in-workflows) pass.
  • 0 unresolved threads; PR OPEN, MERGEABLE, targets dev.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

1 similar comment
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@greptile-apps

greptile-apps Bot commented Aug 3, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a per-slot resetContextPerTurn flag that wipes an agent slot's SDK model context at each node→node handoff, enabling reviewers to start each cycle without anchor bias from prior turns. The implementation is split cleanly across three layers: a new AgentSession.clearConversationContext() primitive, injection-layer gating in TaskAgentManager, and a MessageInputKind classifier that excludes human input, recovery nags, and external-event digests from triggering clears.

  • Clear primitive (agent-session.ts): generation bump before stop() prevents the old query's finally from publishing a premature idle; cost rollup, pointer wipe, ACP state, and env restore all land in one atomic DB write; startStreamingQuery() pre-warms the fresh conversation before the caller enqueues the handoff.
  • Injection serialization (task-agent-manager.ts): a per-session promise-chain mutex (withSessionInjectLock) ensures concurrent injects cannot interleave with the multi-await clear; the map entry self-cleans when no subsequent waiter is chained, preventing unbounded growth.
  • Fingerprint / restamp (template-hash.ts): nodeAgentResetContext is omitted from the fingerprint when no slot has the flag, so only affected workflows (the Coding Workflow reviewer) get a new hash — unrelated built-ins are not mass-restamped.

Confidence Score: 5/5

Safe to merge — the context-clear path degrades gracefully on failure, all gating conditions are covered by tests, and the lock correctly serializes concurrent injects without risk of deadlock.

The clear primitive's ordering (generation bump → stop → atomic DB write → restart) is carefully sequenced and matches the existing restart pattern. The promise-chain mutex is correct: tail resolves only after release(), so any concurrent caller waits regardless of whether the prior promise was already settled. InputKind classification exhaustively covers the inject entry points. No pre-existing invariants are broken: NeoKai session IDs are stable, sdk_messages rows are untouched, and the fingerprint change only affects workflows that actually set the flag.

Files Needing Attention: No files require special attention — the core logic in task-agent-manager.ts and agent-session.ts is well-tested, and edge cases (empty agents array, clear failure, ACP provider, cost rollup atomicity) all have dedicated test coverage.

Important Files Changed

Filename Overview
packages/daemon/src/lib/agent/agent-session.ts Adds clearConversationContext(): stops query with a pre-stop generation bump (so the old finally skips setIdle), wipes SDK/ACP session pointers, atomically persists cost rollup + pointer clear in one DB write, restores provider env, and restarts a fresh query.
packages/daemon/src/lib/space/runtime/task-agent-manager.ts Adds per-session promise-chain serialization (withSessionInjectLock), slotResetsContextForSession() data lookup, and gating in injectMessageIntoSession. InputKind propagation correctly excludes human/system/recovery/external-event paths from triggering clears.
packages/daemon/src/lib/agent/query-runner.ts Adds clearLastConsumedUserMessage() and an early-return stale-query guard in the catch block so a generation-bumped query doesn't retry, clear the queue, or surface errors from an intentional stop.
packages/daemon/src/lib/acp/acp-query-runner.ts Mirrors query-runner.ts: adds clearLastConsumedUserMessage() and the stale-query guard in the catch block for ACP sessions.
packages/daemon/src/lib/space/workflows/built-in-workflows.ts Enables resetContextPerTurn on the Coding Workflow reviewer slot and includes it in mergeNodeStructuralFieldsFromTemplate so re-stamps propagate the flag into installed spaces.
packages/daemon/src/lib/space/workflows/template-hash.ts Adds nodeAgentResetContext to the fingerprint only when non-empty, so workflows without the flag preserve their pre-upgrade hash and avoid mass-restamping.
packages/shared/src/types.ts Adds MessageInputKind ('task'
packages/shared/src/types/space.ts Adds resetContextPerTurn?: boolean to WorkflowNodeAgent and ExportedWorkflowNodeAgent; backward-compatible optional field.
packages/web/src/components/space/visual-editor/NodeConfigPanel.tsx Adds SlotResetContextToggle component and wires it into both single-agent and multi-agent UI paths.
packages/daemon/tests/unit/5-space/runtime/reset-context-per-turn.test.ts Covers all gating cases (task/human/system/first-turn/busy/no-flag/concurrent serialization/empty-agents degradation/lock cleanup).
packages/daemon/tests/unit/1-core/agent/agent-session-clear-context.test.ts Tests the clear primitive directly: pointer wipe, single DB write atomicity, messages preserved, generation bump ordering, ACP state, cost rollup, env restore, and runner last-consumed message cleared.
packages/daemon/src/lib/space/managers/space-workflow-manager.ts Adds API-boundary validation rejecting non-boolean resetContextPerTurn (including null).

Sequence Diagram

sequenceDiagram
  participant C as Caller (node→node handoff)
  participant TAM as TaskAgentManager
  participant Lock as sessionInjectLock (per-session promise chain)
  participant AS as AgentSession
  participant SDK as SDK Query Runner

  C->>TAM: "injectSubSessionMessage(sid, msg, isSynthetic=true)"
  TAM->>TAM: "inputKind = 'task'"
  TAM->>Lock: withSessionInjectLock(sid, fn)
  Note over Lock: chains on prior promise, sets map[sid]=tail
  Lock-->>TAM: acquired (await prev resolved)
  TAM->>TAM: slotResetsContextForSession(sid)
  Note over TAM: inputKind=task AND idle AND hasPriorContext AND flagSet
  TAM->>AS: clearConversationContext()
  AS->>AS: incrementQueryGeneration() BEFORE stop()
  AS->>SDK: lifecycleManager.stop()
  Note over SDK: old finally sees stale generation, skips setIdle()
  AS->>AS: wipe sdkSessionId/sdkOriginPath + ACP fields
  AS->>AS: db.updateSession(updates) single atomic write
  AS->>SDK: startStreamingQuery() fresh empty conversation
  AS-->>TAM: done
  TAM->>AS: ensureQueryStarted() no-op or recovery retry
  TAM->>TAM: db.saveUserMessage() + enqueue handoff
  TAM->>Lock: release() map entry self-cleans if last waiter
Loading

Reviews (2): Last reviewed commit: "fix: persist clearConversationContext co..." | Re-trigger Greptile

Comment thread packages/daemon/src/lib/agent/agent-session.ts
Comment thread packages/daemon/src/lib/agent/agent-session.ts Outdated
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

1 similar comment
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lsm has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@lsm lsm left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Review by glm-5.1 (GLM)

Model: glm-5.1 | Client: NeoKai | Provider: GLM

Recommendation: REQUEST_CHANGES — one P1 correctness finding (a new idle-race the redesign introduces) plus one P3 stale-comment. The /clear redesign is a genuine improvement over stop→wipe→restart, but its central claim — "there is no query stop, so no generation-bump idle-race to suppress" — does not hold: /clear's own turn emits a completion-eligible idle that fires the once-firing completion callback before the handoff is reviewed.


P1 — In-stream /clear fires the completion callback before the handoff (new idle-race)

The mechanism (traced end-to-end):

  1. The reuse dispatch path registers a fresh completion callback for the execution turn (task-agent-manager.ts:1323) before the handoff is injected. The listener subscribes to session.updated and fires on the first event carrying processingState.status === 'idle' with sdkCount > 0 (task-agent-manager.ts:2697-2716).
  2. The handoff inject then calls clearConversationContext()messageQueue.enqueue('/clear', true) (agent-session.ts:916), enqueuing /clear ahead of the handoff into the persistent query's generator.
  3. Because /clear is an internal message, createMessageGeneratorWrapper skips setProcessing for it (query-runner.ts:1617-1623). The session therefore stays idle (turn boundary) through the /clear turn — it never transitions to processing.
  4. The SDK then emits a result for the /clear turn (confirmed by the new online test, clear-context-in-stream.test.ts, which counts /clear's result). That result hits sdk-message-handler.ts:690:
    if (isSDKResultMessage(message) && !this.usesSessionStateChangedTurnEnd) {
      await stateManager.setIdle();
    }
  5. setIdle()setState() publishes session.updated with processingState.status === 'idle' unconditionally — there is no idempotency guard, so an idle→idle "transition" still publishes (processing-state-manager.ts:344, setState).
  6. That publish matches the completion-listener predicate (processingState.idle + sdkCount > 0 — prior turns left sdkCount > 0, untouched by the internal /clear). The callback fires on /clear's idle, before the handoff has been pulled.

Empirical confirmation of the lynchpin. I wrote a throwaway repro against ProcessingStateManager (since deleted): calling setIdle() on an already-idle state published exactly one session.updated carrying processingState.status === 'idle'. That is the precise event the listener fires on. The idle→idle re-publish is real, not theoretical.

Consequence. handleSubSessionComplete runs early and (a) flips the reviewer execution to idle before it reviews, and (b) consumes the once-firing callback — fired = true + unsubscribe — so the reviewer's real completion after the review is a no-op. The reviewer's session still processes the handoff (the session processing-state and the execution-record status are independent machines), so the review itself is not lost in the common case, and the workflow still advances on send_message. But:

  • The execution record is prematurely idle for the entire review window, and execution.status === 'idle' is consumed by multiple runtime-tick paths — notably activateRestartRecoveryDownstreamNodes (space-runtime.ts:6552), which treats an idle source-execution as a stalled transition to advance. A daemon restart landing in the /clear-idle window could therefore advance past the reviewer without a review.
  • More generally, the invariant the redesign rests on ("no idle event escapes the clear") is false, so any future consumer keyed on the completion callback / execution-idle inherits a latent hazard.

Why the tests don't catch it. agent-session-clear-context.test.ts stubs ensureQueryStarted/enqueue and never routes /clear's result through sdk-message-handler, so it cannot observe the idle publish or the callback. clear-context-in-stream.test.ts drives the raw SDK query() and asserts only context-clearing + id rotation — it does not exercise the daemon's completion-listener timing. Neither test covers the callback-during-clear path.

Suggested fixes (pick one, with a regression test that registers a completion callback and asserts it fires only after the handoff's result, not after /clear's):

  • Preferred — make setState idempotent for no-op transitions (skip the publish when newState is observably equal to the current state). /clear's turn never sets processing (internal), so its setIdle becomes a silent idle→idle and the listener correctly waits for the handoff's genuine processing→idle. Audit callers that might rely on an idle re-broadcast first.
  • Or gate the completion listener to fire only on an idle that follows actual processing in the same logical turn (track last non-idle status).
  • Or suppress the result→setIdle path for /clear's turn (e.g. a short-lived "clear in flight" token the result handler checks), mirroring how createMessageGeneratorWrapper already skips setProcessing for internal messages.

P3 — Stale comment describes the removed stop→restart design

task-agent-manager.ts:468-472:

"A resetContextPerTurn clear stops and restarts the SDK query across several awaits; without serialization a concurrent inject …"

Under the in-stream /clear redesign there is no stop/restart (the serialization rationale is still valid — it keeps /clear + handoff enqueue atomic — but the description is now inaccurate). Please update the wording.


What I verified

  • PR state: OPEN, MERGEABLE, CLEAN; all checks SUCCESS (SHA 36fcdffdb); 0 unresolved review threads.
  • Orphan removal is clean (clearLastConsumedUserMessage / isClearingConversationContext gone); handleSystemInit capture-on-change is idempotent; pastSdkSessionIds is capped at 50 with consecutive-dedupe; the cost rollup is a single updateSession write; gating keys on sdkSessionId so the ACP path is a true no-op. All of these are good.
  • The resetContextPerTurn validation rejects non-boolean values, and the export/import round-trip preserves the flag.

The data-driven per-slot flag, the cost/trace handling, and the ACP gating are all sound. The one thing blocking is the idle-timing interaction above.

Comment thread packages/daemon/src/lib/agent/agent-session.ts Outdated
Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts
Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts Outdated
Comment thread packages/daemon/src/lib/space/runtime/task-agent-manager.ts Outdated

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lsm has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@lsm lsm left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Review by glm-5.1 (GLM)

Model: glm-5.1 | Client: NeoKai | Provider: GLM

Recommendation: APPROVE. The round-6 P1 (in-stream /clear fired the completion callback before the handoff) is fixed and verified; the two P3 stale comments are refreshed. Zero blocking findings.

P1 fix — verified correct

clearConversationContext now arms a one-shot suppressIdleOnNextResult token on the message handler before enqueuing /clear (agent-session.ts:921), releases it on enqueue failure (agent-session.ts:927), and the handler consumes it on /clear's result (sdk-message-handler.ts:937), skipping both the result→setIdle (:713) and finishTurn. The handoff's own genuine processing→idle then completes the turn.

I empirically confirmed this against the live SDK rather than just reading the code. I drove a streaming-input query(), yielded a normal turn, then /clear, then a probe, and logged every message /clear emits:

hook_started → hook_response → init (rotated id) → assistant → result(subtype=success)

Three things this establishes:

  1. /clear's result is subtype=successhandleResultMessage runs → the token is consumed at :937. The suppression fires on exactly the path /clear takes.
  2. /clear emits no session_state_changed → the handleSessionStateChangedMessagefinishTurn bypass path is not reachable for /clear today.
  3. /clear's assistant message does not set processinghandleAssistantMessage only emits tool-use events/metadata, and detectPhaseFromMessage is a no-op unless already processing; createMessageGeneratorWrapper skips setProcessing for the internal /clear. So the /clear turn is genuinely idle→idle, which is why the spurious idle was firing and why suppressing it is the right call.

Net: /clear's turn publishes no completion-eligible idle, so the once-firing node-agent completion callback now fires only at the handoff's real processing→idle. The race is closed. (The deferred idempotent-setState fix would also have worked since it's an idle→idle no-op — both approaches are valid; the token has zero blast radius outside /clear's turn, which is a reasonable trade.)

The regression coverage is solid: /clear's result calls setIdle 0× and the next result completes normally; clearConversationContext arms before enqueue and releases on enqueue failure. I re-ran both files locally — 85 pass, 0 fail — and CI is fully green on 54083d748 (all unit + online shards).

P3 stale comments — fixed

Both refreshed: task-agent-manager.ts:470 and :1774 now describe the in-stream /clear mechanism instead of the removed stop→wipe→restart.

Informational note (not a request, not blocking)

The token is consumed inside the success-only handleResultMessage branch (:937). That is sound for /clear because it's a local slash command whose result is empirically subtype=success. If a future SDK ever made /clear emit a non-success result or a session_state_changed-driven turn-end, the token would need broadening (e.g. consume at the :713 guard for any result, and guard handleSessionStateChangedMessage too). Not the case today; flagging only so the assumption is on the record.

State

PR #2291: OPEN, MERGEABLE, CLEAN; all checks SUCCESS (run 30877193482); 0 unresolved review threads; HEAD 54083d748. Ready to merge.

lsm added 19 commits August 5, 2026 02:55
Add an optional per-slot resetContextPerTurn boolean to WorkflowNodeAgent
(next to timeoutMs/toolGuards) so a workflow node can opt into fresh model
context at the start of each handoff. Data-driven — no role-name lookup, no
DB migration (optional JSON field; existing workflows default to off).

Also add MessageInputKind ('task' | 'human' | 'system') to classify injected
inputs at the injection layer so only task inputs (handoffs) can trigger the
clear; human input and system recovery nags are excluded by classification.
Give a workflow node fresh eyes by wiping its SDK model context at the start
of each coder handoff, while keeping full NeoKai message history for the UI.

- AgentSession.clearConversationContext(): the /clear equivalent. Stops the
  query, wipes the SDK session pointer (sdkSessionId/sdkOriginPath) in memory
  and DB, and restarts a fresh conversation with no resume. The SDK assigns a
  new session_id (captured by SDKMessageHandler); NeoKai's sdk_messages rows,
  keyed by the stable NeoKai session id, are untouched. Rotating sdkSessionId
  is required — a stale id would make a later daemon restart resume the
  pre-clear history, defeating the feature. NeoKai never keys UI threading on
  sdkSessionId, so the rotation is transparent to the frontend.
- TaskAgentManager.injectMessageIntoSession classifies each input (task /
  human / system) and clears only for task inputs to a slot with
  resetContextPerTurn, at a turn boundary, when prior context exists. Human
  input, system recovery nags, the first turn (no sdkSessionId), a busy
  session, and slots without the flag never clear.
- Enable resetContextPerTurn on the built-in Coding Workflow Reviewer slot.

The flag is read purely from the workflow definition — no 'reviewer' name
special-casing. Coder behavior is unchanged (its slot has no flag).
Add a 'Fresh context each turn' checkbox to the agent-slot section of
NodeConfigPanel (single-agent and per-slot in multi-agent nodes), default
off, with a help note explaining it wipes the agent's model memory between
handoffs while preserving UI history. Persisted onto the agent slot via the
same edit/save path as the other slot options; single-agent shorthand
serializes through to WorkflowNodeAgent.resetContextPerTurn.
P1-1 idle-state race: clearConversationContext no longer emits a client-
visible idle mid-clear. The session is already idle at the turn-start call
site and stop()/startStreamingQuery() do not publish one on the normal path,
so the previous setIdle() was redundant and would prematurely fire the one-
shot node-agent completion callback (re-registered on session reuse) before
the agent processed the handoff — deterministically breaking completion
detection on every 2nd+ reviewer cycle. Added a regression test asserting no
session.updated(idle) is published during the clear.

P1-2 template propagation: add resetContextPerTurn to the workflow fingerprint
(template-hash) so toggling it on a built-in template is detected as drift,
and to mergeNodeStructuralFieldsFromTemplate so the restamp applies it —
otherwise existing seeded spaces never receive the flag.

P2-4 export/import: carry the flag through ExportedWorkflowNodeAgent (type +
Zod schema + serializer + install path).
P2-5 template picker: carry it through workflowToTemplate/buildTemplateNodes
and the WorkflowTemplate step/slot types.
P2-6 editor conversion: preserve it across single<->multi conversion
(removeAgent + primarySlot), matching disabledSkillIds.
P2-7 delivery-path guard: wrap resolveNodeAgents in slotResetsContextForSession
with try/catch so a corrupt/empty-agents node degrades to 'no clear' instead
of dropping the handoff.
P2-3 ACP: documented that the feature is SDK-only (ACP never sets sdkSessionId,
so the guard short-circuits and there is no resumable context to clear).

bun run check clean; space-runtime shards + workflow/export/web tests pass.
VeXQA — input-kind too broad: agent.message.inject (external-event digests)
and notifySourceSession (hook-failure notices) are synthetic but NOT node→node
handoffs, so the 'isSynthetic => task' derivation cleared reset-enabled slots
on them, violating the 'only task inputs clear' contract. Added an explicit
inputKind override param to injectSubSessionMessage and pass 'system' from
both callers; handoff paths (flushPendingMessagesForTarget, send_message
delivery, kickoff) keep defaulting to 'task'.

VeXQU — ACP support: clearConversationContext now clears the provider-
appropriate resumable pointer — sdkSessionId for SDK slots, and acpSessionId
+ the acpInstructionsSent flag for ACP slots (mirroring the normal reset
path's clearAcpSessionStateForReset) — and the injection guard accepts prior
context from either pointer. ACP slots with resetContextPerTurn now get fresh
context too instead of silently no-oping.

Tests: synthetic non-handoff inject does not clear; ACP session state is
cleared for ACP-provider slots. bun run check clean; space-runtime shards pass.
…ew P2)

VfIz0 — avoid mass restamp: nodeAgentResetContext is now omitted from the
workflow fingerprint when no slot has the flag (rather than serialized as
[]). This keeps the hash stable for every built-in without a reset-enabled
slot, so the upgrade does NOT mass-restamp Plan/Research/Review-Only/Fullstack
workflows (which would reapply template autonomy/hooks/prompts and overwrite
operator edits). Only templates that actually set the flag (Coding Workflow)
drift and restamp.

VfIz9 — serialize the clear: added a per-session promise-chain mutex around
injectSubSessionMessageWithOrigin so a resetContextPerTurn clear (stop → wipe
→ restart) cannot interleave with a concurrent inject to the same session.
injectMessageIntoSession is not re-entrant, so no self-deadlock; holds release
in finally. Regression test parks one inject in an async clear and asserts a
second concurrent inject waits on the lock instead of delivering.

VfIzw (busy-handoff) is addressed by reply, not code — see the review thread:
clearing only at turn-start is the spec'd contract, a busy session is
mid-turn, and the proposed defer fix would require modifying the shared
deferred-replay path that bypasses the injection layer.

bun run check clean; space-runtime shards + inject-heavy tests pass.
… flag (review)

VfSc3 (P1) — the prior idle-race fix was incomplete: lifecycleManager.stop()
awaits the query promise, whose runQuery finally block calls
stateManager.setIdle() (generation still current, not cleaning up), publishing
a completion-eligible session.updated. Added isClearingConversationContext()
on AgentSession, set for the duration of the clear's stop(); the SDK
QueryRunner finally (query-runner.ts) and the ACP runner finally
(acp-query-runner.ts) now skip setIdle() while it is set. Test asserts the
flag is armed precisely during stop() (the prior test mocked stop and hid
this).

VfSc- (P2) — clearConversationContext now rolls metadata.lastSdkCost into
costBaseline (and resets lastSdkCost) before stopping, mirroring reset(), so
session cost totals stay accurate across fresh-context turns.

VfSdB (P2) — SpaceWorkflowManager.validateNodeAgentRef rejects non-boolean
resetContextPerTurn values (network JSON is not type-protected), so persisted
config, the editor, and the strict-=== runtime cannot disagree.

bun run check clean; agent-layer, ACP, and space-runtime shards pass.
withSessionInjectLock now deletes its map entry when no later caller chained
onto it, so sessionInjectLocks doesn't retain one resolved promise per
historical session ID (and per rejected nonexistent ID) and grow without bound
over a long-running daemon. Standard self-cleaning mutex: each holder compares
the map's current tail to its own before deleting.

Vfc_I (post-restart FIFO ordering) and Vfc_S (shared-session slot resolution)
are addressed by reply, not code — see the threads.
… P2/P3)

Replaces the isClearingConversationContext boolean flag with a query-generation
bump in clearConversationContext before stop(). The old query's runQuery finally
then sees a stale generation and skips its setIdle() via the EXISTING isStaleQuery
guard (query-runner.ts:1376 / acp-query-runner.ts:756) — the same mechanism
restart relies on.

This resolves both review items:
- P2 (load-bearing but untested): the suppression is now the existing,
  already-tested isStaleQuery guard (query-runner.test.ts:1217 asserts a stale
  query skips the entire finally cleanup block that contains setIdle, so removing
  the guard would fail that test). clearConversationContext's contribution —
  bumping the generation before stop — is asserted directly.
- P3 (timing window): robust. Even if the subprocess exits after stop()'s
  termination timeout, the late finally still observes the stale generation and
  suppresses the idle, unlike a flag that reset on stop()'s return.

Reverts the flag (QueryRunnerContext method, AgentSession field/setter, the
finally checks in both runners, and the test-harness stubs). bun run check clean;
agent-layer, ACP, and space-runtime shards pass.
…ate (review)

Round-3 follow-ups on the resetContextPerTurn feature:
- SlotSkillsToggle: when a shorthand single-agent node is promoted to an
  explicit agents[] slot via the skills toggle, copy step.resetContextPerTurn
  onto the created slot (the last shorthand→explicit conversion path that
  dropped the flag).
- ACP clear: also clear metadata.acpContextUsageEstimate. AcpQueryAdapter seeds
  a new conversation's usage from it when the provider emits no usage_update, so
  leaving it started each fresh turn with the prior turn's token total and
  inflated reported usage. Added acpContextUsageEstimate to SessionMetadata.
- Canvas dirty signature: include resetContextPerTurn (shorthand + per-slot) in
  buildTemplateCanvasSignature so toggling it marks the canvas dirty and the
  'Replace current canvas?' confirmation is no longer bypassed.

(The generation-bump idle-suppression refactor in the prior commit resolves the
round-3 P2/P3 on the load-bearing guard and timing window.) bun run check clean;
agent-layer, ACP, and web tests pass.
…VfZKr)

The generation-bump that suppresses the clear-time idle also makes the old
query's finally skip its originalEnvVars restore (that cleanup only runs for
non-stale queries). Without restoring, the cleared provider's env (base URL,
credentials, daemon-port vars) leaks into the next query's originalEnvVars
snapshot and contaminates later provider setup.

clearConversationContext now restores the daemon's original env (and clears
the snapshot) right after stop(), before the fresh query applies its own env.
Test spies restoreEnvVars to assert it's called with the prior snapshot.
…on (review P2)

P2-1 — null validation: dropped the !== null exemption in
SpaceWorkflowManager.validateNodeAgentRef so resetContextPerTurn: null is
rejected (typeof null !== 'boolean'). This matches the import Zod schema
(z.boolean().optional() accepts undefined, not null), closing the
validate-ok / export-ok / import-reject inconsistency.

P2-2 — catch setIdle during a clear: the generation bump suppresses the
finally's setIdle but not the catch's. When stop() kills the subprocess during
a clear, the surfaced error can be a transient connection error (not
AbortError), hitting a catch retry/terminal branch that calls setIdle with no
generation check — firing the completion callback before the cleared handoff is
enqueued. Added getQueryGeneration() === queryGeneration guards to every catch
setIdle/retry site (SDK: transient/startup-timeout/message-not-found/provider
retries + the terminal-catch setIdle; ACP: the retry branch + the terminal
catch block). Normal retries are unaffected (the guard holds for non-stale
queries). New query-runner test asserts a stale query neither sets idle nor
retries from the catch on a transient error.

(The optional P3 _lastConsumedUserMessage clear is left as-is — not a live
bug, per the review.) bun run check clean; agent-layer, ACP, and both
space-runtime shards pass.
The generation bump that suppresses the clear-time idle also makes the old
query's finally skip clearing _lastConsumedUserMessage. If the fresh query then
hits a retryable error before consuming the new handoff (e.g. a startup
timeout), its retry could re-enqueue the previous turn's message into the
fresh context.

Added clearLastConsumedUserMessage() to both QueryRunner and AcpQueryRunner;
clearConversationContext calls it next to messageQueue.clear(). Test verifies
the runner's lastConsumedUserMessage is null after a clear.
…e mutation (review)

Replaces the scattered per-site getQueryGeneration() guards on the catch's
retry/setIdle sites with a single early-return at the top of both runners'
catch handlers (right after the isCleaningUp() check). A stale query — one
whose generation was bumped by a resetContextPerTurn clear before stop() — now
does NOTHING in the catch: no retry, no messageQueue.clear(), no idle, no error
surfacing. The error is from the intentional stop; the newer query owns the
queue, env, and completion lifecycle.

This closes the gap the per-site guards missed (the terminal messageQueue.clear()
that could delete a fresh handoff enqueued by the replacement query after a
stop() timeout) and is simpler than the scattered guards. The finally's
existing isStaleQuery check still handles cleanup uniformly. Error logging is
preserved (it runs before the early-return).

bun run check clean; agent-layer, ACP, and both space-runtime shards pass.
…restore

The clear-env-restore test spied on getProviderService().restoreEnvVars. The
test imports provider-service via a deep-relative .ts specifier while the source
uses '../provider-service'; a spy on the singleton is fragile to module-identity
differences across Bun versions/platforms, which produced a deterministic
CI-only 'spy was not called' failure (the test/code under test was byte-identical
to the green pre-merge run). restoreEnvVars mutates process.env, a single
process global regardless of singleton identity, so observe the effect there.
Production behavior is unchanged (it always mutates the real process.env).
…one write

Merge the prior-turn cost rollup into the single final updateSession payload
alongside the sdkSessionId/ACP pointer clears, instead of a separate early
metadata write. The early write was redundant with the final write whenever both
fired, and on a mid-clear DB failure it could leave the session with cost rolled
but the resume pointer not cleared (a half-applied clear). Also clarifies the
deliberate startStreamingQuery + caller ensureQueryStarted redundancy with a
comment (Greptile P2).

Adds a test asserting the cost roll and pointer clear land in one atomic
updateSession call.
…+restart

Replace clearConversationContext's stop→wipe→restart dance with the SDK's
/clear command, issued in-stream as an internal control message ahead of the
triggering handoff. The SDK's pull-based generator serializes /clear before the
handoff (it won't pull the next message until the /clear turn completes), so the
handoff always runs in the fresh conversation.

This drops the most fragile parts of the previous implementation: there is no
query stop, so no generation-bump idle-race to suppress (the P1-1 machinery),
no provider-env restore, no messageQueue/runner clear, and no manual restart.
Empirically verified against the bundled CLI (2.1.179) that /clear in streaming-
input mode clears context for the next message and rotates the SDK session id
(online regression test in tests/online/lifecycle/).

The SDK rotates sdkSessionId itself on /clear; handleSystemInit now captures it
on every init where the id changes (was: only when unset), so daemon-restart
resume points at the live conversation, not a stale pre-clear one. The prior id
is appended to a capped metadata.pastSdkSessionIds trace for audit.

Cost handling is unchanged: the prior turn's lastSdkCost is still rolled into
costBaseline before the rotation (the result handler's restart-detection is
unreliable across /clear — the post-clear cost can be >= the prior).

SDK-only: /clear is a Claude-Code command, so resetContextPerTurn on an ACP
(codex) slot is now a documented no-op (gated on sdkSessionId). Drops the
orphaned clearLastConsumedUserMessage from both query runners.
The in-stream /clear redesign removed the generation-bump idle-race suppression
the stop→wipe→restart version had, but /clear's own result re-introduced the
race via a different path: /clear is internal so the generator skips
setProcessing, leaving the session idle through the /clear turn; its result then
hits result→setIdle AND finishTurn, both of which publish processingState.idle
unconditionally (idle→idle). That publish matches the completion-callback
predicate and fires it before the cleared handoff is reviewed, prematurely
marking the execution idle.

Fix: clearConversationContext arms a one-shot idle-suppression token on the
message handler before enqueuing /clear. The result handler skips both idle
publishes (the result→setIdle branch and finishTurn) for that one result, then
consumes the token; the handoff's own genuine processing→idle completes the
turn. The token is released (not consumed) if the /clear enqueue fails, so the
handoff's result still completes normally. Targeted to /clear's turn (zero
blast radius on other state consumers); the broader idempotent-setState fix is
left as a separate follow-up.

Regression test: /clear's result calls setIdle 0 times; the next result
completes normally. Plus clearConversationContext arms/releases the token.

Also refreshes two stale comments that still described stop→wipe→restart.
dev added resolveNodeExecutionForSubSession (a cancelled/archived task
guard) at the top of injectSubSessionMessageWithOrigin. The rebase onto
dev replayed this test with its original mock, which only stubbed
getByAgentSessionId — so the guard threw listByAgentSessionId is not a
function. Stub it to return [] so the guard finds no execution and skips
the terminal-task rejection (these tests exercise the clear path, not the
guard). getById is never reached: SESSION_ID doesn't parse to an embedded
exec id.
@lsm
lsm force-pushed the space/clear-reviewer-context-fresh-eyes-on-each-coder-handoff-2 branch from 54083d7 to 12777ea Compare August 5, 2026 07:07
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lsm has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant