@kenkaiiii/ggcoder 5.74.3 → 5.75.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/assets/skills/bulletproof/SKILL.md +1 -1
- package/assets/skills/clarify/SKILL.md +1 -1
- package/assets/skills/code-review/SKILL.md +1 -1
- package/assets/skills/compliance-guard/SKILL.md +1 -1
- package/assets/skills/durable/SKILL.md +1 -1
- package/assets/skills/evidence-led-ui/SKILL.md +1 -1
- package/assets/skills/lean/SKILL.md +1 -1
- package/assets/skills/refactoring/SKILL.md +1 -1
- package/assets/skills/root-cause/SKILL.md +1 -1
- package/assets/skills/shared-language/SKILL.md +1 -1
- package/assets/skills/tdd/SKILL.md +1 -1
- package/dist/app-sidecar/app-settings.d.ts +65 -0
- package/dist/app-sidecar/app-settings.js +147 -0
- package/dist/app-sidecar/app-settings.js.map +1 -0
- package/dist/app-sidecar/attachments.d.ts +11 -0
- package/dist/app-sidecar/attachments.js +47 -0
- package/dist/app-sidecar/attachments.js.map +1 -0
- package/dist/app-sidecar/auth-routes.d.ts +3 -0
- package/dist/app-sidecar/auth-routes.js +290 -0
- package/dist/app-sidecar/auth-routes.js.map +1 -0
- package/dist/app-sidecar/autopilot-runner.d.ts +61 -0
- package/dist/app-sidecar/autopilot-runner.js +367 -0
- package/dist/app-sidecar/autopilot-runner.js.map +1 -0
- package/dist/app-sidecar/file-search.d.ts +15 -0
- package/dist/app-sidecar/file-search.js +103 -0
- package/dist/app-sidecar/file-search.js.map +1 -0
- package/dist/app-sidecar/hf-pull-manager.d.ts +20 -0
- package/dist/app-sidecar/hf-pull-manager.js +224 -0
- package/dist/app-sidecar/hf-pull-manager.js.map +1 -0
- package/dist/app-sidecar/history-routes.d.ts +3 -0
- package/dist/app-sidecar/history-routes.js +375 -0
- package/dist/app-sidecar/history-routes.js.map +1 -0
- package/dist/app-sidecar/history.d.ts +67 -0
- package/dist/app-sidecar/history.js +23 -0
- package/dist/app-sidecar/history.js.map +1 -0
- package/dist/app-sidecar/http.d.ts +5 -0
- package/dist/app-sidecar/http.js +27 -0
- package/dist/app-sidecar/http.js.map +1 -0
- package/dist/app-sidecar/integration-routes.d.ts +3 -0
- package/dist/app-sidecar/integration-routes.js +286 -0
- package/dist/app-sidecar/integration-routes.js.map +1 -0
- package/dist/app-sidecar/json-mode.d.ts +10 -0
- package/dist/app-sidecar/json-mode.js +73 -0
- package/dist/app-sidecar/json-mode.js.map +1 -0
- package/dist/app-sidecar/keep-awake.d.ts +4 -0
- package/dist/app-sidecar/keep-awake.js +5 -0
- package/dist/app-sidecar/keep-awake.js.map +1 -0
- package/dist/app-sidecar/ken-context.d.ts +20 -0
- package/dist/app-sidecar/ken-context.js +55 -0
- package/dist/app-sidecar/ken-context.js.map +1 -0
- package/dist/app-sidecar/mcp-routes.d.ts +3 -0
- package/dist/app-sidecar/mcp-routes.js +185 -0
- package/dist/app-sidecar/mcp-routes.js.map +1 -0
- package/dist/app-sidecar/mcp-rows.d.ts +31 -0
- package/dist/app-sidecar/mcp-rows.js +77 -0
- package/dist/app-sidecar/mcp-rows.js.map +1 -0
- package/dist/app-sidecar/progress-manager.d.ts +10 -0
- package/dist/app-sidecar/progress-manager.js +116 -0
- package/dist/app-sidecar/progress-manager.js.map +1 -0
- package/dist/app-sidecar/prompt-routes.d.ts +3 -0
- package/dist/app-sidecar/prompt-routes.js +354 -0
- package/dist/app-sidecar/prompt-routes.js.map +1 -0
- package/dist/app-sidecar/route-context.d.ts +145 -0
- package/dist/app-sidecar/route-context.js +2 -0
- package/dist/app-sidecar/route-context.js.map +1 -0
- package/dist/app-sidecar/run-control-routes.d.ts +3 -0
- package/dist/app-sidecar/run-control-routes.js +430 -0
- package/dist/app-sidecar/run-control-routes.js.map +1 -0
- package/dist/app-sidecar/session-routes.d.ts +3 -0
- package/dist/app-sidecar/session-routes.js +115 -0
- package/dist/app-sidecar/session-routes.js.map +1 -0
- package/dist/app-sidecar/session-types.d.ts +39 -0
- package/dist/app-sidecar/session-types.js +5 -0
- package/dist/app-sidecar/session-types.js.map +1 -0
- package/dist/app-sidecar/task-routes.d.ts +3 -0
- package/dist/app-sidecar/task-routes.js +149 -0
- package/dist/app-sidecar/task-routes.js.map +1 -0
- package/dist/app-sidecar/usage.d.ts +19 -0
- package/dist/app-sidecar/usage.js +153 -0
- package/dist/app-sidecar/usage.js.map +1 -0
- package/dist/app-sidecar/workspace-routes.d.ts +3 -0
- package/dist/app-sidecar/workspace-routes.js +244 -0
- package/dist/app-sidecar/workspace-routes.js.map +1 -0
- package/dist/app-sidecar.js +440 -3720
- package/dist/app-sidecar.js.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/config.js +1 -1
- package/dist/core/acceptance-checks.js +3 -9
- package/dist/core/acceptance-checks.js.map +1 -1
- package/dist/core/agent-session/helpers.d.ts +26 -0
- package/dist/core/agent-session/helpers.js +47 -0
- package/dist/core/agent-session/helpers.js.map +1 -0
- package/dist/core/agent-session/mcp-tools.d.ts +68 -0
- package/dist/core/agent-session/mcp-tools.js +195 -0
- package/dist/core/agent-session/mcp-tools.js.map +1 -0
- package/dist/core/agent-session/run-loop-monitor.d.ts +63 -0
- package/dist/core/agent-session/run-loop-monitor.js +164 -0
- package/dist/core/agent-session/run-loop-monitor.js.map +1 -0
- package/dist/core/agent-session/transcript-markers.d.ts +57 -0
- package/dist/core/agent-session/transcript-markers.js +166 -0
- package/dist/core/agent-session/transcript-markers.js.map +1 -0
- package/dist/core/agent-session/verification-tracker.d.ts +46 -0
- package/dist/core/agent-session/verification-tracker.js +246 -0
- package/dist/core/agent-session/verification-tracker.js.map +1 -0
- package/dist/core/agent-session.d.ts +21 -87
- package/dist/core/agent-session.js +141 -712
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-providers.js +3 -3
- package/dist/core/auth-providers.js.map +1 -1
- package/dist/core/bundled-agents.js +6 -6
- package/dist/core/bundled-agents.js.map +1 -1
- package/dist/core/checklist-git.d.ts +19 -0
- package/dist/core/checklist-git.js +54 -0
- package/dist/core/checklist-git.js.map +1 -0
- package/dist/core/checklist-items.d.ts +10 -1
- package/dist/core/checklist-items.js +22 -0
- package/dist/core/checklist-items.js.map +1 -1
- package/dist/core/checklist-prompt.d.ts +5 -1
- package/dist/core/checklist-prompt.js +16 -5
- package/dist/core/checklist-prompt.js.map +1 -1
- package/dist/core/checklist-snapshot.d.ts +5 -1
- package/dist/core/checklist-snapshot.js +19 -2
- package/dist/core/checklist-snapshot.js.map +1 -1
- package/dist/core/context-limits.js +1 -1
- package/dist/core/context-limits.js.map +1 -1
- package/dist/core/skills.js +6 -9
- package/dist/core/skills.js.map +1 -1
- package/dist/core/style-packs/index.js +2 -11
- package/dist/core/style-packs/index.js.map +1 -1
- package/dist/core/style-packs/packs.d.ts +3 -12
- package/dist/core/style-packs/packs.js +132 -173
- package/dist/core/style-packs/packs.js.map +1 -1
- package/dist/system-prompt.js +55 -78
- package/dist/system-prompt.js.map +1 -1
- package/dist/tools/ask-user.js +13 -74
- package/dist/tools/ask-user.js.map +1 -1
- package/dist/tools/bash.js +13 -78
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/checklist.d.ts +2 -10
- package/dist/tools/checklist.js +7 -39
- package/dist/tools/checklist.js.map +1 -1
- package/dist/tools/code-nav.js +7 -29
- package/dist/tools/code-nav.js.map +1 -1
- package/dist/tools/edit.d.ts +3 -9
- package/dist/tools/edit.js +66 -65
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/enter-plan.js +2 -7
- package/dist/tools/enter-plan.js.map +1 -1
- package/dist/tools/exit-plan.js +2 -3
- package/dist/tools/exit-plan.js.map +1 -1
- package/dist/tools/find.js +3 -4
- package/dist/tools/find.js.map +1 -1
- package/dist/tools/grep.js +6 -18
- package/dist/tools/grep.js.map +1 -1
- package/dist/tools/ls.js +3 -3
- package/dist/tools/ls.js.map +1 -1
- package/dist/tools/prompt-hints.js +34 -47
- package/dist/tools/prompt-hints.js.map +1 -1
- package/dist/tools/read.js +8 -20
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/search-code.js +4 -12
- package/dist/tools/search-code.js.map +1 -1
- package/dist/tools/skill.js +13 -7
- package/dist/tools/skill.js.map +1 -1
- package/dist/tools/subagent-control.js +7 -22
- package/dist/tools/subagent-control.js.map +1 -1
- package/dist/tools/subagent.js +4 -7
- package/dist/tools/subagent.js.map +1 -1
- package/dist/tools/task-output.js +4 -15
- package/dist/tools/task-output.js.map +1 -1
- package/dist/tools/task-send.js +5 -17
- package/dist/tools/task-send.js.map +1 -1
- package/dist/tools/task-stop.js +2 -2
- package/dist/tools/task-stop.js.map +1 -1
- package/dist/tools/tool-search.js +2 -7
- package/dist/tools/tool-search.js.map +1 -1
- package/dist/tools/tool-tiers.d.ts +17 -4
- package/dist/tools/tool-tiers.js +38 -15
- package/dist/tools/tool-tiers.js.map +1 -1
- package/dist/tools/web-fetch.js +7 -33
- package/dist/tools/web-fetch.js.map +1 -1
- package/dist/tools/write.js +3 -4
- package/dist/tools/write.js.map +1 -1
- package/dist/ui/components/Footer.js +1 -2
- package/dist/ui/components/Footer.js.map +1 -1
- package/package.json +7 -4
- package/dist/core/api-benchmark.d.ts +0 -63
- package/dist/core/api-benchmark.js +0 -381
- package/dist/core/api-benchmark.js.map +0 -1
- package/dist/core/bash-spawn-benchmark.d.ts +0 -1
- package/dist/core/bash-spawn-benchmark.js +0 -139
- package/dist/core/bash-spawn-benchmark.js.map +0 -1
- package/dist/core/cache-warm-benchmark.d.ts +0 -21
- package/dist/core/cache-warm-benchmark.js +0 -170
- package/dist/core/cache-warm-benchmark.js.map +0 -1
- package/dist/core/fast-apply-benchmark.d.ts +0 -25
- package/dist/core/fast-apply-benchmark.js +0 -284
- package/dist/core/fast-apply-benchmark.js.map +0 -1
- package/dist/core/hashline-edit-benchmark.d.ts +0 -88
- package/dist/core/hashline-edit-benchmark.js +0 -518
- package/dist/core/hashline-edit-benchmark.js.map +0 -1
- package/dist/core/semantic-search-benchmark.d.ts +0 -36
- package/dist/core/semantic-search-benchmark.js +0 -211
- package/dist/core/semantic-search-benchmark.js.map +0 -1
- package/dist/core/speed-benchmark.d.ts +0 -132
- package/dist/core/speed-benchmark.js +0 -410
- package/dist/core/speed-benchmark.js.map +0 -1
|
@@ -13,31 +13,26 @@ import { dualAuthProvider } from "@kenkaiiii/gg-core";
|
|
|
13
13
|
import { getClaudeCliUserAgent } from "./claude-code-version.js";
|
|
14
14
|
import { kimiCodingHeaders, isKimiCodingEndpoint } from "./oauth/kimi.js";
|
|
15
15
|
import { isGrokCliEndpoint } from "./oauth/xai.js";
|
|
16
|
-
import { SessionManager,
|
|
16
|
+
import { SessionManager, } from "./session-manager.js";
|
|
17
17
|
import { ExtensionLoader } from "./extensions/loader.js";
|
|
18
18
|
import { shouldCompact, compact, } from "./compaction/compactor.js";
|
|
19
|
-
import { getHistoryMessageVisibility
|
|
19
|
+
import { getHistoryMessageVisibility } from "./session-history.js";
|
|
20
20
|
import { sourceFingerprint as computeSourceFingerprint } from "./session-compaction.js";
|
|
21
|
-
import { getAuthStorageKeys, getContextWindow, getModel,
|
|
21
|
+
import { getAuthStorageKeys, getContextWindow, getModel, MODELS } from "./model-registry.js";
|
|
22
22
|
import { discoverSkills } from "./skills.js";
|
|
23
23
|
import { ensureAppDirs } from "../config.js";
|
|
24
24
|
import { buildSubAgentSystemPrompt, buildSystemPrompt, } from "../system-prompt.js";
|
|
25
25
|
import { createTools, createWebSearchTool, } from "../tools/index.js";
|
|
26
26
|
import { partitionToolsByTier } from "../tools/tool-tiers.js";
|
|
27
|
-
import { autoBackgroundedId, REVIEW_REJECTED_BEFORE_START } from "../tools/bash.js";
|
|
28
27
|
import { buildProcessCompletionFollowUp } from "./process-gate.js";
|
|
29
28
|
import { PLAN_SUBMISSION_NUDGE, shouldNudgePlanSubmission } from "./plan-submission-gate.js";
|
|
30
29
|
import { buildSubAgentCompletionFollowUp } from "./subagent-manager.js";
|
|
31
30
|
import { applyAsyncSubagentPolicy } from "./subagent-policy.js";
|
|
32
31
|
import { resolveResponsesLite, resolveStrictTools, } from "./codex-request-shape.js";
|
|
33
32
|
import { subAgentDescription } from "../tools/subagent.js";
|
|
34
|
-
import {
|
|
35
|
-
import { MCPClientManager, getAllMcpServers } from "./mcp/index.js";
|
|
36
|
-
import { clampMcpToolDescription, DeferredToolCatalog } from "./mcp/deferred-catalog.js";
|
|
33
|
+
import { getAllMcpServers } from "./mcp/index.js";
|
|
37
34
|
import { CONTEXT_LIMITS, resolveSessionContextLimits, } from "./context-limits.js";
|
|
38
|
-
import { McpCatalogCache } from "./mcp/catalog-cache.js";
|
|
39
35
|
import { describeDropped, importForeignSession, } from "./foreign-session-import.js";
|
|
40
|
-
import { createToolSearchTool } from "../tools/tool-search.js";
|
|
41
36
|
import { createSessionStatsTool } from "../tools/session-stats.js";
|
|
42
37
|
import { createDiagnoseCommand, isInternalDiagnosticsEnabled, SessionDiagnosticsRecorder, } from "./internal-diagnostics.js";
|
|
43
38
|
import { log } from "./logger.js";
|
|
@@ -54,15 +49,13 @@ import { pruneStaleToolResults } from "./compaction/tool-result-pruner.js";
|
|
|
54
49
|
import { discoverAgents } from "./agents.js";
|
|
55
50
|
import { enhancePrompt } from "../utils/prompt-enhancer.js";
|
|
56
51
|
import { detectLanguages, detectProjectStack } from "./language-detector.js";
|
|
57
|
-
import {
|
|
52
|
+
import { buildLoopBreakMessage } from "./loop-breaker.js";
|
|
58
53
|
import { buildRegroundingMessage, requestTextForRegrounding } from "./regrounding.js";
|
|
59
|
-
import { buildSemanticLoopJudgePrompt, buildSemanticLoopMessage,
|
|
54
|
+
import { buildSemanticLoopJudgePrompt, buildSemanticLoopMessage, parseSemanticLoopVerdict, SEMANTIC_LOOP_JUDGE_TIMEOUT_MS, withJudgeTimeout, } from "./semantic-loop-check.js";
|
|
60
55
|
import { buildEnvDeltaMessage } from "./env-delta.js";
|
|
61
56
|
import { wrapSteeringText, buildNotificationSteeringText, STEERING_PREFIX } from "./steering.js";
|
|
62
57
|
import { AgentNotificationQueue } from "./agent-notifications.js";
|
|
63
|
-
import { VerificationGate,
|
|
64
|
-
import { classifyVerificationCommand } from "./verification-evidence.js";
|
|
65
|
-
import { captureVerificationSnapshot } from "./verification-snapshot.js";
|
|
58
|
+
import { VerificationGate, VERIFICATION_STATE_KIND } from "./verification-gate.js";
|
|
66
59
|
import { findUserSessionPrompt, getUserSessionPrompt } from "./session-preview.js";
|
|
67
60
|
import { normalizeMessageImages } from "./message-images.js";
|
|
68
61
|
import { loadStreamRules } from "./stream-rules.js";
|
|
@@ -70,41 +63,12 @@ import crypto from "node:crypto";
|
|
|
70
63
|
import fs from "node:fs/promises";
|
|
71
64
|
import os from "node:os";
|
|
72
65
|
import path from "node:path";
|
|
73
|
-
import {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// ── Tool-result policy ─────────────────────────────────────
|
|
80
|
-
/** Resolve the per-result cap passed to the agent loop for the active transport. */
|
|
81
|
-
export function resolveSessionToolResultCharLimit(model, provider, accountId) {
|
|
82
|
-
return (getToolResultCharLimit(model, { provider, accountId }) ??
|
|
83
|
-
Math.floor(getContextWindow(model, { provider, accountId }) * 3.5 * 0.3));
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Aggregate budget for ALL tool results produced in one assistant turn.
|
|
87
|
-
* Individual results are already capped, but wide parallel fan-outs (GPT-5.6's
|
|
88
|
-
* signature behavior) were observed injecting 100k+ uncached tokens in a single
|
|
89
|
-
* turn. ~15% of the context window in chars (1 token ≈ 3.5 chars), floored at
|
|
90
|
-
* 100KB so small windows still fit two full-size reads, ceilinged at 240KB so
|
|
91
|
-
* 1M-context models don't waive the budget entirely.
|
|
92
|
-
*/
|
|
93
|
-
export function resolveSessionTurnToolResultCharLimit(model, provider, accountId) {
|
|
94
|
-
const contextChars = getContextWindow(model, { provider, accountId }) * 3.5;
|
|
95
|
-
return Math.max(100_000, Math.min(Math.floor(contextChars * 0.15), 240_000));
|
|
96
|
-
}
|
|
97
|
-
/** Marker the compactor prepends to the summary message it injects. */
|
|
98
|
-
/**
|
|
99
|
-
* True when an assistant message ends a turn with tool calls still awaiting
|
|
100
|
-
* their results. Inserting a user message there would orphan the tool_use
|
|
101
|
-
* blocks and the provider rejects the next request.
|
|
102
|
-
*/
|
|
103
|
-
function hasUnresolvedToolCalls(message) {
|
|
104
|
-
if (typeof message.content === "string" || !Array.isArray(message.content))
|
|
105
|
-
return false;
|
|
106
|
-
return message.content.some((part) => part.type === "tool_call");
|
|
107
|
-
}
|
|
66
|
+
import { resolveSessionToolResultCharLimit, resolveSessionTurnToolResultCharLimit, hasUnresolvedToolCalls, resolveHomePath, } from "./agent-session/helpers.js";
|
|
67
|
+
import { RunLoopMonitor } from "./agent-session/run-loop-monitor.js";
|
|
68
|
+
import { VerificationTracker } from "./agent-session/verification-tracker.js";
|
|
69
|
+
import { McpToolRegistry } from "./agent-session/mcp-tools.js";
|
|
70
|
+
import { TranscriptMarkers } from "./agent-session/transcript-markers.js";
|
|
71
|
+
export { resolveSessionToolResultCharLimit, resolveSessionTurnToolResultCharLimit, } from "./agent-session/helpers.js";
|
|
108
72
|
// ── Agent Session ──────────────────────────────────────────
|
|
109
73
|
export class AgentSession {
|
|
110
74
|
eventBus = new EventBus();
|
|
@@ -114,24 +78,10 @@ export class AgentSession {
|
|
|
114
78
|
sessionManager;
|
|
115
79
|
extensionLoader = new ExtensionLoader();
|
|
116
80
|
messages = [];
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
// alongside the session and reloaded on resume
|
|
120
|
-
|
|
121
|
-
// webview can interleave them chronologically.
|
|
122
|
-
kenTurns = [];
|
|
123
|
-
// Autopilot Ken (auto-reviewer) markers recorded against this build session:
|
|
124
|
-
// the review verdict shown in the transcript (prompted / done / human /
|
|
125
|
-
// capped). Same not-on-the-DAG treatment as kenTurns — advisory only,
|
|
126
|
-
// persisted + reloaded so a resumed session shows the identical Ken bubble
|
|
127
|
-
// the live run showed instead of dropping it or replaying a raw verdict.
|
|
128
|
-
autopilotMarkers = [];
|
|
129
|
-
// Generic app transcript markers (plan-mode banner, task header, error rows,
|
|
130
|
-
// user-bubble display hints). Same not-on-the-DAG treatment as kenTurns —
|
|
131
|
-
// display only, persisted + reloaded so a resumed session shows the same
|
|
132
|
-
// transcript rows the live run showed.
|
|
133
|
-
appMarkers = [];
|
|
134
|
-
turnMetrics = [];
|
|
81
|
+
// Display-only transcript history (Ken turns, autopilot verdicts, app markers,
|
|
82
|
+
// turn metrics). NEVER part of `messages` (GG Coder must not see them), but
|
|
83
|
+
// persisted alongside the session and reloaded on resume.
|
|
84
|
+
markers = new TranscriptMarkers(() => this.sessionManager, () => this.sessionPath);
|
|
135
85
|
cacheDiagnostics = new CacheDiagnostics();
|
|
136
86
|
/** Internal-only (GG_INTERNAL): live per-session cost/reliability recorder.
|
|
137
87
|
* Absent entirely in public builds — see core/internal-diagnostics.ts. */
|
|
@@ -150,25 +100,8 @@ export class AgentSession {
|
|
|
150
100
|
// ── Self-correction hook state (mirrors the TUI's useAgentLoop refs) ──
|
|
151
101
|
// Reset at the start of every run; observed from the event stream; read by
|
|
152
102
|
// the loop-break (mid-loop) callback.
|
|
153
|
-
|
|
154
|
-
hookText = "";
|
|
155
|
-
hookConsecutiveFailures = 0;
|
|
156
|
-
hookRepeatedNoProgressCalls = 0;
|
|
157
|
-
hookProgressTracker = new ToolCallProgressTracker();
|
|
158
|
-
hookCycleDetector = new CycleDetector();
|
|
159
|
-
hookCyclicPattern = null;
|
|
160
|
-
hookFileEditCounts = new Map();
|
|
161
|
-
hookToolCalls = new Map();
|
|
162
|
-
backgroundVerification = new Map();
|
|
163
|
-
/** 0 = none; 1 = first nudge sent; 2 = final stop-and-report injected. */
|
|
164
|
-
loopBreakInjected = 0;
|
|
103
|
+
loopMonitor = new RunLoopMonitor();
|
|
165
104
|
regroundingInjected = false;
|
|
166
|
-
/** Recent tool-call digests for the semantic loop judge — bounded ring. */
|
|
167
|
-
hookRecentCalls = [];
|
|
168
|
-
/** LLM-judged loop detection state. `verdict` holds a LOOP verdict awaiting
|
|
169
|
-
* injection at the next steering poll; judge failures fail open (no
|
|
170
|
-
* injection) and still consume budget + cooldown. */
|
|
171
|
-
semanticLoop = { checksUsed: 0, lastCheckTurn: 0, pending: false, verdict: null, injected: false };
|
|
172
105
|
/**
|
|
173
106
|
* The environment as the cached system prompt currently describes it.
|
|
174
107
|
* Re-recorded on every prompt build, so a rebuild (e.g. `/add-dir`) needs no
|
|
@@ -184,6 +117,8 @@ export class AgentSession {
|
|
|
184
117
|
/** Verification evidence: code edited this run, and what has proved it since.
|
|
185
118
|
* Passive tracking only — it feeds run status and autopilot, never a turn. */
|
|
186
119
|
verificationGate = new VerificationGate();
|
|
120
|
+
/** Host-observed evidence (edited files, in-flight and background checks) feeding the gate. */
|
|
121
|
+
verificationTracker;
|
|
187
122
|
/** Mirror of the last `hook_armed` value, so the event fires only on an edge. */
|
|
188
123
|
preFinalArmed = false;
|
|
189
124
|
compactionOccurred = false;
|
|
@@ -249,9 +184,8 @@ export class AgentSession {
|
|
|
249
184
|
this.lspManager?.clearPendingDiagnostics();
|
|
250
185
|
void this.subAgentManager?.interruptAll();
|
|
251
186
|
};
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
mcpCatalog;
|
|
187
|
+
/** MCP client, deferred tool_search catalog and cached-stub reconcile state. */
|
|
188
|
+
mcp = new McpToolRegistry(() => this.tools, () => this.contextLimits);
|
|
255
189
|
/** Resolved prompt-injection byte budgets (contextLimits setting). */
|
|
256
190
|
contextLimits = CONTEXT_LIMITS;
|
|
257
191
|
/**
|
|
@@ -260,11 +194,6 @@ export class AgentSession {
|
|
|
260
194
|
* can discover and promote them; a promoted name drops out of this list.
|
|
261
195
|
*/
|
|
262
196
|
deferredBuiltinToolNames = [];
|
|
263
|
-
/** Live (connected) MCP tools by name — the reconcile target for cached stubs. */
|
|
264
|
-
liveMcpTools = new Map();
|
|
265
|
-
/** Server name for each cached-only tool, so a stub knows what to wait on. */
|
|
266
|
-
cachedMcpToolServers = new Map();
|
|
267
|
-
mcpCatalogCache = new McpCatalogCache();
|
|
268
197
|
provider;
|
|
269
198
|
model;
|
|
270
199
|
cwd;
|
|
@@ -337,6 +266,7 @@ export class AgentSession {
|
|
|
337
266
|
opts;
|
|
338
267
|
constructor(options) {
|
|
339
268
|
this.opts = options;
|
|
269
|
+
this.verificationTracker = new VerificationTracker(this.verificationGate, options.cwd, () => this.processManager?.list() ?? []);
|
|
340
270
|
this.provider = options.provider;
|
|
341
271
|
this.model = options.model;
|
|
342
272
|
this.cwd = options.cwd;
|
|
@@ -438,7 +368,7 @@ export class AgentSession {
|
|
|
438
368
|
authStorage: this.authStorage,
|
|
439
369
|
onFileMutated: (filePath) => {
|
|
440
370
|
const relative = path.relative(this.cwd, filePath) || path.basename(filePath);
|
|
441
|
-
this.
|
|
371
|
+
this.verificationTracker.recordFileMutated(relative);
|
|
442
372
|
},
|
|
443
373
|
// Lazy — sessionId/model/provider can change after createTools() runs, so
|
|
444
374
|
// sub-agent spawns read the current parent state at execution time.
|
|
@@ -485,6 +415,10 @@ export class AgentSession {
|
|
|
485
415
|
// every request. Allow-listed sessions keep the eager path — their fixed
|
|
486
416
|
// tool expectations predate the catalog, and tool_search isn't allow-listed.
|
|
487
417
|
if (!this.opts.allowedTools && this.settingsManager.get("deferredBuiltinTools")) {
|
|
418
|
+
// Wrap before partitioning: spawn_agent itself may be deferred, and the
|
|
419
|
+
// catalog must hold the wrapped tool so a promoted spawn still loads
|
|
420
|
+
// wait_agent.
|
|
421
|
+
this.promoteFollowUpTools();
|
|
488
422
|
const { core, deferred } = partitionToolsByTier(this.tools);
|
|
489
423
|
if (deferred.length > 0) {
|
|
490
424
|
// Append-only: `core` preserves the original relative order and
|
|
@@ -492,10 +426,7 @@ export class AgentSession {
|
|
|
492
426
|
// sits inside the cached prefix stays byte-stable across turns.
|
|
493
427
|
this.tools = core;
|
|
494
428
|
this.deferredBuiltinToolNames = deferred.map((t) => t.name);
|
|
495
|
-
this.
|
|
496
|
-
this.mcpCatalog.add(deferred);
|
|
497
|
-
this.ensureToolSearchTool();
|
|
498
|
-
this.promoteWaitAgentAfterSpawn();
|
|
429
|
+
this.mcp.addDeferred(deferred);
|
|
499
430
|
}
|
|
500
431
|
}
|
|
501
432
|
this.rebuildReadTool = rebuildReadTool;
|
|
@@ -513,8 +444,7 @@ export class AgentSession {
|
|
|
513
444
|
// its listening handshake until this resolves), `backgroundMcpConnect`
|
|
514
445
|
// moves the connect off the critical path so the session becomes usable
|
|
515
446
|
// immediately and tools are appended whenever the servers come up.
|
|
516
|
-
this.
|
|
517
|
-
catalogCache: this.mcpCatalogCache,
|
|
447
|
+
this.mcp.createManager({
|
|
518
448
|
modernProtocol: this.settingsManager.get("mcpModernProtocol"),
|
|
519
449
|
onElicit: this.opts.onMcpElicit,
|
|
520
450
|
});
|
|
@@ -663,7 +593,8 @@ export class AgentSession {
|
|
|
663
593
|
* turn, so background-connected servers become available on the next prompt.
|
|
664
594
|
*/
|
|
665
595
|
async connectMcpServers() {
|
|
666
|
-
|
|
596
|
+
const mcpManager = this.mcp.manager;
|
|
597
|
+
if (!mcpManager)
|
|
667
598
|
return;
|
|
668
599
|
// Allow-listed (read-only advisory) sessions enforce a fixed tool set by
|
|
669
600
|
// name. An MCP server is only connected when its name is explicitly
|
|
@@ -699,7 +630,7 @@ export class AgentSession {
|
|
|
699
630
|
// empty catalog and tool_search would answer "the catalog is empty" for
|
|
700
631
|
// capabilities that genuinely exist — a wrong answer, not a slow one.
|
|
701
632
|
await this.seedMcpCatalogFromCache(servers);
|
|
702
|
-
const connected = await
|
|
633
|
+
const connected = await mcpManager.connectAll(servers);
|
|
703
634
|
// Defense-in-depth: even from a whitelisted server, only push tools that
|
|
704
635
|
// pass the allow-list (no-op when there's no allow-list).
|
|
705
636
|
const mcpTools = this.opts.allowedTools
|
|
@@ -738,170 +669,65 @@ export class AgentSession {
|
|
|
738
669
|
* pushed eagerly when the user opted out.
|
|
739
670
|
* Allow-listed sessions (Ken) always get the eager path — their fixed tool
|
|
740
671
|
* expectations predate the catalog, and tool_search isn't allow-listed.
|
|
741
|
-
* Promotion pushes onto the live `this.tools` array the running agent loop
|
|
742
|
-
* re-reads every turn, so promoted tools are callable on the next step.
|
|
743
672
|
*/
|
|
744
673
|
addMcpTools(mcpTools) {
|
|
745
|
-
if (mcpTools.length === 0)
|
|
746
|
-
return;
|
|
747
|
-
for (const tool of mcpTools) {
|
|
748
|
-
this.liveMcpTools.set(tool.name, tool);
|
|
749
|
-
this.cachedMcpToolServers.delete(tool.name);
|
|
750
|
-
}
|
|
751
674
|
const defer = !this.opts.allowedTools && this.settingsManager.get("deferredMcpTools");
|
|
752
|
-
|
|
753
|
-
// Eager path bypasses the catalog, so budget descriptions here too.
|
|
754
|
-
this.replaceOrPushTools(mcpTools.map((tool) => clampMcpToolDescription(tool, this.contextLimits)));
|
|
755
|
-
return;
|
|
756
|
-
}
|
|
757
|
-
this.mcpCatalog ??= new DeferredToolCatalog(this.contextLimits);
|
|
758
|
-
// `add` is name-keyed, so live definitions replace cached stubs in place.
|
|
759
|
-
this.mcpCatalog.add(mcpTools);
|
|
760
|
-
// A stub the model already promoted lives in `this.tools`; swap it for the
|
|
761
|
-
// live tool so later calls dispatch directly instead of through the stub.
|
|
762
|
-
this.replaceLivePromotedTools(mcpTools);
|
|
763
|
-
this.ensureToolSearchTool();
|
|
675
|
+
this.mcp.addMcpTools(mcpTools, defer);
|
|
764
676
|
}
|
|
765
677
|
/**
|
|
766
|
-
*
|
|
767
|
-
*
|
|
768
|
-
*
|
|
769
|
-
*
|
|
770
|
-
*
|
|
771
|
-
*
|
|
772
|
-
*
|
|
678
|
+
* Deferred follow-up tools whose need is certain once a trigger succeeds.
|
|
679
|
+
* `wait_agent` follows nearly every `spawn_agent`, and loading it through
|
|
680
|
+
* `tool_search` cost a whole turn (bench 41: ~6 s per fan-out). Likewise a
|
|
681
|
+
* background `bash` is always followed by `task_output` (and sometimes
|
|
682
|
+
* `task_send`/`task_stop`). Promote them as soon as the trigger succeeds: the
|
|
683
|
+
* tool list grows exactly as it would after that `tool_search`, one turn
|
|
684
|
+
* earlier, and sessions that never trigger keep the smaller prefix.
|
|
685
|
+
* Call before tier partitioning so a deferred trigger is wrapped too.
|
|
773
686
|
*/
|
|
774
|
-
|
|
775
|
-
this.
|
|
776
|
-
|
|
777
|
-
return;
|
|
778
|
-
this.tools.push(createToolSearchTool(this.mcpCatalog, (promoted) => {
|
|
779
|
-
this.tools.push(...promoted);
|
|
780
|
-
}, async (toolName) => {
|
|
781
|
-
if (this.liveMcpTools.has(toolName))
|
|
782
|
-
return undefined;
|
|
783
|
-
const serverName = this.cachedMcpToolServers.get(toolName);
|
|
784
|
-
if (!serverName)
|
|
785
|
-
return undefined;
|
|
786
|
-
const outcome = (await this.mcpManager?.whenConnected(serverName)) ?? {
|
|
787
|
-
ok: false,
|
|
788
|
-
error: "MCP is disabled for this session",
|
|
789
|
-
};
|
|
790
|
-
return outcome.ok
|
|
791
|
-
? { serverName, ok: true }
|
|
792
|
-
: { serverName, ok: false, error: outcome.error };
|
|
793
|
-
}, this.contextLimits));
|
|
687
|
+
promoteFollowUpTools() {
|
|
688
|
+
this.wrapToPromote("spawn_agent", ["wait_agent"]);
|
|
689
|
+
this.wrapToPromote("bash", ["task_output", "task_send", "task_stop"], (args) => args.run_in_background === true);
|
|
794
690
|
}
|
|
795
|
-
/**
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
691
|
+
/** Append a deferred built-in to the live tool list (append-only). */
|
|
692
|
+
promoteDeferredBuiltin(name) {
|
|
693
|
+
if (!this.deferredBuiltinToolNames.includes(name))
|
|
694
|
+
return undefined;
|
|
695
|
+
const live = this.tools.find((t) => t.name === name);
|
|
696
|
+
if (live)
|
|
697
|
+
return live;
|
|
698
|
+
const [tool] = this.mcp.promote([name]);
|
|
699
|
+
if (tool)
|
|
700
|
+
this.tools.push(tool);
|
|
701
|
+
return tool;
|
|
702
|
+
}
|
|
703
|
+
wrapToPromote(trigger, followUps, when = () => true) {
|
|
704
|
+
const index = this.tools.findIndex((t) => t.name === trigger);
|
|
705
|
+
const tool = index >= 0 ? this.tools[index] : undefined;
|
|
706
|
+
if (!tool)
|
|
806
707
|
return;
|
|
807
708
|
this.tools[index] = {
|
|
808
|
-
...
|
|
709
|
+
...tool,
|
|
809
710
|
execute: async (args, context) => {
|
|
810
|
-
const result = await
|
|
811
|
-
if (
|
|
812
|
-
this.tools.
|
|
711
|
+
const result = await tool.execute(args, context);
|
|
712
|
+
if (when(args)) {
|
|
713
|
+
const missing = followUps.filter((name) => !this.tools.some((t) => t.name === name));
|
|
714
|
+
if (missing.length > 0)
|
|
715
|
+
this.tools.push(...this.mcp.promote(missing));
|
|
813
716
|
}
|
|
814
717
|
return result;
|
|
815
718
|
},
|
|
816
719
|
};
|
|
817
720
|
}
|
|
818
|
-
/** Append tools, replacing any same-named entry (cached stub → live tool). */
|
|
819
|
-
replaceOrPushTools(tools) {
|
|
820
|
-
for (const tool of tools) {
|
|
821
|
-
const index = this.tools.findIndex((t) => t.name === tool.name);
|
|
822
|
-
if (index >= 0)
|
|
823
|
-
this.tools[index] = tool;
|
|
824
|
-
else
|
|
825
|
-
this.tools.push(tool);
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
/** Swap already-promoted cached stubs for their live equivalents, in place. */
|
|
829
|
-
replaceLivePromotedTools(tools) {
|
|
830
|
-
for (const tool of tools) {
|
|
831
|
-
const index = this.tools.findIndex((t) => t.name === tool.name);
|
|
832
|
-
if (index >= 0)
|
|
833
|
-
this.tools[index] = tool;
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
721
|
/**
|
|
837
722
|
* Publish cached tool definitions into the deferred catalog so `tool_search`
|
|
838
|
-
* answers correctly on turn 1
|
|
839
|
-
* description and input schema; calling it waits for the live connection and
|
|
840
|
-
* then dispatches against the real client, or returns a clear error when that
|
|
841
|
-
* server ultimately failed. Live tools replace stubs on connect.
|
|
723
|
+
* answers correctly on turn 1 (see {@link McpToolRegistry.seedFromCache}).
|
|
842
724
|
*/
|
|
843
725
|
async seedMcpCatalogFromCache(servers) {
|
|
844
726
|
if (!this.opts.backgroundMcpConnect)
|
|
845
727
|
return;
|
|
846
728
|
if (this.opts.allowedTools || !this.settingsManager.get("deferredMcpTools"))
|
|
847
729
|
return;
|
|
848
|
-
|
|
849
|
-
try {
|
|
850
|
-
entries = await this.mcpCatalogCache.entriesFor(servers);
|
|
851
|
-
}
|
|
852
|
-
catch {
|
|
853
|
-
return;
|
|
854
|
-
}
|
|
855
|
-
const stubs = [];
|
|
856
|
-
for (const [serverName, entry] of entries) {
|
|
857
|
-
for (const cached of entry.tools) {
|
|
858
|
-
if (this.liveMcpTools.has(cached.name))
|
|
859
|
-
continue;
|
|
860
|
-
this.cachedMcpToolServers.set(cached.name, serverName);
|
|
861
|
-
stubs.push(this.buildCachedMcpTool(serverName, cached));
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
if (stubs.length === 0)
|
|
865
|
-
return;
|
|
866
|
-
log("INFO", "mcp", "Seeded deferred tool catalog from cache", {
|
|
867
|
-
tools: String(stubs.length),
|
|
868
|
-
servers: String(entries.size),
|
|
869
|
-
});
|
|
870
|
-
this.addCachedMcpTools(stubs);
|
|
871
|
-
}
|
|
872
|
-
/** Catalog-only registration for cached stubs — never marks them live. */
|
|
873
|
-
addCachedMcpTools(stubs) {
|
|
874
|
-
this.mcpCatalog ??= new DeferredToolCatalog(this.contextLimits);
|
|
875
|
-
this.mcpCatalog.add(stubs);
|
|
876
|
-
this.ensureToolSearchTool();
|
|
877
|
-
}
|
|
878
|
-
buildCachedMcpTool(serverName, cached) {
|
|
879
|
-
return {
|
|
880
|
-
name: cached.name,
|
|
881
|
-
description: cached.description,
|
|
882
|
-
parameters: z.record(z.string(), z.unknown()),
|
|
883
|
-
rawInputSchema: cached.rawInputSchema,
|
|
884
|
-
execute: async (args, context) => {
|
|
885
|
-
const live = this.liveMcpTools.get(cached.name);
|
|
886
|
-
if (live)
|
|
887
|
-
return live.execute(args, context);
|
|
888
|
-
const outcome = (await this.mcpManager?.whenConnected(serverName)) ?? {
|
|
889
|
-
ok: false,
|
|
890
|
-
error: "MCP is disabled for this session",
|
|
891
|
-
};
|
|
892
|
-
if (!outcome.ok) {
|
|
893
|
-
return (`MCP tool ${cached.name} is unavailable: server "${serverName}" did not connect ` +
|
|
894
|
-
`(${outcome.error}). This tool was offered from a cached catalog. ` +
|
|
895
|
-
`Use a different approach or ask the user to check their MCP configuration.`);
|
|
896
|
-
}
|
|
897
|
-
const connected = this.liveMcpTools.get(cached.name);
|
|
898
|
-
if (!connected) {
|
|
899
|
-
return (`MCP tool ${cached.name} no longer exists: server "${serverName}" connected but ` +
|
|
900
|
-
`does not expose it. The cached catalog entry was stale.`);
|
|
901
|
-
}
|
|
902
|
-
return connected.execute(args, context);
|
|
903
|
-
},
|
|
904
|
-
};
|
|
730
|
+
await this.mcp.seedFromCache(servers);
|
|
905
731
|
}
|
|
906
732
|
/**
|
|
907
733
|
* Resolve a `/name [args]` input to the prompt template it expands into, or
|
|
@@ -1090,34 +916,15 @@ export class AgentSession {
|
|
|
1090
916
|
resetHookState(originalRequest) {
|
|
1091
917
|
this.opts.completionReview?.begin(originalRequest);
|
|
1092
918
|
this.lspManager?.clearPendingDiagnostics();
|
|
1093
|
-
this.
|
|
1094
|
-
this.
|
|
1095
|
-
this.hookConsecutiveFailures = 0;
|
|
1096
|
-
this.hookRepeatedNoProgressCalls = 0;
|
|
1097
|
-
this.hookProgressTracker.reset();
|
|
1098
|
-
this.hookCycleDetector.reset();
|
|
1099
|
-
this.hookCyclicPattern = null;
|
|
1100
|
-
this.hookFileEditCounts.clear();
|
|
1101
|
-
this.hookToolCalls.clear();
|
|
1102
|
-
this.loopBreakInjected = 0;
|
|
919
|
+
this.loopMonitor.reset();
|
|
920
|
+
this.verificationTracker.resetRun();
|
|
1103
921
|
this.regroundingInjected = false;
|
|
1104
|
-
this.hookRecentCalls = [];
|
|
1105
|
-
this.semanticLoop = {
|
|
1106
|
-
checksUsed: 0,
|
|
1107
|
-
lastCheckTurn: 0,
|
|
1108
|
-
pending: false,
|
|
1109
|
-
verdict: null,
|
|
1110
|
-
injected: false,
|
|
1111
|
-
};
|
|
1112
922
|
this.runStartedAt = Date.now();
|
|
1113
923
|
this.processGateInjected = 0;
|
|
1114
924
|
this.planSubmissionNudges = 0;
|
|
1115
925
|
this.verificationGate.beginRun();
|
|
1116
926
|
const processes = new Set(this.processManager?.list().map((p) => p.id) ?? []);
|
|
1117
|
-
|
|
1118
|
-
if (!processes.has(id))
|
|
1119
|
-
this.backgroundVerification.delete(id);
|
|
1120
|
-
}
|
|
927
|
+
this.verificationTracker.pruneBackground(processes);
|
|
1121
928
|
this.compactionOccurred = false;
|
|
1122
929
|
// Post-turn compaction may have landed between runs — adopt its armed
|
|
1123
930
|
// signal so the re-grounding hook fires for this run exactly as it would
|
|
@@ -1141,160 +948,20 @@ export class AgentSession {
|
|
|
1141
948
|
}
|
|
1142
949
|
switch (event.type) {
|
|
1143
950
|
case "text_delta":
|
|
1144
|
-
this.
|
|
951
|
+
this.loopMonitor.recordText(event.text);
|
|
1145
952
|
break;
|
|
1146
953
|
case "tool_call_start": {
|
|
1147
|
-
this.
|
|
1148
|
-
name: event.name,
|
|
1149
|
-
args: event.args ?? {},
|
|
1150
|
-
revision: this.verificationGate.revision,
|
|
1151
|
-
});
|
|
1152
|
-
const startClassification = event.name === "bash" && typeof event.args?.command === "string"
|
|
1153
|
-
? classifyVerificationCommand(event.args.command)
|
|
1154
|
-
: null;
|
|
1155
|
-
if (startClassification &&
|
|
1156
|
-
typeof event.args?.command === "string" &&
|
|
1157
|
-
(isVerificationCommand(event.args.command) ||
|
|
1158
|
-
startClassification.accepted ||
|
|
1159
|
-
// Must match the tool_call_end predicate: a snapshot-eligible
|
|
1160
|
-
// command that never captured a "before" snapshot is misread at
|
|
1161
|
-
// the end as an uncomparable workspace and re-arms the gate.
|
|
1162
|
-
startClassification.snapshotEligible === true)) {
|
|
1163
|
-
// A check that can rewrite files (--fix, build scripts, emitters)
|
|
1164
|
-
// invalidates earlier in-flight evidence AND marks the run as
|
|
1165
|
-
// touched. A check that is merely UNRECOGNIZED (`make test`, `deno
|
|
1166
|
-
// test`) rewrites nothing we can point to: bumping the revision for
|
|
1167
|
-
// it poisoned the gate on green output and re-armed the hook into
|
|
1168
|
-
// every later question turn.
|
|
1169
|
-
this.verificationGate.recordVerificationAttempt();
|
|
1170
|
-
const classification = startClassification;
|
|
1171
|
-
if (classification.snapshotEligible && event.args.persist !== true) {
|
|
1172
|
-
const call = this.hookToolCalls.get(event.toolCallId);
|
|
1173
|
-
call.sourceSnapshot = await captureVerificationSnapshot(this.opts.cwd, [
|
|
1174
|
-
...this.hookFileEditCounts.keys(),
|
|
1175
|
-
]);
|
|
1176
|
-
if (call.sourceSnapshot === null)
|
|
1177
|
-
this.verificationGate.requireFreshVerification(true);
|
|
1178
|
-
}
|
|
1179
|
-
else if ((classification.accepted && event.args.persist !== true) ||
|
|
1180
|
-
(!classification.accepted && classification.mayMutate)) {
|
|
1181
|
-
// Flag the workspace unknown only when tool_call_end can resolve
|
|
1182
|
-
// it: a bounded check records pass/fail, a file-rewriting command
|
|
1183
|
-
// bumps the revision. An unrecognized read-only check (`biome ci`)
|
|
1184
|
-
// or a persistent-shell run records nothing at the end, so
|
|
1185
|
-
// flagging it left verified work Unverified forever — and
|
|
1186
|
-
// autopilot silently refused every later turn.
|
|
1187
|
-
this.verificationGate.requireFreshVerification(!classification.accepted && classification.mayMutate);
|
|
1188
|
-
}
|
|
954
|
+
if (await this.verificationTracker.recordToolCallStart(event)) {
|
|
1189
955
|
await this.persistVerificationState();
|
|
1190
956
|
}
|
|
1191
957
|
break;
|
|
1192
958
|
}
|
|
1193
959
|
case "tool_call_end": {
|
|
1194
|
-
const call = this.
|
|
960
|
+
const call = this.verificationTracker.getToolCall(event.toolCallId);
|
|
1195
961
|
const name = call?.name ?? "";
|
|
1196
962
|
const args = call?.args;
|
|
1197
|
-
this.
|
|
1198
|
-
|
|
1199
|
-
this.hookStats.toolFailures += 1;
|
|
1200
|
-
this.hookConsecutiveFailures = event.isError ? this.hookConsecutiveFailures + 1 : 0;
|
|
1201
|
-
this.hookRepeatedNoProgressCalls = this.hookProgressTracker.record(name, args, event.result, event.isError);
|
|
1202
|
-
this.hookCyclicPattern = this.hookCycleDetector.record(name, args, event.result, event.isError);
|
|
1203
|
-
// Semantic-loop judge input: a bounded digest of WHAT was attempted and
|
|
1204
|
-
// HOW it came out. Args/results are sliced AT RECORD TIME — a write with
|
|
1205
|
-
// a 50 KiB payload or a bash dump must never inflate the ring, and the
|
|
1206
|
-
// judge needs shapes, not payloads.
|
|
1207
|
-
this.hookRecentCalls.push({
|
|
1208
|
-
tool: name,
|
|
1209
|
-
args: args === undefined ? "" : JSON.stringify(args).slice(0, 300),
|
|
1210
|
-
ok: !event.isError,
|
|
1211
|
-
result: event.result.slice(0, 400),
|
|
1212
|
-
});
|
|
1213
|
-
if (this.hookRecentCalls.length > MAX_SEMANTIC_LOOP_CALLS) {
|
|
1214
|
-
this.hookRecentCalls.splice(0, this.hookRecentCalls.length - MAX_SEMANTIC_LOOP_CALLS);
|
|
1215
|
-
}
|
|
1216
|
-
// Only host-observed successful mutations and trustworthy check results
|
|
1217
|
-
// affect approval. The model's text is never evidence.
|
|
1218
|
-
let verificationChanged = false;
|
|
1219
|
-
// In plan mode write/edit can only touch `.gg/plans/*.md`; a code-file
|
|
1220
|
-
// write there was refused (refusals return text, not isError). Counting
|
|
1221
|
-
// it as a change left "unverified" work that blocked plan approval.
|
|
1222
|
-
if (!event.isError && args && !this.planModeRef.current) {
|
|
1223
|
-
if (name === "edit" || name === "write") {
|
|
1224
|
-
// Check-owning files (tsconfig.json, pytest.ini, vitest.config.ts …)
|
|
1225
|
-
// are tracked even when they are not source code: editing one
|
|
1226
|
-
// invalidates earlier check results. A multi-file edit records every file.
|
|
1227
|
-
for (const filePath of editTargetPaths(args)) {
|
|
1228
|
-
if (isCodeFilePath(filePath) || isCheckOwnFile(filePath)) {
|
|
1229
|
-
this.verificationGate.recordMutation(filePath);
|
|
1230
|
-
verificationChanged = true;
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
if (args && call && name === "bash") {
|
|
1236
|
-
const command = typeof args.command === "string" ? args.command : "";
|
|
1237
|
-
const classification = classifyVerificationCommand(command);
|
|
1238
|
-
if (args.review === true && event.result === REVIEW_REJECTED_BEFORE_START) {
|
|
1239
|
-
// The core tool rejected its arguments BEFORE spawning. Never mint
|
|
1240
|
-
// a pass, or poison the failed-check ledger with an unexecuted check.
|
|
1241
|
-
// A child's output cannot match: bash always prefixes it with Exit code.
|
|
1242
|
-
this.verificationGate.recordRejectedCheck(command, REVIEW_REJECTED_BEFORE_START);
|
|
1243
|
-
delete call.sourceSnapshot;
|
|
1244
|
-
verificationChanged = true;
|
|
1245
|
-
}
|
|
1246
|
-
else if (classification.accepted || classification.snapshotEligible) {
|
|
1247
|
-
// A foreground check that outlived the default budget was moved to
|
|
1248
|
-
// the background, not failed: track it to its real exit the same way.
|
|
1249
|
-
const autoBackgroundId = event.isError ? undefined : autoBackgroundedId(event.result);
|
|
1250
|
-
if ((autoBackgroundId !== undefined || args.run_in_background === true) &&
|
|
1251
|
-
!event.isError &&
|
|
1252
|
-
args.persist !== true) {
|
|
1253
|
-
const id = autoBackgroundId ?? /^ID:\s*(\S+)/m.exec(event.result)?.[1];
|
|
1254
|
-
// No parseable ID means the check cannot be tracked to a real exit
|
|
1255
|
-
// code — no evidence either way. Recording a FAILURE here made
|
|
1256
|
-
// every later green run of a different spelling look owed.
|
|
1257
|
-
if (id)
|
|
1258
|
-
this.backgroundVerification.set(id, {
|
|
1259
|
-
revision: call.revision,
|
|
1260
|
-
command,
|
|
1261
|
-
...(classification.snapshotEligible
|
|
1262
|
-
? { sourceSnapshot: call.sourceSnapshot ?? null }
|
|
1263
|
-
: {}),
|
|
1264
|
-
});
|
|
1265
|
-
else if (classification.snapshotEligible)
|
|
1266
|
-
this.verificationGate.requireFreshVerification(true);
|
|
1267
|
-
}
|
|
1268
|
-
else if (args.persist === true) {
|
|
1269
|
-
// Persistent-shell checks are not bounded evidence (steering can
|
|
1270
|
-
// interleave): neither a pass nor a failure. A recorded failure
|
|
1271
|
-
// here blocked approval for sessions that prefer the shell.
|
|
1272
|
-
}
|
|
1273
|
-
else if (classification.snapshotEligible) {
|
|
1274
|
-
await this.finishSnapshotVerification({ command, revision: call.revision, sourceSnapshot: call.sourceSnapshot ?? null }, !event.isError && /^Exit code:\s*0(?:\s|$)/i.test(event.result.trim()));
|
|
1275
|
-
verificationChanged = true;
|
|
1276
|
-
}
|
|
1277
|
-
else {
|
|
1278
|
-
if (!event.isError && /^Exit code:\s*0(?:\s|$)/i.test(event.result.trim())) {
|
|
1279
|
-
this.verificationGate.recordVerification(call.revision, command);
|
|
1280
|
-
}
|
|
1281
|
-
else {
|
|
1282
|
-
this.verificationGate.recordFailedVerification(command, call.revision);
|
|
1283
|
-
}
|
|
1284
|
-
verificationChanged = true;
|
|
1285
|
-
}
|
|
1286
|
-
delete call.sourceSnapshot;
|
|
1287
|
-
}
|
|
1288
|
-
else if (classification.candidate) {
|
|
1289
|
-
// Green but untrusted: remember WHY so the demand can tell the
|
|
1290
|
-
// agent which command shape actually clears the gate.
|
|
1291
|
-
this.verificationGate.recordRejectedCheck(command, classification.reason);
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
if (!event.isError && args && name === "task_output" && typeof args.id === "string") {
|
|
1295
|
-
verificationChanged =
|
|
1296
|
-
(await this.recordFinishedBackgroundVerification(args.id)) || verificationChanged;
|
|
1297
|
-
}
|
|
963
|
+
this.loopMonitor.recordToolEnd(name, args, event.result, event.isError);
|
|
964
|
+
const verificationChanged = await this.verificationTracker.recordToolCallEnd(event, call, this.planModeRef.current);
|
|
1298
965
|
if (verificationChanged)
|
|
1299
966
|
await this.persistVerificationState();
|
|
1300
967
|
// Tool results are what push the run over the review gate, and they all
|
|
@@ -1304,7 +971,7 @@ export class AgentSession {
|
|
|
1304
971
|
break;
|
|
1305
972
|
}
|
|
1306
973
|
case "turn_end":
|
|
1307
|
-
this.
|
|
974
|
+
this.loopMonitor.recordTurn(event.turn);
|
|
1308
975
|
this.refreshHookArming();
|
|
1309
976
|
for (let index = this.messages.length - 1; index >= 0; index--) {
|
|
1310
977
|
const anchor = this.messages[index];
|
|
@@ -1481,51 +1148,26 @@ export class AgentSession {
|
|
|
1481
1148
|
// Deterministic stuck verdict, computed once and shared: the semantic
|
|
1482
1149
|
// judge must not spend tokens on a burst the deterministic breaker is
|
|
1483
1150
|
// about to correct itself.
|
|
1484
|
-
const deterministicDecision = evaluateLoopBreak(
|
|
1485
|
-
consecutiveFailures: this.hookConsecutiveFailures,
|
|
1486
|
-
repeatedNoProgressCalls: this.hookRepeatedNoProgressCalls,
|
|
1487
|
-
textRepetitionDetected: detectTextRepetition(this.hookText),
|
|
1488
|
-
...(this.hookCyclicPattern ? { cyclicPattern: this.hookCyclicPattern } : {}),
|
|
1489
|
-
});
|
|
1151
|
+
const deterministicDecision = this.loopMonitor.evaluateLoopBreak();
|
|
1490
1152
|
this.maybeStartSemanticLoopCheck(deterministicDecision.shouldBreak);
|
|
1491
1153
|
// Two-stage loop-breaker: stage 1 nudges; a FRESH detection after that
|
|
1492
1154
|
// injects the harsher final stop-and-report prompt. Signals reset after
|
|
1493
1155
|
// each injection so stage 2 only fires on new evidence.
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
this.hookConsecutiveFailures = 0;
|
|
1503
|
-
this.hookRepeatedNoProgressCalls = 0;
|
|
1504
|
-
// The deterministic breaker owns this burst — a semantic verdict from
|
|
1505
|
-
// the same burst must not double-correct on the next poll.
|
|
1506
|
-
this.semanticLoop.verdict = null;
|
|
1507
|
-
// Clear the text buffer too — otherwise a stage-1 text-repetition
|
|
1508
|
-
// trigger still sees the same repeated tail on the next check and
|
|
1509
|
-
// escalates to stage 2 on stale evidence.
|
|
1510
|
-
this.hookText = "";
|
|
1511
|
-
log("INFO", "loop-break", "Injecting loop-break nudge", {
|
|
1512
|
-
stage: String(stage),
|
|
1513
|
-
reasons: decision.reasons.join(", "),
|
|
1514
|
-
});
|
|
1515
|
-
this.eventBus.emit("hook", { kind: "loop_break" });
|
|
1516
|
-
return [buildLoopBreakMessage(decision.reasons, stage === 2)];
|
|
1517
|
-
}
|
|
1156
|
+
const stage = this.loopMonitor.takeLoopBreakStage(deterministicDecision);
|
|
1157
|
+
if (stage !== null) {
|
|
1158
|
+
log("INFO", "loop-break", "Injecting loop-break nudge", {
|
|
1159
|
+
stage: String(stage),
|
|
1160
|
+
reasons: deterministicDecision.reasons.join(", "),
|
|
1161
|
+
});
|
|
1162
|
+
this.eventBus.emit("hook", { kind: "loop_break" });
|
|
1163
|
+
return [buildLoopBreakMessage(deterministicDecision.reasons, stage === 2)];
|
|
1518
1164
|
}
|
|
1519
1165
|
// Semantic loop-break: an LLM verdict (started by maybeStartSemanticLoopCheck
|
|
1520
1166
|
// on a suspicious-but-syntactically-quiet burst) is consumed here, exactly
|
|
1521
1167
|
// once, with the deterministic breaker getting priority above. Fail-open:
|
|
1522
1168
|
// no verdict or no-loop verdict injects nothing.
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
this.semanticLoop.injected = true;
|
|
1526
|
-
this.semanticLoop.verdict = null;
|
|
1527
|
-
// The judged burst has been addressed; a fresh burst must re-accumulate.
|
|
1528
|
-
this.hookConsecutiveFailures = 0;
|
|
1169
|
+
const verdict = this.loopMonitor.takeSemanticVerdict();
|
|
1170
|
+
if (verdict) {
|
|
1529
1171
|
log("INFO", "loop-break", "Injecting semantic loop-break steering", {
|
|
1530
1172
|
reason: verdict.reason,
|
|
1531
1173
|
});
|
|
@@ -1545,34 +1187,26 @@ export class AgentSession {
|
|
|
1545
1187
|
* Fire-and-forget: the call runs while the next turn streams, and a finished
|
|
1546
1188
|
* verdict is consumed by the NEXT steering poll — never blocking a turn. */
|
|
1547
1189
|
maybeStartSemanticLoopCheck(deterministicBreak) {
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
totalFailures: this.hookStats.toolFailures,
|
|
1551
|
-
turns: this.hookStats.turns,
|
|
1552
|
-
lastCheckTurn: this.semanticLoop.lastCheckTurn,
|
|
1553
|
-
checksUsed: this.semanticLoop.checksUsed,
|
|
1554
|
-
checkPending: this.semanticLoop.pending,
|
|
1555
|
-
deterministicBreak,
|
|
1556
|
-
})) {
|
|
1190
|
+
const monitor = this.loopMonitor;
|
|
1191
|
+
if (!monitor.shouldRunSemanticCheck(deterministicBreak)) {
|
|
1557
1192
|
return;
|
|
1558
1193
|
}
|
|
1559
|
-
// resetHookState replaces
|
|
1194
|
+
// resetHookState replaces the run state on every prompt. A late judge must
|
|
1560
1195
|
// not publish a verdict or consume the next run's budget/cooldown.
|
|
1561
|
-
const runState =
|
|
1562
|
-
runState.pending = true;
|
|
1196
|
+
const runState = monitor.beginSemanticCheck();
|
|
1563
1197
|
log("INFO", "loop-break", "Starting semantic loop judge", {
|
|
1564
|
-
turn: String(
|
|
1565
|
-
consecutiveFailures: String(
|
|
1566
|
-
recentCalls: String(
|
|
1198
|
+
turn: String(monitor.stats.turns),
|
|
1199
|
+
consecutiveFailures: String(monitor.consecutiveFailures),
|
|
1200
|
+
recentCalls: String(monitor.recentCalls.length),
|
|
1567
1201
|
});
|
|
1568
1202
|
void (async () => {
|
|
1569
1203
|
try {
|
|
1570
|
-
const prompt = buildSemanticLoopJudgePrompt(
|
|
1204
|
+
const prompt = buildSemanticLoopJudgePrompt(monitor.recentCalls, this.originalRequest);
|
|
1571
1205
|
const raw = await (this.opts.semanticLoopJudge?.(prompt) ??
|
|
1572
1206
|
this.callSemanticLoopJudge(prompt));
|
|
1573
1207
|
const verdict = parseSemanticLoopVerdict(raw);
|
|
1574
|
-
if (
|
|
1575
|
-
runState
|
|
1208
|
+
if (verdict?.loop)
|
|
1209
|
+
monitor.publishSemanticVerdict(runState, verdict);
|
|
1576
1210
|
}
|
|
1577
1211
|
catch (error) {
|
|
1578
1212
|
// Fail open: judge errors never stop a run. Budget and cooldown are
|
|
@@ -1582,11 +1216,7 @@ export class AgentSession {
|
|
|
1582
1216
|
});
|
|
1583
1217
|
}
|
|
1584
1218
|
finally {
|
|
1585
|
-
|
|
1586
|
-
runState.pending = false;
|
|
1587
|
-
runState.checksUsed += 1;
|
|
1588
|
-
runState.lastCheckTurn = this.hookStats.turns;
|
|
1589
|
-
}
|
|
1219
|
+
monitor.settleSemanticCheck(runState);
|
|
1590
1220
|
}
|
|
1591
1221
|
})();
|
|
1592
1222
|
}
|
|
@@ -1682,23 +1312,7 @@ export class AgentSession {
|
|
|
1682
1312
|
* with — so reuse the same stuck signals that drive the loop-breaker.
|
|
1683
1313
|
*/
|
|
1684
1314
|
shouldExtendTurnBudget(ctx) {
|
|
1685
|
-
const refusals =
|
|
1686
|
-
const stuck = evaluateLoopBreak({
|
|
1687
|
-
consecutiveFailures: this.hookConsecutiveFailures,
|
|
1688
|
-
repeatedNoProgressCalls: this.hookRepeatedNoProgressCalls,
|
|
1689
|
-
textRepetitionDetected: detectTextRepetition(this.hookText),
|
|
1690
|
-
...(this.hookCyclicPattern ? { cyclicPattern: this.hookCyclicPattern } : {}),
|
|
1691
|
-
});
|
|
1692
|
-
if (stuck.shouldBreak)
|
|
1693
|
-
refusals.push(...stuck.reasons);
|
|
1694
|
-
// Stage 2 means the loop-breaker already detected spinning twice and told
|
|
1695
|
-
// the agent to stop and report. Do not overrule that with more turns.
|
|
1696
|
-
if (this.loopBreakInjected >= 2)
|
|
1697
|
-
refusals.push("loop-breaker already escalated");
|
|
1698
|
-
const { toolCalls, toolFailures } = this.hookStats;
|
|
1699
|
-
if (toolCalls > 0 && toolFailures / toolCalls > TURN_EXTENSION_MAX_FAILURE_RATIO) {
|
|
1700
|
-
refusals.push(`${toolFailures}/${toolCalls} tool calls failed`);
|
|
1701
|
-
}
|
|
1315
|
+
const refusals = this.loopMonitor.turnExtensionRefusals();
|
|
1702
1316
|
const granted = refusals.length === 0;
|
|
1703
1317
|
log("INFO", "turn-budget", granted ? "Extending turn budget" : "Refusing turn-budget extension", {
|
|
1704
1318
|
turn: String(ctx.turn),
|
|
@@ -1734,11 +1348,7 @@ export class AgentSession {
|
|
|
1734
1348
|
async getHookFollowUpMessages() {
|
|
1735
1349
|
// Exit notifications and task_output refer to the same host process record.
|
|
1736
1350
|
// Do not force an extra polling tool/turn merely to acknowledge a known exit.
|
|
1737
|
-
|
|
1738
|
-
for (const id of this.backgroundVerification.keys()) {
|
|
1739
|
-
backgroundChanged =
|
|
1740
|
-
(await this.recordFinishedBackgroundVerification(id)) || backgroundChanged;
|
|
1741
|
-
}
|
|
1351
|
+
const backgroundChanged = await this.verificationTracker.settleFinishedBackground();
|
|
1742
1352
|
if (backgroundChanged)
|
|
1743
1353
|
await this.persistVerificationState();
|
|
1744
1354
|
// Edits return immediately; only the completion boundary waits for remaining
|
|
@@ -2015,6 +1625,11 @@ export class AgentSession {
|
|
|
2015
1625
|
maxTurnToolResultChars: resolveSessionTurnToolResultCharLimit(this.model, this.provider, accountId),
|
|
2016
1626
|
// Warn when web/MCP output contains instruction-like text (see injection-detect.ts).
|
|
2017
1627
|
transformToolResult: flagUntrustedToolResult,
|
|
1628
|
+
// A deferred built-in called by its advertised name runs directly
|
|
1629
|
+
// instead of costing a turn on "Unknown tool" then tool_search.
|
|
1630
|
+
...(options.disableTools
|
|
1631
|
+
? {}
|
|
1632
|
+
: { resolveTool: (name) => this.promoteDeferredBuiltin(name) }),
|
|
2018
1633
|
// Self-correction hooks (same as the TUI): loop-break + re-grounding are
|
|
2019
1634
|
// polled mid-loop; the ideal review is polled when the agent would stop.
|
|
2020
1635
|
getSteeringMessages: () => this.getHookSteeringMessages(),
|
|
@@ -2336,11 +1951,12 @@ export class AgentSession {
|
|
|
2336
1951
|
// there avoids tearing down a live stdio child and
|
|
2337
1952
|
// gambling on a `npx` re-spawn that could fail and drop the tools.
|
|
2338
1953
|
const glmInvolved = this.provider === "glm" || prevProvider === "glm";
|
|
2339
|
-
|
|
1954
|
+
const mcpManager = this.mcp.manager;
|
|
1955
|
+
if (mcpManager && glmInvolved) {
|
|
2340
1956
|
// Remove old MCP tools
|
|
2341
1957
|
this.tools = this.tools.filter((t) => !t.name.startsWith("mcp__"));
|
|
2342
1958
|
// Disconnect old MCP servers
|
|
2343
|
-
await
|
|
1959
|
+
await mcpManager.dispose();
|
|
2344
1960
|
// Connect new MCP servers for the new provider
|
|
2345
1961
|
try {
|
|
2346
1962
|
let apiKey;
|
|
@@ -2357,13 +1973,11 @@ export class AgentSession {
|
|
|
2357
1973
|
const servers = await getAllMcpServers(this.provider, apiKey, this.cwd, {
|
|
2358
1974
|
allowProjectScope: this.settingsManager.isProjectTrusted(this.cwd),
|
|
2359
1975
|
});
|
|
2360
|
-
const mcpTools = await
|
|
1976
|
+
const mcpTools = await mcpManager.connectAll(servers);
|
|
2361
1977
|
// Drop stale MCP tools from both the live set and deferred catalog before
|
|
2362
1978
|
// re-adding. Some tools may already have been promoted out of the catalog.
|
|
2363
1979
|
this.tools = this.tools.filter((t) => !t.name.startsWith("mcp__"));
|
|
2364
|
-
this.
|
|
2365
|
-
this.liveMcpTools.clear();
|
|
2366
|
-
this.cachedMcpToolServers.clear();
|
|
1980
|
+
this.mcp.clearMcpTools();
|
|
2367
1981
|
this.addMcpTools(mcpTools);
|
|
2368
1982
|
}
|
|
2369
1983
|
catch (err) {
|
|
@@ -2406,10 +2020,7 @@ export class AgentSession {
|
|
|
2406
2020
|
this.checkpointGeneration = loaded.header.generation ?? 0;
|
|
2407
2021
|
this.currentLeafId = loaded.header.leafId;
|
|
2408
2022
|
this.setSessionPath(loaded.path);
|
|
2409
|
-
this.
|
|
2410
|
-
this.autopilotMarkers = this.sessionManager.getAutopilotMarkers(loaded.entries, loaded.header.leafId);
|
|
2411
|
-
this.appMarkers = this.sessionManager.getAppMarkers(loaded.entries, loaded.header.leafId);
|
|
2412
|
-
this.turnMetrics = this.sessionManager.getTurnMetrics(loaded.entries);
|
|
2023
|
+
this.markers.restore(loaded.entries, loaded.header.leafId);
|
|
2413
2024
|
this.lastPersistedIndex = this.messages.length;
|
|
2414
2025
|
this.providerContext = null;
|
|
2415
2026
|
await this.subAgentManager?.rebindParentSession(this.sessionId);
|
|
@@ -2449,10 +2060,7 @@ export class AgentSession {
|
|
|
2449
2060
|
await this.persistMessage(message);
|
|
2450
2061
|
}
|
|
2451
2062
|
this.lastPersistedIndex = this.messages.length;
|
|
2452
|
-
await this.
|
|
2453
|
-
await this.rePersistKenTurns();
|
|
2454
|
-
await this.rePersistAutopilotMarkers();
|
|
2455
|
-
await this.rePersistAppMarkers();
|
|
2063
|
+
await this.markers.rePersistAll();
|
|
2456
2064
|
await this.persistVerificationState();
|
|
2457
2065
|
await this.persistAppMarker("compaction", {
|
|
2458
2066
|
originalCount: result.originalCount,
|
|
@@ -2821,7 +2429,7 @@ export class AgentSession {
|
|
|
2821
2429
|
// explicit new sessions reset the conversation identity.
|
|
2822
2430
|
if (!preserveConversation) {
|
|
2823
2431
|
this.verificationGate.reset();
|
|
2824
|
-
this.
|
|
2432
|
+
this.verificationTracker.clearBackground();
|
|
2825
2433
|
this.conversationId = "";
|
|
2826
2434
|
this.checkpointGeneration = 0;
|
|
2827
2435
|
this.sessionPreview = "";
|
|
@@ -2833,10 +2441,7 @@ export class AgentSession {
|
|
|
2833
2441
|
// turns / autopilot verdicts / app markers linger in memory, show up in the
|
|
2834
2442
|
// new session's /history, and get re-persisted into the new file by the
|
|
2835
2443
|
// next compaction — the cross-session duplicate-marker propagation bug.
|
|
2836
|
-
this.
|
|
2837
|
-
this.autopilotMarkers = [];
|
|
2838
|
-
this.appMarkers = [];
|
|
2839
|
-
this.turnMetrics = [];
|
|
2444
|
+
this.markers.clear();
|
|
2840
2445
|
const basePrompt = await this.buildBasePrompt(false, undefined);
|
|
2841
2446
|
this.baseSystemPrompt = basePrompt;
|
|
2842
2447
|
this.messages = [{ role: "system", content: this.withSystemPromptTail(basePrompt) }];
|
|
@@ -2958,9 +2563,7 @@ export class AgentSession {
|
|
|
2958
2563
|
else {
|
|
2959
2564
|
used = calculateActiveContextTokens(this.messages);
|
|
2960
2565
|
}
|
|
2961
|
-
const costUsd = this.
|
|
2962
|
-
? this.turnMetrics.reduce((sum, m) => sum + (m.cost.status === "known" ? m.cost.usd : 0), 0)
|
|
2963
|
-
: undefined;
|
|
2566
|
+
const costUsd = this.markers.knownCostUsd();
|
|
2964
2567
|
return costUsd === undefined ? { used, size } : { used, size, costUsd };
|
|
2965
2568
|
}
|
|
2966
2569
|
/**
|
|
@@ -3084,6 +2687,11 @@ export class AgentSession {
|
|
|
3084
2687
|
*/
|
|
3085
2688
|
async setPlanMode(active) {
|
|
3086
2689
|
this.planModeRef.current = active;
|
|
2690
|
+
// Plan mode always ends with `exit_plan`; a deferred one must be callable
|
|
2691
|
+
// without a tool_search turn. Append-only, like any promotion.
|
|
2692
|
+
if (active && !this.tools.some((t) => t.name === "exit_plan")) {
|
|
2693
|
+
this.tools.push(...this.mcp.promote(["exit_plan"]));
|
|
2694
|
+
}
|
|
3087
2695
|
// Entering plan mode discards any prior approved-plan contract (a new plan
|
|
3088
2696
|
// is about to be drafted); exiting keeps it (set explicitly via accept).
|
|
3089
2697
|
if (active)
|
|
@@ -3287,7 +2895,10 @@ export class AgentSession {
|
|
|
3287
2895
|
orchestrationPolicyTail() {
|
|
3288
2896
|
if (this.opts.orchestrationPrompt === false)
|
|
3289
2897
|
return "";
|
|
3290
|
-
return applyAsyncSubagentPolicy("", this.provider, this.model, this.thinkingLevel,
|
|
2898
|
+
return applyAsyncSubagentPolicy("", this.provider, this.model, this.thinkingLevel,
|
|
2899
|
+
// Deferred spawn_agent/wait_agent are one call away (resolveTool loads
|
|
2900
|
+
// them on first use), so the policy must still see them.
|
|
2901
|
+
[...this.tools.map((tool) => tool.name), ...this.deferredBuiltinToolNames]).trim();
|
|
3291
2902
|
}
|
|
3292
2903
|
/**
|
|
3293
2904
|
* Re-render the uncached tail of messages[0] in place. The cached prefix is
|
|
@@ -3309,12 +2920,7 @@ export class AgentSession {
|
|
|
3309
2920
|
return this.messages;
|
|
3310
2921
|
}
|
|
3311
2922
|
getTurnMetrics() {
|
|
3312
|
-
return this.
|
|
3313
|
-
...metric,
|
|
3314
|
-
usage: { ...metric.usage },
|
|
3315
|
-
timing: { ...metric.timing },
|
|
3316
|
-
cost: { ...metric.cost },
|
|
3317
|
-
}));
|
|
2923
|
+
return this.markers.getTurnMetrics();
|
|
3318
2924
|
}
|
|
3319
2925
|
async persistTurnMetric(event) {
|
|
3320
2926
|
if (event.stopReason === "error")
|
|
@@ -3341,31 +2947,23 @@ export class AgentSession {
|
|
|
3341
2947
|
reason: "No authoritative effective-dated provider pricing is available",
|
|
3342
2948
|
},
|
|
3343
2949
|
};
|
|
3344
|
-
this.
|
|
2950
|
+
this.markers.addTurnMetric(payload);
|
|
3345
2951
|
// Internal diagnostics piggyback on the authoritative per-turn metric —
|
|
3346
2952
|
// one source of truth, and the record flushes to disk every turn.
|
|
3347
2953
|
this.diagnosticsRecorder?.recordTurnMetric(payload);
|
|
3348
|
-
|
|
3349
|
-
await this.sessionManager.appendTurnMetric(this.sessionPath, payload);
|
|
3350
|
-
}
|
|
3351
|
-
async rePersistTurnMetrics() {
|
|
3352
|
-
if (!this.sessionPath)
|
|
3353
|
-
return;
|
|
3354
|
-
for (const metric of this.turnMetrics) {
|
|
3355
|
-
await this.sessionManager.appendTurnMetric(this.sessionPath, metric);
|
|
3356
|
-
}
|
|
2954
|
+
await this.markers.persistTurnMetric(payload);
|
|
3357
2955
|
}
|
|
3358
2956
|
/** Ken Kai (mentor) turns recorded against this session, in record order. Used
|
|
3359
2957
|
* by the host to interleave Ken's advisory exchanges back into the transcript
|
|
3360
2958
|
* on resume. Never part of the LLM message history. */
|
|
3361
2959
|
getKenTurns() {
|
|
3362
|
-
return this.
|
|
2960
|
+
return this.markers.getKenTurns();
|
|
3363
2961
|
}
|
|
3364
2962
|
/** Autopilot verdict markers recorded against this session, in record order.
|
|
3365
2963
|
* Used by the host to interleave the auto-review loop's markers back into
|
|
3366
2964
|
* the transcript on resume, mirroring `getKenTurns`. */
|
|
3367
2965
|
getAutopilotMarkers() {
|
|
3368
|
-
return this.
|
|
2966
|
+
return this.markers.getAutopilotMarkers();
|
|
3369
2967
|
}
|
|
3370
2968
|
/** Non-system messages that are actually on disk. Transcript markers anchor
|
|
3371
2969
|
* against this (not the in-memory list, which can run ahead after a failed
|
|
@@ -3380,15 +2978,7 @@ export class AgentSession {
|
|
|
3380
2978
|
* file, so the new file carries positions that match its own transcript.
|
|
3381
2979
|
*/
|
|
3382
2980
|
remapMarkerAnchors(remap) {
|
|
3383
|
-
|
|
3384
|
-
return;
|
|
3385
|
-
const move = (payload) => ({
|
|
3386
|
-
...payload,
|
|
3387
|
-
afterMessageCount: remapAnchorForCompaction(payload.afterMessageCount, remap),
|
|
3388
|
-
});
|
|
3389
|
-
this.kenTurns = this.kenTurns.map(move);
|
|
3390
|
-
this.autopilotMarkers = this.autopilotMarkers.map(move);
|
|
3391
|
-
this.appMarkers = this.appMarkers.map(move);
|
|
2981
|
+
this.markers.remapAnchors(remap);
|
|
3392
2982
|
}
|
|
3393
2983
|
/**
|
|
3394
2984
|
* Record one Ken Kai (mentor agent) turn against this build session: the
|
|
@@ -3401,39 +2991,7 @@ export class AgentSession {
|
|
|
3401
2991
|
* appendEntry's own handling.
|
|
3402
2992
|
*/
|
|
3403
2993
|
async persistKenTurn(question, reply) {
|
|
3404
|
-
|
|
3405
|
-
const payload = { version: 1, question, reply, afterMessageCount };
|
|
3406
|
-
this.kenTurns.push(payload);
|
|
3407
|
-
if (!this.sessionPath)
|
|
3408
|
-
return;
|
|
3409
|
-
const entry = {
|
|
3410
|
-
type: "custom",
|
|
3411
|
-
kind: KEN_TURN_CUSTOM_KIND,
|
|
3412
|
-
id: crypto.randomUUID(),
|
|
3413
|
-
parentId: null,
|
|
3414
|
-
timestamp: new Date().toISOString(),
|
|
3415
|
-
data: payload,
|
|
3416
|
-
};
|
|
3417
|
-
await this.sessionManager.appendEntry(this.sessionPath, entry);
|
|
3418
|
-
}
|
|
3419
|
-
/** Re-append the in-memory Ken turns to the current session file. Called after
|
|
3420
|
-
* a continuation/compaction file is created so Ken's advisory history isn't
|
|
3421
|
-
* lost when the session is rewritten (those rewrites only re-persist
|
|
3422
|
-
* messages). Each turn keeps its original `afterMessageCount` anchor. */
|
|
3423
|
-
async rePersistKenTurns() {
|
|
3424
|
-
if (!this.sessionPath)
|
|
3425
|
-
return;
|
|
3426
|
-
for (const payload of this.kenTurns) {
|
|
3427
|
-
const entry = {
|
|
3428
|
-
type: "custom",
|
|
3429
|
-
kind: KEN_TURN_CUSTOM_KIND,
|
|
3430
|
-
id: crypto.randomUUID(),
|
|
3431
|
-
parentId: null,
|
|
3432
|
-
timestamp: new Date().toISOString(),
|
|
3433
|
-
data: stripRecordedPosition(payload),
|
|
3434
|
-
};
|
|
3435
|
-
await this.sessionManager.appendEntry(this.sessionPath, entry);
|
|
3436
|
-
}
|
|
2994
|
+
await this.markers.recordKenTurn(question, reply, this.persistedTranscriptCount());
|
|
3437
2995
|
}
|
|
3438
2996
|
/**
|
|
3439
2997
|
* Record one autopilot verdict marker (prompted / done / human / capped)
|
|
@@ -3443,23 +3001,6 @@ export class AgentSession {
|
|
|
3443
3001
|
* instead of dropping the marker or falling back to a raw verdict string.
|
|
3444
3002
|
* No-op persistence for transient sessions (kept in memory only).
|
|
3445
3003
|
*/
|
|
3446
|
-
async recordFinishedBackgroundVerification(id) {
|
|
3447
|
-
const started = this.backgroundVerification.get(id);
|
|
3448
|
-
const proc = this.processManager?.list().find((entry) => entry.id === id);
|
|
3449
|
-
if (!started || !proc || proc.exitCode === null)
|
|
3450
|
-
return false;
|
|
3451
|
-
if (started.sourceSnapshot !== undefined) {
|
|
3452
|
-
await this.finishSnapshotVerification(started, proc.exitCode === 0);
|
|
3453
|
-
}
|
|
3454
|
-
else if (proc.exitCode === 0) {
|
|
3455
|
-
this.verificationGate.recordVerification(started.revision, started.command);
|
|
3456
|
-
}
|
|
3457
|
-
else {
|
|
3458
|
-
this.verificationGate.recordFailedVerification(started.command, started.revision);
|
|
3459
|
-
}
|
|
3460
|
-
this.backgroundVerification.delete(id);
|
|
3461
|
-
return true;
|
|
3462
|
-
}
|
|
3463
3004
|
getVerificationEvidence() {
|
|
3464
3005
|
return this.verificationGate.evidence();
|
|
3465
3006
|
}
|
|
@@ -3471,39 +3012,8 @@ export class AgentSession {
|
|
|
3471
3012
|
evidence: this.verificationGate.evidence("run"),
|
|
3472
3013
|
};
|
|
3473
3014
|
}
|
|
3474
|
-
async finishSnapshotVerification(check, passed) {
|
|
3475
|
-
const after = check.sourceSnapshot
|
|
3476
|
-
? await captureVerificationSnapshot(this.opts.cwd, [...this.hookFileEditCounts.keys()])
|
|
3477
|
-
: null;
|
|
3478
|
-
if (after === null || after !== check.sourceSnapshot) {
|
|
3479
|
-
this.verificationGate.requireFreshVerification(true);
|
|
3480
|
-
this.verificationGate.recordRejectedCheck(check.command, after === null
|
|
3481
|
-
? "Workspace inputs could not be compared; run a read-only check after the command"
|
|
3482
|
-
: "Command changed workspace inputs; run checks against the changed source");
|
|
3483
|
-
if (!passed)
|
|
3484
|
-
this.verificationGate.recordFailedVerification(check.command);
|
|
3485
|
-
}
|
|
3486
|
-
else if (passed) {
|
|
3487
|
-
const classification = classifyVerificationCommand(check.command);
|
|
3488
|
-
if (classification.snapshotPreserveOnly) {
|
|
3489
|
-
this.verificationGate.recordRejectedCheck(check.command, classification.reason);
|
|
3490
|
-
}
|
|
3491
|
-
else {
|
|
3492
|
-
this.verificationGate.recordVerification(check.revision, check.command);
|
|
3493
|
-
}
|
|
3494
|
-
}
|
|
3495
|
-
else {
|
|
3496
|
-
this.verificationGate.recordFailedVerification(check.command, check.revision);
|
|
3497
|
-
}
|
|
3498
|
-
}
|
|
3499
3015
|
getVerificationProblem() {
|
|
3500
|
-
|
|
3501
|
-
return "Unverified: a build is still running or its workspace comparison is pending.";
|
|
3502
|
-
}
|
|
3503
|
-
return (this.verificationGate.verificationProblem() ??
|
|
3504
|
-
(this.backgroundVerification.size > 0
|
|
3505
|
-
? "Unverified: a background check is still running or its result has not been confirmed."
|
|
3506
|
-
: null));
|
|
3016
|
+
return this.verificationTracker.verificationProblem();
|
|
3507
3017
|
}
|
|
3508
3018
|
async persistCompletionReviewState() {
|
|
3509
3019
|
if (!this.sessionPath || !this.opts.completionReview)
|
|
@@ -3536,50 +3046,13 @@ export class AgentSession {
|
|
|
3536
3046
|
await this.sessionManager.appendEntry(this.sessionPath, entry);
|
|
3537
3047
|
}
|
|
3538
3048
|
async persistAutopilotMarker(phase, extra) {
|
|
3539
|
-
|
|
3540
|
-
const payload = {
|
|
3541
|
-
version: 1,
|
|
3542
|
-
phase,
|
|
3543
|
-
afterMessageCount,
|
|
3544
|
-
...(extra?.reason !== undefined ? { reason: extra.reason } : {}),
|
|
3545
|
-
...(extra?.body !== undefined ? { body: extra.body } : {}),
|
|
3546
|
-
};
|
|
3547
|
-
this.autopilotMarkers.push(payload);
|
|
3548
|
-
if (!this.sessionPath)
|
|
3549
|
-
return;
|
|
3550
|
-
const entry = {
|
|
3551
|
-
type: "custom",
|
|
3552
|
-
kind: AUTOPILOT_MARKER_CUSTOM_KIND,
|
|
3553
|
-
id: crypto.randomUUID(),
|
|
3554
|
-
parentId: null,
|
|
3555
|
-
timestamp: new Date().toISOString(),
|
|
3556
|
-
data: payload,
|
|
3557
|
-
};
|
|
3558
|
-
await this.sessionManager.appendEntry(this.sessionPath, entry);
|
|
3559
|
-
}
|
|
3560
|
-
/** Re-append the in-memory autopilot markers to the current session file.
|
|
3561
|
-
* Mirrors `rePersistKenTurns` — called after a continuation/compaction file
|
|
3562
|
-
* is created so the auto-review history survives the rewrite. */
|
|
3563
|
-
async rePersistAutopilotMarkers() {
|
|
3564
|
-
if (!this.sessionPath)
|
|
3565
|
-
return;
|
|
3566
|
-
for (const payload of this.autopilotMarkers) {
|
|
3567
|
-
const entry = {
|
|
3568
|
-
type: "custom",
|
|
3569
|
-
kind: AUTOPILOT_MARKER_CUSTOM_KIND,
|
|
3570
|
-
id: crypto.randomUUID(),
|
|
3571
|
-
parentId: null,
|
|
3572
|
-
timestamp: new Date().toISOString(),
|
|
3573
|
-
data: stripRecordedPosition(payload),
|
|
3574
|
-
};
|
|
3575
|
-
await this.sessionManager.appendEntry(this.sessionPath, entry);
|
|
3576
|
-
}
|
|
3049
|
+
await this.markers.recordAutopilotMarker(phase, extra, this.persistedTranscriptCount());
|
|
3577
3050
|
}
|
|
3578
3051
|
/** App transcript markers recorded against this session, in record order.
|
|
3579
3052
|
* Used by the host to interleave display-only rows (plan banner, task
|
|
3580
3053
|
* header, errors, user-bubble hints) back into the transcript on resume. */
|
|
3581
3054
|
getAppMarkers() {
|
|
3582
|
-
return this.
|
|
3055
|
+
return this.markers.getAppMarkers();
|
|
3583
3056
|
}
|
|
3584
3057
|
/**
|
|
3585
3058
|
* Record one app transcript marker (display-only row) against this session.
|
|
@@ -3591,20 +3064,7 @@ export class AgentSession {
|
|
|
3591
3064
|
* No-op persistence for transient sessions.
|
|
3592
3065
|
*/
|
|
3593
3066
|
async persistAppMarker(kind, data, anchorOffset = 0) {
|
|
3594
|
-
|
|
3595
|
-
const payload = { version: 1, kind, afterMessageCount, data };
|
|
3596
|
-
this.appMarkers.push(payload);
|
|
3597
|
-
if (!this.sessionPath)
|
|
3598
|
-
return;
|
|
3599
|
-
const entry = {
|
|
3600
|
-
type: "custom",
|
|
3601
|
-
kind: APP_MARKER_CUSTOM_KIND,
|
|
3602
|
-
id: crypto.randomUUID(),
|
|
3603
|
-
parentId: null,
|
|
3604
|
-
timestamp: new Date().toISOString(),
|
|
3605
|
-
data: payload,
|
|
3606
|
-
};
|
|
3607
|
-
await this.sessionManager.appendEntry(this.sessionPath, entry);
|
|
3067
|
+
await this.markers.recordAppMarker(kind, data, this.persistedTranscriptCount() + anchorOffset);
|
|
3608
3068
|
}
|
|
3609
3069
|
/**
|
|
3610
3070
|
* Open the run journal for one `RunLifecycle` generation.
|
|
@@ -3633,24 +3093,6 @@ export class AgentSession {
|
|
|
3633
3093
|
outcome,
|
|
3634
3094
|
});
|
|
3635
3095
|
}
|
|
3636
|
-
/** Re-append the in-memory app markers to the current session file. Mirrors
|
|
3637
|
-
* `rePersistKenTurns` — called after a continuation/compaction file is
|
|
3638
|
-
* created so display-only rows survive the rewrite. */
|
|
3639
|
-
async rePersistAppMarkers() {
|
|
3640
|
-
if (!this.sessionPath)
|
|
3641
|
-
return;
|
|
3642
|
-
for (const payload of this.appMarkers) {
|
|
3643
|
-
const entry = {
|
|
3644
|
-
type: "custom",
|
|
3645
|
-
kind: APP_MARKER_CUSTOM_KIND,
|
|
3646
|
-
id: crypto.randomUUID(),
|
|
3647
|
-
parentId: null,
|
|
3648
|
-
timestamp: new Date().toISOString(),
|
|
3649
|
-
data: stripRecordedPosition(payload),
|
|
3650
|
-
};
|
|
3651
|
-
await this.sessionManager.appendEntry(this.sessionPath, entry);
|
|
3652
|
-
}
|
|
3653
|
-
}
|
|
3654
3096
|
/**
|
|
3655
3097
|
* Rewrite a draft prompt into a tighter, terminology-correct version using
|
|
3656
3098
|
* the ACTIVE provider/model. A stateless one-off LLM call (no agent loop, no
|
|
@@ -3892,7 +3334,7 @@ export class AgentSession {
|
|
|
3892
3334
|
this.stopBackgroundProcesses();
|
|
3893
3335
|
this.lspManager?.shutdownAll();
|
|
3894
3336
|
this.debugManager?.shutdown();
|
|
3895
|
-
await Promise.all([this.subAgentManager?.shutdownAll(), this.
|
|
3337
|
+
await Promise.all([this.subAgentManager?.shutdownAll(), this.mcp.manager?.dispose()]);
|
|
3896
3338
|
await this.extensionLoader.deactivateAll();
|
|
3897
3339
|
this.setSessionPath("");
|
|
3898
3340
|
this.eventBus.removeAllListeners();
|
|
@@ -3937,7 +3379,7 @@ export class AgentSession {
|
|
|
3937
3379
|
.reverse()
|
|
3938
3380
|
.find((entry) => entry.type === "custom" && entry.kind === COMPLETION_REVIEW_STATE_KIND);
|
|
3939
3381
|
this.opts.completionReview?.restore(savedCompletionReview?.type === "custom" ? savedCompletionReview.data : null);
|
|
3940
|
-
this.
|
|
3382
|
+
this.verificationTracker.clearBackground();
|
|
3941
3383
|
const savedVerification = [...loaded.entries]
|
|
3942
3384
|
.reverse()
|
|
3943
3385
|
.find((entry) => entry.type === "custom" && entry.kind === VERIFICATION_STATE_KIND);
|
|
@@ -3969,12 +3411,8 @@ export class AgentSession {
|
|
|
3969
3411
|
// The leaf is passed so each marker also carries its FILE-order position,
|
|
3970
3412
|
// the fallback used when a legacy anchor is out of range (see
|
|
3971
3413
|
// RecordedPosition).
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
this.autopilotMarkers = this.sessionManager.getAutopilotMarkers(loaded.entries, loaded.header.leafId);
|
|
3975
|
-
// Restore app transcript markers (plan banner / task header / errors / hints).
|
|
3976
|
-
this.appMarkers = this.sessionManager.getAppMarkers(loaded.entries, loaded.header.leafId);
|
|
3977
|
-
this.turnMetrics = this.sessionManager.getTurnMetrics(loaded.entries);
|
|
3414
|
+
// Autopilot verdicts, app markers and turn metrics are restored the same way.
|
|
3415
|
+
this.markers.restore(loaded.entries, loaded.header.leafId);
|
|
3978
3416
|
// A run that opened the journal and never closed it died mid-flight. Read
|
|
3979
3417
|
// it here, before anything rewrites the file, and report it once the
|
|
3980
3418
|
// transcript is in place.
|
|
@@ -4174,13 +3612,4 @@ export class AgentSession {
|
|
|
4174
3612
|
}
|
|
4175
3613
|
}
|
|
4176
3614
|
}
|
|
4177
|
-
/** Expand a leading `~` so `/import ~/.codex/...` works from any shell. */
|
|
4178
|
-
function resolveHomePath(filePath) {
|
|
4179
|
-
if (filePath === "~")
|
|
4180
|
-
return os.homedir();
|
|
4181
|
-
if (filePath.startsWith("~/") || filePath.startsWith("~\\")) {
|
|
4182
|
-
return path.join(os.homedir(), filePath.slice(2));
|
|
4183
|
-
}
|
|
4184
|
-
return filePath;
|
|
4185
|
-
}
|
|
4186
3615
|
//# sourceMappingURL=agent-session.js.map
|