Skip to content

refactor(agent): split runtime and harden loop#1959

Merged
zerob13 merged 6 commits into
devfrom
refactor/agent-runtime-reliability
Jul 13, 2026
Merged

refactor(agent): split runtime and harden loop#1959
zerob13 merged 6 commits into
devfrom
refactor/agent-runtime-reliability

Conversation

@zhangmo8

@zhangmo8 zhangmo8 commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Split AgentRuntimePresenter from a multi-thousand-line monolith into a thin facade plus focused services (session lifecycle, pending input, generation control, stream lifecycle, interaction resume, memory/compaction, turn preparation, etc.), preserving the public IPC/presenter contract.
  • Harden native agent loop reliability: cumulative multi-round usage accounting, consistent terminal runOutcome/stopReason, no-progress repeated-tool-loop guard, MCP tool call cancellation via AbortSignal, and subagent run timeouts/concurrency/handoff guardrails.
  • Add offline native-agent behavior eval harness (pnpm run test:agent:eval) and wire it into PR CI.

Context / docs

  • Architecture: docs/architecture/agent-runtime-presenter-split/, docs/architecture/native-agent-evaluation/, docs/architecture/subagent-run-guardrails/
  • Issues: multi-round usage, repeated tool loop, terminal outcome consistency, MCP tool cancellation under docs/issues/

Test plan

  • pnpm run test:agent:eval
  • Focused unit suites for new services and process guards (agentRuntimePresenter/*, mcpClient, toolManager, subagentOrchestratorTool)
  • pnpm run lint / architecture cleanup guard (lint:agent-cleanup) if available in local workflow
  • Smoke: start agent turn, multi-tool rounds, stop mid-MCP tool, permission pause/resume, subagent run timeout and capacity limits
  • Confirm message metadata retains cumulative usage, provider-round count, tool-call count, and correct terminal stop reasons

Summary by CodeRabbit

  • New Features

    • Added configurable subagent run deadlines and a limit of three active runs per parent.
    • Introduced a deterministic native-agent evaluation harness (with a new eval test command) for scenario-based reporting.
    • Improved cancellation/abort handling across agent runs, tool execution, MCP calls, media/compaction, and standalone generation.
    • Enhanced run status/stop-reason and token/usage accounting across pauses/resumes, plus clearer termination for empty/provider/context/tool-call limits.
    • Added detection/termination for repeated no-progress tool loops and improved follow-up pending state tracking.
  • Documentation

    • Added architecture and issue documentation for evaluation, guardrails, usage accounting, cancellation, and terminal outcomes.
  • Tests

    • Added deterministic native-agent eval scenarios and expanded cancellation, orchestration, and accounting coverage.

Extract AgentRuntimePresenter into focused services, fix multi-round
usage/terminal outcomes, add no-progress and MCP abort guards,
subagent run limits, and a CI native-agent eval baseline.
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7de63590-8ff7-420d-9c35-3314798bc215

📥 Commits

Reviewing files that changed from the base of the PR and between 9d90fb7 and a80679a.

📒 Files selected for processing (11)
  • docs/issues/agent-multiround-usage/spec.md
  • docs/issues/agent-terminal-outcome-consistency/spec.md
  • docs/issues/mcp-tool-cancellation/spec.md
  • src/main/agent/deepchat/loop/deepChatLoopEngine.ts
  • src/main/presenter/agentRuntimePresenter/dispatch.ts
  • src/main/presenter/agentRuntimePresenter/index.ts
  • src/main/presenter/agentRuntimePresenter/process.ts
  • test/main/presenter/agentRuntimePresenter/acpCompatibilityAdapters.test.ts
  • test/main/presenter/agentRuntimePresenter/agentRuntimePresenter.test.ts
  • test/main/presenter/agentRuntimePresenter/dispatch.test.ts
  • test/main/presenter/agentRuntimePresenter/process.test.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • docs/issues/agent-terminal-outcome-consistency/spec.md
  • src/main/agent/deepchat/loop/deepChatLoopEngine.ts
  • docs/issues/mcp-tool-cancellation/spec.md
  • docs/issues/agent-multiround-usage/spec.md
  • test/main/presenter/agentRuntimePresenter/process.test.ts
  • src/main/presenter/agentRuntimePresenter/process.ts
  • test/main/presenter/agentRuntimePresenter/agentRuntimePresenter.test.ts

📝 Walkthrough

Walkthrough

This PR adds a deterministic native-agent evaluation harness and CI command, expands terminal accounting and no-progress detection, propagates cancellation through agent, MCP, provider, and compaction flows, and adds subagent deadline/capacity guardrails. It also updates shared metadata contracts, persistence paths, architecture documentation, and extensive runtime tests.

Changes

Native Agent Evaluation and Runtime Guardrails

Layer / File(s) Summary
Evaluation harness and scenario coverage
test/main/evals/nativeAgent/*, package.json, .github/workflows/prcheck.yml
Adds scripted provider/tool scenarios, normalized reports, aggregate budget checks, deterministic fake-timer tests, and the test:agent:eval command wired into CI.
Terminal accounting and no-progress handling
src/main/presenter/agentRuntimePresenter/{process,accumulator,dispatch,messageStore,noProgressToolLoopGuard}.ts, src/shared/types/agent-interface.d.ts, tests
Persists per-round usage and terminal metadata, restores accounting across pauses, distinguishes stop reasons, enforces tool-call limits, and detects repeated tool batches with correction and termination behavior.
Cancellation propagation
src/main/lib/*, src/main/presenter/{mcpPresenter,llmProviderPresenter,toolPresenter,agentRuntimePresenter}/*, tests
Adds abort-aware awaiting and forwards cancellation through image caching, provider operations, MCP connection/tool/sampling flows, tool permissions, deferred execution, compaction, and stream preparation.
Agent orchestration and persistence
src/main/presenter/agentRuntimePresenter/{index,sessionStore}.ts, src/main/agent/deepchat/*, shared types, tests
Updates session configuration persistence, interaction ownership, follow-up state, provider availability checks, active-generation replacement, and resumed tool accounting.
Subagent lifecycle guardrails
src/main/presenter/toolPresenter/agentTools/subagentOrchestratorTool.ts, tests
Adds bounded run lifetimes, deadline cancellation, a three-run parent cap, structured handoff requirements, and cancellation-safe tape finalization.
Architecture and issue documentation
docs/architecture/*, docs/issues/*, docs/features/mock-long-chat-debug-data/spec.md
Documents native-agent evaluation, subagent guardrails, multi-round usage, repeated tool loops, terminal outcomes, and MCP cancellation behavior.
Supporting maintenance and validation
scripts/agent-cleanup-guard.mjs, test/main/scripts/architectureGuard.test.ts, related tests
Extends side-effect import detection, updates architecture-guard fixtures, and adjusts supporting persistence, memory, authentication, and integration tests.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: refactoring the agent runtime and hardening the loop behavior.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/agent-runtime-reliability

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

zhangmo8 added 2 commits July 13, 2026 18:24
Keep DeepChatLoopEngine architecture while porting multi-round usage,
terminal outcomes, no-progress guards, MCP abort, subagent limits,
and native-agent eval onto the layered runtime.
Drop the obsolete runtime facade size guard, remove a retired
message-store mock symbol, and apply format cleanup.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 18

🧹 Nitpick comments (6)
test/main/presenter/agentRuntimePresenter/streamLifecycleService.test.ts (1)

771-844: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Assert request tracing never persists authorization secrets.

The fixture supplies Bearer secret, but the assertions pass even if that value reaches insertMessageTrace.

Proposed regression assertion
     expect(fixture.messageStore.insertMessageTrace).toHaveBeenCalledWith(
       expect.objectContaining({
         sessionId: 's1',
         messageId: 'assistant-1',
         requestSeq: 1,
         endpoint: 'https://api.example.invalid/chat'
       })
     )
+    expect(
+      JSON.stringify(fixture.messageStore.insertMessageTrace.mock.calls[0]?.[0])
+    ).not.toContain('Bearer secret')
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/main/presenter/agentRuntimePresenter/streamLifecycleService.test.ts`
around lines 771 - 844, Strengthen the insertMessageTrace assertion in the
stream lifecycle test to verify persisted trace data does not contain the
authorization header or Bearer secret supplied through traceConfig.persist. Keep
the existing request metadata assertions and assert the sensitive value is
absent from the recorded call.
test/main/presenter/agentRuntimePresenter/generationControlService.test.ts (1)

31-38: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Assert that replacing a run aborts the superseded controller.

The test permits same-session replacement but only checks ID uniqueness. Verify that the first controller cannot continue executing after ownership moves to the second run.

Proposed test coverage
-    const first = service.registerActiveGeneration('s1', 'm1', new AbortController())
-    const second = service.registerActiveGeneration('s1', 'm2', new AbortController())
+    const firstController = new AbortController()
+    const secondController = new AbortController()
+    const first = service.registerActiveGeneration('s1', 'm1', firstController)
+    const second = service.registerActiveGeneration('s1', 'm2', secondController)

     expect(first.runId).toMatch(/^s1:[A-Za-z0-9_-]+$/)
     expect(second.runId).toMatch(/^s1:[A-Za-z0-9_-]+$/)
     expect(second.runId).not.toBe(first.runId)
+    expect(firstController.signal.aborted).toBe(true)
+    expect(secondController.signal.aborted).toBe(false)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/main/presenter/agentRuntimePresenter/generationControlService.test.ts`
around lines 31 - 38, Update the repeated-run test around
registerActiveGeneration to retain the first AbortController, register the
second run for the same session, and assert that the first controller’s signal
is aborted after replacement. Preserve the existing run ID format and uniqueness
assertions.
src/main/presenter/agentRuntimePresenter/noProgressToolLoopGuard.ts (1)

68-92: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Nested string values inside tool results bypass volatile normalization.

normalizeResultValue's string branch returns the value unchanged instead of applying normalizeVolatileString. Only top-level string content (parse failure or bare-string JSON) gets timestamp/UUID stripping in normalizeToolResult. Any nested string field carrying an embedded timestamp/UUID (not matching a VOLATILE_RESULT_KEYS name) will make the fingerprint differ across otherwise-identical batches, weakening no-progress detection (false negatives).

♻️ Suggested fix
   if (typeof value === 'string') {
-    return value
+    return normalizeVolatileString(value)
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/presenter/agentRuntimePresenter/noProgressToolLoopGuard.ts` around
lines 68 - 92, Update normalizeResultValue so every string value, including
nested array and object entries, passes through normalizeVolatileString before
returning. Preserve the existing VOLATILE_RESULT_KEYS handling and recursive
traversal so keyed volatile values still normalize to <volatile>.
test/main/presenter/agentRuntimePresenter/accumulator.test.ts (1)

237-266: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a second-round test for commitRoundUsage.

Only a single round is exercised here. Given "cumulative multi-round usage accounting" is a headline reliability goal of this PR, add a case that calls accumulate+commitRoundUsage twice (simulating two provider rounds) to confirm totals accumulate correctly across rounds and roundUsage doesn't leak stale values into the next round.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/main/presenter/agentRuntimePresenter/accumulator.test.ts` around lines
237 - 266, Add a second-round scenario to the existing “usage keeps the latest
round snapshot and commits it to metadata once” test, calling accumulate and
commitRoundUsage twice with distinct usage values. Assert metadata totals
accumulate across both rounds and verify roundUsage is cleared after each commit
so values from the first round do not leak into the second.
test/main/presenter/agentRuntimePresenter/turnPreparationService.test.ts (1)

351-364: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover both sides of the ACP-subagent conjunction.

Add cases for an ACP regular session and a non-ACP subagent. The current positive case would still pass if the implementation incorrectly bypassed tools for either condition independently.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/main/presenter/agentRuntimePresenter/turnPreparationService.test.ts`
around lines 351 - 364, Add separate test cases covering the negative sides of
the ACP-subagent condition: an ACP-backed regular session and a non-ACP subagent
session. In each case, verify local tools and prompt layers are not bypassed,
using the existing loadToolDefinitionsForSession and buildSystemPromptWithSkills
assertions and mocks.
test/main/presenter/agentRuntimePresenter/noProgressToolLoopGuard.test.ts (1)

107-124: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add positive coverage for the hard-stop path.

These assertions prove weak acknowledgements never terminate, but no test drives substantive identical results until shouldTerminate becomes true. A regression disabling repeated-tool-loop termination would remain green.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/main/presenter/agentRuntimePresenter/noProgressToolLoopGuard.test.ts`
around lines 107 - 124, Add a test case alongside the existing
NoProgressToolLoopGuard coverage that repeatedly observes identical substantive
tool results until the guard reaches its hard-stop threshold, then assert the
returned observation has shouldTerminate: true. Keep the acknowledgement-only
scenario unchanged and use the existing guard, toolCall, and toolMessage helpers
to exercise the termination path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/architecture/agent-system.md`:
- Around line 74-99: Update the agentRuntimePresenter directory tree in the
architecture documentation to include the existing accumulator.ts,
tapeService.ts, and types.ts modules, preserving the current entries and tree
formatting.

In `@src/main/presenter/agentRuntimePresenter/generationControlService.ts`:
- Around line 82-94: The registerActiveGeneration flow must dispose of any
existing same-session generation before replacing ownership: abort or reject the
prior active/preparation run, then store the new generation and controller.
Update src/main/presenter/agentRuntimePresenter/generationControlService.ts
lines 82-94 accordingly, and add coverage in
test/main/presenter/agentRuntimePresenter/generationControlService.test.ts lines
31-38 asserting the first controller is aborted while the newly registered
controller remains active.

In `@src/main/presenter/agentRuntimePresenter/interactionResumeService.ts`:
- Around line 340-387: Update normalizeReviewDecision to require a valid
normalized riskLevel before honoring any allow/auto_allow decision. When
riskLevel is missing or invalid, return ask_user with the existing actionHash
and rationale handling; only permit execution after accepted risk
classification, while preserving critical/high risk restrictions.

In `@src/main/presenter/agentRuntimePresenter/memoryCompactionService.ts`:
- Around line 244-254: Update cancelManualCompaction and its corresponding
cancellation path to restore the persisted session summary state after
invalidateManualCompaction changes the epoch. Refresh the deleted compaction
message using the current epoch/state, while ensuring the invalidated run cannot
overwrite a newer compaction run; ensure sessionCompactionStates no longer
remains 'compacting' after cancellation.
- Around line 1062-1085: Update extractPlainTextFromRecord to return the
original record.content when JSON parsing fails, preserving raw plain-text
messages for memory ingestion while keeping the existing parsed user and
assistant-content extraction behavior unchanged.

In `@src/main/presenter/agentRuntimePresenter/noProgressToolLoopGuard.ts`:
- Around line 167-183: Update appendCorrection to clone the selected
lastToolMessage before modifying its content, then replace the corresponding
message in messages with that clone. Preserve the existing correction formatting
and return behavior while ensuring the original ChatMessage objects remain
unchanged for consumers of the input array.

In `@src/main/presenter/agentRuntimePresenter/sessionLifecycleService.ts`:
- Around line 61-64: Update the initSession logging in sessionLifecycleService
to stop including the full projectDir value. Replace it with a boolean or
equivalent indicator showing only whether a project directory is configured,
while preserving the existing session, provider, model, and permission fields.
- Around line 255-263: Update resolveProjectDir to load the persisted previous
project directory when sessionProjectDirs has no cached entry, before comparing
it with the normalized incoming directory. Preserve null handling for sessions
without a persisted directory, and ensure the existing prompt and tool profile
cache invalidation runs when the persisted and incoming directories differ.
- Around line 304-330: Update the session-state handling in the runtimeState and
rebuilt-state branches so pending interactions only override the status on the
returned session state, without mutating or caching status as “generating” in
runtimeSharedState.runtimeState. Preserve the underlying cached/rebuilt status,
and apply the interaction-derived overlay when constructing the return value.

In `@src/main/presenter/agentRuntimePresenter/sessionSettingsService.ts`:
- Around line 78-85: Update setPermissionMode to call
sessionStore.updatePermissionMode before mutating
runtimeSharedState.runtimeState, so failed persistence leaves in-memory state
unchanged. In the model-switching flow around the lines 109-124, transactionally
persist both the model and sanitized replacement settings before updating
runtime/cache state. In the generation-settings update flow around lines
160-166, complete persistence before replacing cached generation settings.
- Around line 59-61: Update normalizePermissionMode so unknown, null, or
undefined values fall back to the least-permissive default mode, default,
instead of full_access; preserve the existing handling of default and
auto_approve.

In `@src/main/presenter/agentRuntimePresenter/streamLifecycleService.ts`:
- Around line 318-321: Update the processMessage logging around the
DeepChatAgent info call to remove normalizedInput.text content entirely; retain
only non-sensitive metadata such as the input length alongside the existing
sessionId and projectDir fields.

In `@src/main/presenter/agentRuntimePresenter/turnPreparationService.ts`:
- Around line 262-315: Make every pre-stream await that does not accept an
AbortSignal abort-responsive by wrapping it with the existing awaitWithAbort
helper, including session settings, active-skill resolution, tool definitions,
system-prompt construction, and the corresponding manual-compaction path.
Preserve the existing signal propagation for operations that already accept it,
and keep the post-await throwIfAbortRequested checks.
- Around line 253-256: Update the logging in the prepareNewTurn flow to remove
raw prompt content and the resolved projectDir from the DeepChatAgent log.
Replace them with non-sensitive metadata such as normalized input length and a
boolean indicating whether projectDir is present, while preserving the session
identifier and existing logging context.

In `@src/main/presenter/mcpPresenter/toolManager.ts`:
- Around line 489-491: Update the refresh flow around getAllToolDefinitions and
the related logic at the additional referenced location so an aborted or
superseded refresh cannot commit cachedToolDefinitions or toolNameToTargetMap.
Track the active refresh generation or signal, capture it when starting the
refresh, and validate it remains current and non-aborted immediately before
shared-state mutation; discard stale results otherwise while preserving the
existing abort behavior.

In `@test/main/presenter/agentRuntimePresenter/memoryCompactionService.test.ts`:
- Around line 205-214: Update the test for service.applyCompactionIntent so it
asserts the standardized cancellation outcome when the signal is aborted before
the provider throws, rather than expecting the late “late failure” error.
Preserve the existing setup and verify that the transient message is removed and
prior state is restored as indicated by the test name.

In `@test/main/presenter/agentRuntimePresenter/tapeAccessService.test.ts`:
- Around line 96-104: Update the test for discardSubagentTape to assert call
order, not just invocation counts: verify tapeService.ensureSessionTapeReady
completes before tapeService.recordExternalForkDiscard is called. Keep the
existing argument assertions and ensure the test still covers readiness for the
parent session only.

In `@test/main/presenter/agentSessionPresenter/integration.test.ts`:
- Around line 361-364: Update the integration mock’s updateMetadata callback to
refresh the message’s updated_at timestamp whenever metadata is changed,
matching production behavior while preserving the existing messagesStore lookup
and metadata assignment in updateMetadata.

---

Nitpick comments:
In `@src/main/presenter/agentRuntimePresenter/noProgressToolLoopGuard.ts`:
- Around line 68-92: Update normalizeResultValue so every string value,
including nested array and object entries, passes through
normalizeVolatileString before returning. Preserve the existing
VOLATILE_RESULT_KEYS handling and recursive traversal so keyed volatile values
still normalize to <volatile>.

In `@test/main/presenter/agentRuntimePresenter/accumulator.test.ts`:
- Around line 237-266: Add a second-round scenario to the existing “usage keeps
the latest round snapshot and commits it to metadata once” test, calling
accumulate and commitRoundUsage twice with distinct usage values. Assert
metadata totals accumulate across both rounds and verify roundUsage is cleared
after each commit so values from the first round do not leak into the second.

In `@test/main/presenter/agentRuntimePresenter/generationControlService.test.ts`:
- Around line 31-38: Update the repeated-run test around
registerActiveGeneration to retain the first AbortController, register the
second run for the same session, and assert that the first controller’s signal
is aborted after replacement. Preserve the existing run ID format and uniqueness
assertions.

In `@test/main/presenter/agentRuntimePresenter/noProgressToolLoopGuard.test.ts`:
- Around line 107-124: Add a test case alongside the existing
NoProgressToolLoopGuard coverage that repeatedly observes identical substantive
tool results until the guard reaches its hard-stop threshold, then assert the
returned observation has shouldTerminate: true. Keep the acknowledgement-only
scenario unchanged and use the existing guard, toolCall, and toolMessage helpers
to exercise the termination path.

In `@test/main/presenter/agentRuntimePresenter/streamLifecycleService.test.ts`:
- Around line 771-844: Strengthen the insertMessageTrace assertion in the stream
lifecycle test to verify persisted trace data does not contain the authorization
header or Bearer secret supplied through traceConfig.persist. Keep the existing
request metadata assertions and assert the sensitive value is absent from the
recorded call.

In `@test/main/presenter/agentRuntimePresenter/turnPreparationService.test.ts`:
- Around line 351-364: Add separate test cases covering the negative sides of
the ACP-subagent condition: an ACP-backed regular session and a non-ACP subagent
session. In each case, verify local tools and prompt layers are not bypassed,
using the existing loadToolDefinitionsForSession and buildSystemPromptWithSkills
assertions and mocks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4bacd920-c9b7-471d-9876-9074f3d79fea

📥 Commits

Reviewing files that changed from the base of the PR and between a93d573 and dacd810.

📒 Files selected for processing (87)
  • .github/workflows/prcheck.yml
  • docs/ARCHITECTURE.md
  • docs/architecture/agent-runtime-presenter-split/plan.md
  • docs/architecture/agent-runtime-presenter-split/spec.md
  • docs/architecture/agent-runtime-presenter-split/state-map.md
  • docs/architecture/agent-runtime-presenter-split/tasks.md
  • docs/architecture/agent-system.md
  • docs/architecture/native-agent-evaluation/plan.md
  • docs/architecture/native-agent-evaluation/spec.md
  • docs/architecture/native-agent-evaluation/tasks.md
  • docs/architecture/subagent-run-guardrails/plan.md
  • docs/architecture/subagent-run-guardrails/spec.md
  • docs/architecture/subagent-run-guardrails/tasks.md
  • docs/features/mock-long-chat-debug-data/spec.md
  • docs/issues/agent-multiround-usage/spec.md
  • docs/issues/agent-repeated-tool-loop/spec.md
  • docs/issues/agent-terminal-outcome-consistency/spec.md
  • docs/issues/mcp-tool-cancellation/spec.md
  • package.json
  • scripts/agent-cleanup-guard.mjs
  • src/main/lib/awaitWithAbort.ts
  • src/main/lib/toolCallImagePreviews.ts
  • src/main/presenter/agentRuntimePresenter/accumulator.ts
  • src/main/presenter/agentRuntimePresenter/compactionService.ts
  • src/main/presenter/agentRuntimePresenter/dispatch.ts
  • src/main/presenter/agentRuntimePresenter/generationControlService.ts
  • src/main/presenter/agentRuntimePresenter/index.ts
  • src/main/presenter/agentRuntimePresenter/interactionResumeService.ts
  • src/main/presenter/agentRuntimePresenter/memoryCompactionService.ts
  • src/main/presenter/agentRuntimePresenter/messageHistoryService.ts
  • src/main/presenter/agentRuntimePresenter/messageStore.ts
  • src/main/presenter/agentRuntimePresenter/noProgressToolLoopGuard.ts
  • src/main/presenter/agentRuntimePresenter/pendingInputService.ts
  • src/main/presenter/agentRuntimePresenter/process.ts
  • src/main/presenter/agentRuntimePresenter/runtimeSharedState.ts
  • src/main/presenter/agentRuntimePresenter/sessionLifecycleService.ts
  • src/main/presenter/agentRuntimePresenter/sessionSettingsService.ts
  • src/main/presenter/agentRuntimePresenter/streamLifecycleService.ts
  • src/main/presenter/agentRuntimePresenter/tapeAccessService.ts
  • src/main/presenter/agentRuntimePresenter/tapeService.ts
  • src/main/presenter/agentRuntimePresenter/turnPreparationService.ts
  • src/main/presenter/agentRuntimePresenter/types.ts
  • src/main/presenter/llmProviderPresenter/index.ts
  • src/main/presenter/mcpPresenter/index.ts
  • src/main/presenter/mcpPresenter/mcpClient.ts
  • src/main/presenter/mcpPresenter/toolManager.ts
  • src/main/presenter/sqlitePresenter/tables/deepchatMessages.ts
  • src/main/presenter/toolPresenter/agentTools/subagentOrchestratorTool.ts
  • src/main/presenter/toolPresenter/index.ts
  • src/renderer/src/components/chat/messageListItems.ts
  • src/shared/chat.d.ts
  • src/shared/types/agent-interface.d.ts
  • src/shared/types/core/chat.ts
  • src/shared/types/presenters/core.presenter.d.ts
  • src/shared/types/presenters/tool.presenter.d.ts
  • test/main/evals/nativeAgent/harness.ts
  • test/main/evals/nativeAgent/nativeAgentBehavior.eval.test.ts
  • test/main/evals/nativeAgent/scenarios.ts
  • test/main/lib/awaitWithAbort.test.ts
  • test/main/lib/toolCallImagePreviews.test.ts
  • test/main/presenter/agentRuntimePresenter/accumulator.test.ts
  • test/main/presenter/agentRuntimePresenter/agentRuntimePresenter.test.ts
  • test/main/presenter/agentRuntimePresenter/compactionService.test.ts
  • test/main/presenter/agentRuntimePresenter/dispatch.test.ts
  • test/main/presenter/agentRuntimePresenter/generationControlService.test.ts
  • test/main/presenter/agentRuntimePresenter/interactionResumeService.test.ts
  • test/main/presenter/agentRuntimePresenter/memoryCompactionService.test.ts
  • test/main/presenter/agentRuntimePresenter/messageHistoryService.test.ts
  • test/main/presenter/agentRuntimePresenter/messageStore.test.ts
  • test/main/presenter/agentRuntimePresenter/noProgressToolLoopGuard.test.ts
  • test/main/presenter/agentRuntimePresenter/pendingInputService.test.ts
  • test/main/presenter/agentRuntimePresenter/process.test.ts
  • test/main/presenter/agentRuntimePresenter/serviceConstruction.test.ts
  • test/main/presenter/agentRuntimePresenter/sessionLifecycleService.test.ts
  • test/main/presenter/agentRuntimePresenter/sessionSettingsService.test.ts
  • test/main/presenter/agentRuntimePresenter/streamLifecycleService.test.ts
  • test/main/presenter/agentRuntimePresenter/tapeAccessService.test.ts
  • test/main/presenter/agentRuntimePresenter/turnPreparationService.test.ts
  • test/main/presenter/agentSessionPresenter/integration.test.ts
  • test/main/presenter/llmProviderPresenter.test.ts
  • test/main/presenter/mcpClient.test.ts
  • test/main/presenter/mcpPresenter/toolManager.test.ts
  • test/main/presenter/openaiCodexAuth.test.ts
  • test/main/presenter/toolPresenter/agentTools/subagentOrchestratorTool.test.ts
  • test/main/presenter/toolPresenter/toolPresenter.test.ts
  • test/main/routes/debug/createMockChatSession.test.ts
  • test/main/scripts/architectureGuard.test.ts

Comment on lines 74 to 99
```text
input/context preparation
-> register LoopRun
-> enter provider round
-> provider attempt: context gate -> ViewManifest -> rate gate -> stream
-> update output projection
-> execute typed tool batch
-> message commit -> TapeRecorder tool facts
-> continue or settle
-> terminal projection / pending drain / Memory observer
agentRuntimePresenter/
├── index.ts
├── runtimeSharedState.ts
├── sessionSettingsService.ts
├── generationControlService.ts
├── sessionLifecycleService.ts
├── turnPreparationService.ts
├── streamLifecycleService.ts
├── interactionResumeService.ts
├── memoryCompactionService.ts
├── messageHistoryService.ts
├── tapeAccessService.ts
├── process.ts
├── dispatch.ts
├── contextBuilder.ts
├── sessionStore.ts
├── messageStore.ts
├── pendingInputStore.ts
├── pendingInputCoordinator.ts
├── pendingInputService.ts
├── compactionService.ts
├── noProgressToolLoopGuard.ts
├── echo.ts
└── toolOutputGuard.ts
```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Include all runtime modules in the directory tree.

The tree omits existing accumulator.ts, tapeService.ts, and types.ts, so it no longer accurately documents the directory layout.

Proposed documentation fix
 agentRuntimePresenter/
 ├── index.ts
+├── types.ts
+├── accumulator.ts
 ├── runtimeSharedState.ts
 ...
 ├── tapeAccessService.ts
+├── tapeService.ts
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```text
input/context preparation
-> register LoopRun
-> enter provider round
-> provider attempt: context gate -> ViewManifest -> rate gate -> stream
-> update output projection
-> execute typed tool batch
-> message commit -> TapeRecorder tool facts
-> continue or settle
-> terminal projection / pending drain / Memory observer
agentRuntimePresenter/
├── index.ts
├── runtimeSharedState.ts
├── sessionSettingsService.ts
├── generationControlService.ts
├── sessionLifecycleService.ts
├── turnPreparationService.ts
├── streamLifecycleService.ts
├── interactionResumeService.ts
├── memoryCompactionService.ts
├── messageHistoryService.ts
├── tapeAccessService.ts
├── process.ts
├── dispatch.ts
├── contextBuilder.ts
├── sessionStore.ts
├── messageStore.ts
├── pendingInputStore.ts
├── pendingInputCoordinator.ts
├── pendingInputService.ts
├── compactionService.ts
├── noProgressToolLoopGuard.ts
├── echo.ts
└── toolOutputGuard.ts
```
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/architecture/agent-system.md` around lines 74 - 99, Update the
agentRuntimePresenter directory tree in the architecture documentation to
include the existing accumulator.ts, tapeService.ts, and types.ts modules,
preserving the current entries and tree formatting.

Comment on lines +82 to +94
registerActiveGeneration(
sessionId: string,
messageId: string,
abortController: AbortController
): ActiveGeneration {
const generation: ActiveGeneration = {
runId: `${sessionId}:${this.createRunToken()}`,
messageId,
abortController
}
this.runtimeSharedState.activeGenerations.set(sessionId, generation)
this.runtimeSharedState.abortControllers.set(sessionId, abortController)
return generation

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Dispose of superseded same-session generations before transferring ownership.

Repeated registration currently drops the previous controller without aborting it, allowing unreachable work to continue concurrently.

  • src/main/presenter/agentRuntimePresenter/generationControlService.ts#L82-L94: abort or reject the existing active/preparation run before replacing the map entries.
  • test/main/presenter/agentRuntimePresenter/generationControlService.test.ts#L31-L38: assert that replacement aborts the first controller while leaving the new controller active.
📍 Affects 2 files
  • src/main/presenter/agentRuntimePresenter/generationControlService.ts#L82-L94 (this comment)
  • test/main/presenter/agentRuntimePresenter/generationControlService.test.ts#L31-L38
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/presenter/agentRuntimePresenter/generationControlService.ts` around
lines 82 - 94, The registerActiveGeneration flow must dispose of any existing
same-session generation before replacing ownership: abort or reject the prior
active/preparation run, then store the new generation and controller. Update
src/main/presenter/agentRuntimePresenter/generationControlService.ts lines 82-94
accordingly, and add coverage in
test/main/presenter/agentRuntimePresenter/generationControlService.test.ts lines
31-38 asserting the first controller is aborted while the newly registered
controller remains active.

Comment on lines +340 to +387
function normalizeReviewDecision(rawText: string, actionHash: string): ToolPermissionReviewResult {
const jsonText = extractJsonObjectText(rawText)
if (!jsonText) {
return { decision: 'ask_user', rationale: 'Auto-review did not return JSON.', actionHash }
}

try {
const parsed = JSON.parse(jsonText) as Record<string, unknown>
const rawDecision = parsed.decision ?? parsed.outcome
const riskLevel = normalizeRiskLevel(parsed.riskLevel ?? parsed.risk_level)
const userAuthorization = normalizeUserAuthorization(
parsed.userAuthorization ?? parsed.user_authorization
)
const echoedActionHash =
typeof parsed.actionHash === 'string'
? parsed.actionHash
: typeof parsed.action_hash === 'string'
? parsed.action_hash
: undefined
const rationale =
typeof parsed.rationale === 'string'
? parsed.rationale
: typeof parsed.reason === 'string'
? parsed.reason
: undefined

if (echoedActionHash !== actionHash) {
return {
decision: 'ask_user',
riskLevel,
userAuthorization,
rationale: 'Auto-review action hash mismatch.',
actionHash
}
}

let decision: ToolPermissionReviewResult['decision']
if (rawDecision === 'auto_allow' || rawDecision === 'allow') {
decision = 'auto_allow'
} else if (rawDecision === 'block' || rawDecision === 'deny') {
decision = riskLevel === 'critical' ? 'block' : 'ask_user'
} else {
decision = 'ask_user'
}
if (riskLevel === 'critical') decision = 'block'
else if (riskLevel === 'high') decision = 'ask_user'

return { decision, riskLevel, userAuthorization, rationale, actionHash }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Fail closed when the reviewer omits its risk classification.

A hash-matching {"decision":"allow"} response with no valid riskLevel currently becomes auto_allow. Require an accepted risk level before authorizing execution; malformed model output must fall back to ask_user.

Proposed fix
-    if (rawDecision === 'auto_allow' || rawDecision === 'allow') {
+    if (
+      (rawDecision === 'auto_allow' || rawDecision === 'allow') &&
+      (riskLevel === 'low' || riskLevel === 'medium')
+    ) {
       decision = 'auto_allow'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function normalizeReviewDecision(rawText: string, actionHash: string): ToolPermissionReviewResult {
const jsonText = extractJsonObjectText(rawText)
if (!jsonText) {
return { decision: 'ask_user', rationale: 'Auto-review did not return JSON.', actionHash }
}
try {
const parsed = JSON.parse(jsonText) as Record<string, unknown>
const rawDecision = parsed.decision ?? parsed.outcome
const riskLevel = normalizeRiskLevel(parsed.riskLevel ?? parsed.risk_level)
const userAuthorization = normalizeUserAuthorization(
parsed.userAuthorization ?? parsed.user_authorization
)
const echoedActionHash =
typeof parsed.actionHash === 'string'
? parsed.actionHash
: typeof parsed.action_hash === 'string'
? parsed.action_hash
: undefined
const rationale =
typeof parsed.rationale === 'string'
? parsed.rationale
: typeof parsed.reason === 'string'
? parsed.reason
: undefined
if (echoedActionHash !== actionHash) {
return {
decision: 'ask_user',
riskLevel,
userAuthorization,
rationale: 'Auto-review action hash mismatch.',
actionHash
}
}
let decision: ToolPermissionReviewResult['decision']
if (rawDecision === 'auto_allow' || rawDecision === 'allow') {
decision = 'auto_allow'
} else if (rawDecision === 'block' || rawDecision === 'deny') {
decision = riskLevel === 'critical' ? 'block' : 'ask_user'
} else {
decision = 'ask_user'
}
if (riskLevel === 'critical') decision = 'block'
else if (riskLevel === 'high') decision = 'ask_user'
return { decision, riskLevel, userAuthorization, rationale, actionHash }
function normalizeReviewDecision(rawText: string, actionHash: string): ToolPermissionReviewResult {
const jsonText = extractJsonObjectText(rawText)
if (!jsonText) {
return { decision: 'ask_user', rationale: 'Auto-review did not return JSON.', actionHash }
}
try {
const parsed = JSON.parse(jsonText) as Record<string, unknown>
const rawDecision = parsed.decision ?? parsed.outcome
const riskLevel = normalizeRiskLevel(parsed.riskLevel ?? parsed.risk_level)
const userAuthorization = normalizeUserAuthorization(
parsed.userAuthorization ?? parsed.user_authorization
)
const echoedActionHash =
typeof parsed.actionHash === 'string'
? parsed.actionHash
: typeof parsed.action_hash === 'string'
? parsed.action_hash
: undefined
const rationale =
typeof parsed.rationale === 'string'
? parsed.rationale
: typeof parsed.reason === 'string'
? parsed.reason
: undefined
if (echoedActionHash !== actionHash) {
return {
decision: 'ask_user',
riskLevel,
userAuthorization,
rationale: 'Auto-review action hash mismatch.',
actionHash
}
}
let decision: ToolPermissionReviewResult['decision']
if (
(rawDecision === 'auto_allow' || rawDecision === 'allow') &&
(riskLevel === 'low' || riskLevel === 'medium')
) {
decision = 'auto_allow'
} else if (rawDecision === 'block' || rawDecision === 'deny') {
decision = riskLevel === 'critical' ? 'block' : 'ask_user'
} else {
decision = 'ask_user'
}
if (riskLevel === 'critical') decision = 'block'
else if (riskLevel === 'high') decision = 'ask_user'
return { decision, riskLevel, userAuthorization, rationale, actionHash }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/presenter/agentRuntimePresenter/interactionResumeService.ts` around
lines 340 - 387, Update normalizeReviewDecision to require a valid normalized
riskLevel before honoring any allow/auto_allow decision. When riskLevel is
missing or invalid, return ask_user with the existing actionHash and rationale
handling; only permit execution after accepted risk classification, while
preserving critical/high risk restrictions.

Comment on lines +244 to +254
cancelManualCompaction(sessionId: string): boolean {
const controller = this.manualCompactionControllers.get(sessionId)
if (!controller || controller.signal.aborted) return false
this.invalidateManualCompaction(sessionId)
if (
!this.dependencies.runtimeSharedState.activeGenerations.has(sessionId) &&
!this.dependencies.runtimeSharedState.abortControllers.has(sessionId)
) {
this.host.setSessionStatus(sessionId, 'idle')
}
return true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Restore compaction state after cancellation invalidates the epoch.

Cancellation makes the catch-path epoch check fail, so the deleted compaction message is not refreshed and sessionCompactionStates remains 'compacting'. Subsequent getSessionCompactionState() calls return that stale state indefinitely. Explicitly restore the persisted summary state on cancellation and refresh the deleted message without allowing the old run to overwrite a newer run.

Also applies to: 308-316

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/presenter/agentRuntimePresenter/memoryCompactionService.ts` around
lines 244 - 254, Update cancelManualCompaction and its corresponding
cancellation path to restore the persisted session summary state after
invalidateManualCompaction changes the epoch. Refresh the deleted compaction
message using the current epoch/state, while ensuring the invalidated run cannot
overwrite a newer compaction run; ensure sessionCompactionStates no longer
remains 'compacting' after cancellation.

Comment on lines +1062 to +1085
private extractPlainTextFromRecord(record: Pick<ChatMessageRecord, 'role' | 'content'>): string {
try {
const parsed = JSON.parse(record.content) as unknown
if (record.role === 'user') {
const text = (parsed as { text?: unknown })?.text
return typeof text === 'string' ? text.trim() : ''
}
if (Array.isArray(parsed)) {
return parsed
.map((block) => {
const candidate = block as { type?: string; content?: unknown }
if (candidate?.type === 'content' && typeof candidate.content === 'string') {
return candidate.content
}
return ''
})
.filter(Boolean)
.join(' ')
.trim()
}
return ''
} catch {
return ''
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve plain-text messages during memory extraction.

Non-JSON content currently becomes an empty string, although raw message text is supported elsewhere in this PR. This silently excludes legacy/plain-text messages from memory ingestion.

Proposed fix
     try {
       const parsed = JSON.parse(record.content) as unknown
+      if (typeof parsed === 'string') {
+        return parsed.trim()
+      }
       if (record.role === 'user') {
         const text = (parsed as { text?: unknown })?.text
         return typeof text === 'string' ? text.trim() : ''
       }
@@
     } catch {
-      return ''
+      return record.content.trim()
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private extractPlainTextFromRecord(record: Pick<ChatMessageRecord, 'role' | 'content'>): string {
try {
const parsed = JSON.parse(record.content) as unknown
if (record.role === 'user') {
const text = (parsed as { text?: unknown })?.text
return typeof text === 'string' ? text.trim() : ''
}
if (Array.isArray(parsed)) {
return parsed
.map((block) => {
const candidate = block as { type?: string; content?: unknown }
if (candidate?.type === 'content' && typeof candidate.content === 'string') {
return candidate.content
}
return ''
})
.filter(Boolean)
.join(' ')
.trim()
}
return ''
} catch {
return ''
}
private extractPlainTextFromRecord(record: Pick<ChatMessageRecord, 'role' | 'content'>): string {
try {
const parsed = JSON.parse(record.content) as unknown
if (typeof parsed === 'string') {
return parsed.trim()
}
if (record.role === 'user') {
const text = (parsed as { text?: unknown })?.text
return typeof text === 'string' ? text.trim() : ''
}
if (Array.isArray(parsed)) {
return parsed
.map((block) => {
const candidate = block as { type?: string; content?: unknown }
if (candidate?.type === 'content' && typeof candidate.content === 'string') {
return candidate.content
}
return ''
})
.filter(Boolean)
.join(' ')
.trim()
}
return ''
} catch {
return record.content.trim()
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/presenter/agentRuntimePresenter/memoryCompactionService.ts` around
lines 1062 - 1085, Update extractPlainTextFromRecord to return the original
record.content when JSON parsing fails, preserving raw plain-text messages for
memory ingestion while keeping the existing parsed user and assistant-content
extraction behavior unchanged.

Comment on lines +262 to +315
const generationSettings =
await this.dependencies.sessionSettingsService.getEffectiveGenerationSettings(sessionId)
this.logSlowPreStreamStep(sessionId, 'generation-settings', stepStartedAt)
const modelConfig = this.dependencies.configPresenter.getModelConfig(
state.modelId,
state.providerId
)
const useContextBudget = this.shouldUseDeepChatContextBudget(
state.providerId,
modelConfig,
state.modelId
)
this.throwIfAbortRequested(signal)
const interleavedReasoning = this.resolveInterleavedReasoningConfig(
state.providerId,
state.modelId,
generationSettings
)
const contextBudgetLength = this.resolveDeepChatContextBudgetLength(
state.providerId,
generationSettings.contextLength,
modelConfig,
state.modelId
)
const maxTokens = capAgentRequestMaxTokens(generationSettings.maxTokens, contextBudgetLength)

stepStartedAt = Date.now()
this.resetRuntimeActivatedSkills(sessionId)
this.setRuntimeActivatedSkills(sessionId, normalizedInput.activeSkills ?? [])
const sessionActiveSkillNames = await this.resolveActiveSkillNamesForToolProfile(sessionId)
const effectiveActiveSkillNames = this.resolveEffectiveActiveSkillNames(
sessionActiveSkillNames,
sessionId
)
this.logSlowPreStreamStep(sessionId, 'active-skills', stepStartedAt)

stepStartedAt = Date.now()
const tools = await this.loadToolDefinitionsForSession(
sessionId,
projectDir,
effectiveActiveSkillNames
)
this.logSlowPreStreamStep(sessionId, 'tool-definitions', stepStartedAt)
const toolReserveTokens = estimateToolReserveTokens(tools)
this.throwIfAbortRequested(signal)

stepStartedAt = Date.now()
const baseSystemPrompt = await this.buildSystemPromptWithSkills(
sessionId,
generationSettings.systemPrompt,
tools,
effectiveActiveSkillNames
)
this.logSlowPreStreamStep(sessionId, 'system-prompt', stepStartedAt)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Make pre-stream collaborator waits abort-responsive.

throwIfAbortRequested() runs only after each await. If settings, skill, tool, or prompt resolution never settles, cancellation cannot finish the turn. Wrap promises that do not accept a signal with awaitWithAbort, including the manual-compaction path.

Proposed approach
+import { awaitWithAbort } from '`@/lib/awaitWithAbort`'

-      const generationSettings =
-        await this.dependencies.sessionSettingsService.getEffectiveGenerationSettings(sessionId)
+      const generationSettings = await awaitWithAbort(
+        this.dependencies.sessionSettingsService.getEffectiveGenerationSettings(sessionId),
+        signal
+      )

-      const sessionActiveSkillNames = await this.resolveActiveSkillNamesForToolProfile(sessionId)
+      const sessionActiveSkillNames = await awaitWithAbort(
+        this.resolveActiveSkillNamesForToolProfile(sessionId),
+        signal
+      )

-      const tools = await this.loadToolDefinitionsForSession(
-        sessionId,
-        projectDir,
-        effectiveActiveSkillNames
+      const tools = await awaitWithAbort(
+        this.loadToolDefinitionsForSession(sessionId, projectDir, effectiveActiveSkillNames),
+        signal
       )

Also applies to: 513-540

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/presenter/agentRuntimePresenter/turnPreparationService.ts` around
lines 262 - 315, Make every pre-stream await that does not accept an AbortSignal
abort-responsive by wrapping it with the existing awaitWithAbort helper,
including session settings, active-skill resolution, tool definitions,
system-prompt construction, and the corresponding manual-compaction path.
Preserve the existing signal propagation for operations that already accept it,
and keep the post-await throwIfAbortRequested checks.

Comment thread src/main/presenter/mcpPresenter/toolManager.ts
Comment on lines +205 to +214
it('removes the transient message and restores prior state when cancellation wins', async () => {
const controller = new AbortController()
compactionService.applyCompaction.mockImplementation(async () => {
controller.abort()
throw new Error('late failure')
})

await expect(
service.applyCompactionIntent('s1', intent(), { signal: controller.signal })
).rejects.toThrow('late failure')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Assert the cancellation outcome rather than the late provider error.

The signal is aborted before late failure is thrown, yet the test requires that late error to escape. This contradicts both the test name and the PR’s standardized abort-outcome objective.

Proposed assertion
     await expect(
       service.applyCompactionIntent('s1', intent(), { signal: controller.signal })
-    ).rejects.toThrow('late failure')
+    ).rejects.toMatchObject({ name: 'AbortError' })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('removes the transient message and restores prior state when cancellation wins', async () => {
const controller = new AbortController()
compactionService.applyCompaction.mockImplementation(async () => {
controller.abort()
throw new Error('late failure')
})
await expect(
service.applyCompactionIntent('s1', intent(), { signal: controller.signal })
).rejects.toThrow('late failure')
it('removes the transient message and restores prior state when cancellation wins', async () => {
const controller = new AbortController()
compactionService.applyCompaction.mockImplementation(async () => {
controller.abort()
throw new Error('late failure')
})
await expect(
service.applyCompactionIntent('s1', intent(), { signal: controller.signal })
).rejects.toMatchObject({ name: 'AbortError' })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/main/presenter/agentRuntimePresenter/memoryCompactionService.test.ts`
around lines 205 - 214, Update the test for service.applyCompactionIntent so it
asserts the standardized cancellation outcome when the signal is aborted before
the provider throws, rather than expecting the late “late failure” error.
Preserve the existing setup and verify that the transient message is removed and
prior state is restored as indicated by the test name.

Comment on lines +96 to +104
it('ensures only the parent before recording a subagent discard', async () => {
await service.discardSubagentTape('parent', 'child', { reason: 'abandoned' })

expect(tapeService.ensureSessionTapeReady).toHaveBeenCalledTimes(1)
expect(tapeService.ensureSessionTapeReady).toHaveBeenCalledWith('parent', messageStore)
expect(tapeService.recordExternalForkDiscard).toHaveBeenCalledWith('parent', 'child', 'child', {
reason: 'abandoned'
})
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert readiness occurs before recording the discard.

The test title promises ordering, but only verifies that both calls occurred.

Proposed test fix
     expect(tapeService.recordExternalForkDiscard).toHaveBeenCalledWith('parent', 'child', 'child', {
       reason: 'abandoned'
     })
+    expectCalledBefore(
+      tapeService.ensureSessionTapeReady,
+      tapeService.recordExternalForkDiscard
+    )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('ensures only the parent before recording a subagent discard', async () => {
await service.discardSubagentTape('parent', 'child', { reason: 'abandoned' })
expect(tapeService.ensureSessionTapeReady).toHaveBeenCalledTimes(1)
expect(tapeService.ensureSessionTapeReady).toHaveBeenCalledWith('parent', messageStore)
expect(tapeService.recordExternalForkDiscard).toHaveBeenCalledWith('parent', 'child', 'child', {
reason: 'abandoned'
})
})
it('ensures only the parent before recording a subagent discard', async () => {
await service.discardSubagentTape('parent', 'child', { reason: 'abandoned' })
expect(tapeService.ensureSessionTapeReady).toHaveBeenCalledTimes(1)
expect(tapeService.ensureSessionTapeReady).toHaveBeenCalledWith('parent', messageStore)
expect(tapeService.recordExternalForkDiscard).toHaveBeenCalledWith('parent', 'child', 'child', {
reason: 'abandoned'
})
expectCalledBefore(
tapeService.ensureSessionTapeReady,
tapeService.recordExternalForkDiscard
)
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/main/presenter/agentRuntimePresenter/tapeAccessService.test.ts` around
lines 96 - 104, Update the test for discardSubagentTape to assert call order,
not just invocation counts: verify tapeService.ensureSessionTapeReady completes
before tapeService.recordExternalForkDiscard is called. Keep the existing
argument assertions and ensure the test still covers readiness for the parent
session only.

Comment thread test/main/presenter/agentSessionPresenter/integration.test.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/presenter/agentRuntimePresenter/process.ts`:
- Around line 583-585: Update the NoProgressToolLoopGuard construction to pass
state.metadata.noProgressToolLoop directly, matching the constructor’s expected
AgentNoProgressToolLoopMetadata type; remove the unnecessary initialSnapshot
wrapper.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e39ffe25-4502-4df3-a72b-4def1eb51fd6

📥 Commits

Reviewing files that changed from the base of the PR and between dacd810 and 1761a43.

📒 Files selected for processing (7)
  • package.json
  • scripts/agent-cleanup-guard.mjs
  • src/main/presenter/agentRuntimePresenter/accumulator.ts
  • src/main/presenter/agentRuntimePresenter/dispatch.ts
  • src/main/presenter/agentRuntimePresenter/messageStore.ts
  • src/main/presenter/agentRuntimePresenter/process.ts
  • src/main/presenter/agentRuntimePresenter/tapeService.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/main/presenter/agentRuntimePresenter/tapeService.ts
  • package.json
  • src/main/presenter/agentRuntimePresenter/messageStore.ts

Comment thread src/main/presenter/agentRuntimePresenter/process.ts Outdated

@zerob13 zerob13 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Requesting changes for runtime correctness and a red repository test suite.

Validation at 9d90fb7b:

  • pnpm run typecheck:node: passed
  • pnpm run test:agent:eval: passed
  • pnpm exec vitest --run --config vitest.config.ts test/main: failed (10 tests in 3 files; 3,837 passed)

The green PR workflow currently runs the native-agent eval but not test/main, so it does not expose these failures. In addition to the inline lifecycle failure and swapped ACP assertions, six interaction tests fail because the new deepchatMessagesTable.updateMetadata call was not added to the presenter test fixture, and the deferred-subagent cancellation test still expects the old resolved-error contract.

activeSkillNames: controls?.getActiveSkillNames?.(),
enabledSkillNames: controls?.getEnabledSkillNames?.()
})
io.abortSignal.throwIfAborted()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P1] Persist a returned tool result before honoring late cancellation

A tool can finish its side effect and resolve while also aborting the parent signal. This post-await check discards the returned result, so executeTools never applies it and afterRoundPersisted never writes its tool_call / tool_result Tape facts. The new persists the completed tool batch before a post-tool abort test reproduces this and currently fails. Stage and commit a successfully returned result first, then let the outer batch settle the run as aborted.

return
}
deferredToolCallCounted = true
resumeAccounting = incrementToolCallAccounting(resumeAccounting)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P1] Enforce the global cap before running a deferred call

At a post-call permission pause, the probe is already persisted as the 128th call (process.test.ts explicitly covers 127 → 128). Granting that interaction reaches this increment and executeDeferredToolCall without any cap check, so the side effect executes and metadata becomes 129. The loop guard only rejects a later provider-emitted batch and cannot protect this path. Reject or reserve the deferred execution before invoking it, and never persist a count above 128.

error && typeof error === 'object' && 'name' in error
? (error as { name?: unknown }).name
: undefined
return signal.aborted || errorName === 'AbortError' || errorName === 'CanceledError'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P1] Do not infer parent cancellation from an error name

AbortError is also used for model request timeouts (BaseLLMProvider.createModelRequestTimeoutError), and agent tools can invoke those provider paths. With a live io.abortSignal, this helper still classifies that tool-local timeout as parent cancellation; the error escapes the tool batch and processStream persists runOutcome: aborted / runStopReason: user_stop. Treat it as parent cancellation only when the run signal is aborted (or the thrown value is its exact reason); otherwise preserve it as a tool/provider error.


expect(settlement).toEqual({ status: 'completed', stopReason: 'complete' })
expect(message?.status).toBe('sent')
expect(settlement).toEqual({

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P1] Restore the ACP success/failure expectations

The settlement/status expectations for this complete() case and the adapter.fail() case below were swapped. complete() returns completed/sent, while fail() returns error/error; the surrounding event and Tape assertions already reflect those outcomes. The full main suite currently fails both tests. Restore the original success assertions here and the error assertions in the failure case.

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.

2 participants