@kenkaiiii/ggcoder 5.74.2 → 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 +482 -3695
- 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 +27 -87
- package/dist/core/agent-session.js +166 -709
- 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/autopilot-cycle.d.ts +6 -2
- package/dist/core/autopilot-cycle.js +1 -1
- package/dist/core/autopilot-cycle.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/plan-handoff.d.ts +30 -0
- package/dist/core/plan-handoff.js +10 -0
- package/dist/core/plan-handoff.js.map +1 -0
- package/dist/core/plan-submission-gate.d.ts +28 -0
- package/dist/core/plan-submission-gate.js +27 -0
- package/dist/core/plan-submission-gate.js.map +1 -0
- 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,30 +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";
|
|
28
|
+
import { PLAN_SUBMISSION_NUDGE, shouldNudgePlanSubmission } from "./plan-submission-gate.js";
|
|
29
29
|
import { buildSubAgentCompletionFollowUp } from "./subagent-manager.js";
|
|
30
30
|
import { applyAsyncSubagentPolicy } from "./subagent-policy.js";
|
|
31
31
|
import { resolveResponsesLite, resolveStrictTools, } from "./codex-request-shape.js";
|
|
32
32
|
import { subAgentDescription } from "../tools/subagent.js";
|
|
33
|
-
import {
|
|
34
|
-
import { MCPClientManager, getAllMcpServers } from "./mcp/index.js";
|
|
35
|
-
import { clampMcpToolDescription, DeferredToolCatalog } from "./mcp/deferred-catalog.js";
|
|
33
|
+
import { getAllMcpServers } from "./mcp/index.js";
|
|
36
34
|
import { CONTEXT_LIMITS, resolveSessionContextLimits, } from "./context-limits.js";
|
|
37
|
-
import { McpCatalogCache } from "./mcp/catalog-cache.js";
|
|
38
35
|
import { describeDropped, importForeignSession, } from "./foreign-session-import.js";
|
|
39
|
-
import { createToolSearchTool } from "../tools/tool-search.js";
|
|
40
36
|
import { createSessionStatsTool } from "../tools/session-stats.js";
|
|
41
37
|
import { createDiagnoseCommand, isInternalDiagnosticsEnabled, SessionDiagnosticsRecorder, } from "./internal-diagnostics.js";
|
|
42
38
|
import { log } from "./logger.js";
|
|
@@ -53,15 +49,13 @@ import { pruneStaleToolResults } from "./compaction/tool-result-pruner.js";
|
|
|
53
49
|
import { discoverAgents } from "./agents.js";
|
|
54
50
|
import { enhancePrompt } from "../utils/prompt-enhancer.js";
|
|
55
51
|
import { detectLanguages, detectProjectStack } from "./language-detector.js";
|
|
56
|
-
import {
|
|
52
|
+
import { buildLoopBreakMessage } from "./loop-breaker.js";
|
|
57
53
|
import { buildRegroundingMessage, requestTextForRegrounding } from "./regrounding.js";
|
|
58
|
-
import { buildSemanticLoopJudgePrompt, buildSemanticLoopMessage,
|
|
54
|
+
import { buildSemanticLoopJudgePrompt, buildSemanticLoopMessage, parseSemanticLoopVerdict, SEMANTIC_LOOP_JUDGE_TIMEOUT_MS, withJudgeTimeout, } from "./semantic-loop-check.js";
|
|
59
55
|
import { buildEnvDeltaMessage } from "./env-delta.js";
|
|
60
56
|
import { wrapSteeringText, buildNotificationSteeringText, STEERING_PREFIX } from "./steering.js";
|
|
61
57
|
import { AgentNotificationQueue } from "./agent-notifications.js";
|
|
62
|
-
import { VerificationGate,
|
|
63
|
-
import { classifyVerificationCommand } from "./verification-evidence.js";
|
|
64
|
-
import { captureVerificationSnapshot } from "./verification-snapshot.js";
|
|
58
|
+
import { VerificationGate, VERIFICATION_STATE_KIND } from "./verification-gate.js";
|
|
65
59
|
import { findUserSessionPrompt, getUserSessionPrompt } from "./session-preview.js";
|
|
66
60
|
import { normalizeMessageImages } from "./message-images.js";
|
|
67
61
|
import { loadStreamRules } from "./stream-rules.js";
|
|
@@ -69,41 +63,12 @@ import crypto from "node:crypto";
|
|
|
69
63
|
import fs from "node:fs/promises";
|
|
70
64
|
import os from "node:os";
|
|
71
65
|
import path from "node:path";
|
|
72
|
-
import {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
// ── Tool-result policy ─────────────────────────────────────
|
|
79
|
-
/** Resolve the per-result cap passed to the agent loop for the active transport. */
|
|
80
|
-
export function resolveSessionToolResultCharLimit(model, provider, accountId) {
|
|
81
|
-
return (getToolResultCharLimit(model, { provider, accountId }) ??
|
|
82
|
-
Math.floor(getContextWindow(model, { provider, accountId }) * 3.5 * 0.3));
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Aggregate budget for ALL tool results produced in one assistant turn.
|
|
86
|
-
* Individual results are already capped, but wide parallel fan-outs (GPT-5.6's
|
|
87
|
-
* signature behavior) were observed injecting 100k+ uncached tokens in a single
|
|
88
|
-
* turn. ~15% of the context window in chars (1 token ≈ 3.5 chars), floored at
|
|
89
|
-
* 100KB so small windows still fit two full-size reads, ceilinged at 240KB so
|
|
90
|
-
* 1M-context models don't waive the budget entirely.
|
|
91
|
-
*/
|
|
92
|
-
export function resolveSessionTurnToolResultCharLimit(model, provider, accountId) {
|
|
93
|
-
const contextChars = getContextWindow(model, { provider, accountId }) * 3.5;
|
|
94
|
-
return Math.max(100_000, Math.min(Math.floor(contextChars * 0.15), 240_000));
|
|
95
|
-
}
|
|
96
|
-
/** Marker the compactor prepends to the summary message it injects. */
|
|
97
|
-
/**
|
|
98
|
-
* True when an assistant message ends a turn with tool calls still awaiting
|
|
99
|
-
* their results. Inserting a user message there would orphan the tool_use
|
|
100
|
-
* blocks and the provider rejects the next request.
|
|
101
|
-
*/
|
|
102
|
-
function hasUnresolvedToolCalls(message) {
|
|
103
|
-
if (typeof message.content === "string" || !Array.isArray(message.content))
|
|
104
|
-
return false;
|
|
105
|
-
return message.content.some((part) => part.type === "tool_call");
|
|
106
|
-
}
|
|
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";
|
|
107
72
|
// ── Agent Session ──────────────────────────────────────────
|
|
108
73
|
export class AgentSession {
|
|
109
74
|
eventBus = new EventBus();
|
|
@@ -113,24 +78,10 @@ export class AgentSession {
|
|
|
113
78
|
sessionManager;
|
|
114
79
|
extensionLoader = new ExtensionLoader();
|
|
115
80
|
messages = [];
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
// alongside the session and reloaded on resume
|
|
119
|
-
|
|
120
|
-
// webview can interleave them chronologically.
|
|
121
|
-
kenTurns = [];
|
|
122
|
-
// Autopilot Ken (auto-reviewer) markers recorded against this build session:
|
|
123
|
-
// the review verdict shown in the transcript (prompted / done / human /
|
|
124
|
-
// capped). Same not-on-the-DAG treatment as kenTurns — advisory only,
|
|
125
|
-
// persisted + reloaded so a resumed session shows the identical Ken bubble
|
|
126
|
-
// the live run showed instead of dropping it or replaying a raw verdict.
|
|
127
|
-
autopilotMarkers = [];
|
|
128
|
-
// Generic app transcript markers (plan-mode banner, task header, error rows,
|
|
129
|
-
// user-bubble display hints). Same not-on-the-DAG treatment as kenTurns —
|
|
130
|
-
// display only, persisted + reloaded so a resumed session shows the same
|
|
131
|
-
// transcript rows the live run showed.
|
|
132
|
-
appMarkers = [];
|
|
133
|
-
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);
|
|
134
85
|
cacheDiagnostics = new CacheDiagnostics();
|
|
135
86
|
/** Internal-only (GG_INTERNAL): live per-session cost/reliability recorder.
|
|
136
87
|
* Absent entirely in public builds — see core/internal-diagnostics.ts. */
|
|
@@ -149,25 +100,8 @@ export class AgentSession {
|
|
|
149
100
|
// ── Self-correction hook state (mirrors the TUI's useAgentLoop refs) ──
|
|
150
101
|
// Reset at the start of every run; observed from the event stream; read by
|
|
151
102
|
// the loop-break (mid-loop) callback.
|
|
152
|
-
|
|
153
|
-
hookText = "";
|
|
154
|
-
hookConsecutiveFailures = 0;
|
|
155
|
-
hookRepeatedNoProgressCalls = 0;
|
|
156
|
-
hookProgressTracker = new ToolCallProgressTracker();
|
|
157
|
-
hookCycleDetector = new CycleDetector();
|
|
158
|
-
hookCyclicPattern = null;
|
|
159
|
-
hookFileEditCounts = new Map();
|
|
160
|
-
hookToolCalls = new Map();
|
|
161
|
-
backgroundVerification = new Map();
|
|
162
|
-
/** 0 = none; 1 = first nudge sent; 2 = final stop-and-report injected. */
|
|
163
|
-
loopBreakInjected = 0;
|
|
103
|
+
loopMonitor = new RunLoopMonitor();
|
|
164
104
|
regroundingInjected = false;
|
|
165
|
-
/** Recent tool-call digests for the semantic loop judge — bounded ring. */
|
|
166
|
-
hookRecentCalls = [];
|
|
167
|
-
/** LLM-judged loop detection state. `verdict` holds a LOOP verdict awaiting
|
|
168
|
-
* injection at the next steering poll; judge failures fail open (no
|
|
169
|
-
* injection) and still consume budget + cooldown. */
|
|
170
|
-
semanticLoop = { checksUsed: 0, lastCheckTurn: 0, pending: false, verdict: null, injected: false };
|
|
171
105
|
/**
|
|
172
106
|
* The environment as the cached system prompt currently describes it.
|
|
173
107
|
* Re-recorded on every prompt build, so a rebuild (e.g. `/add-dir`) needs no
|
|
@@ -178,9 +112,13 @@ export class AgentSession {
|
|
|
178
112
|
runStartedAt = 0;
|
|
179
113
|
/** Gate injections spent this run, capped by MAX_PROCESS_GATE_INJECTIONS. */
|
|
180
114
|
processGateInjected = 0;
|
|
115
|
+
/** Plan-submission reminders spent this run (plan-submission-gate.ts). */
|
|
116
|
+
planSubmissionNudges = 0;
|
|
181
117
|
/** Verification evidence: code edited this run, and what has proved it since.
|
|
182
118
|
* Passive tracking only — it feeds run status and autopilot, never a turn. */
|
|
183
119
|
verificationGate = new VerificationGate();
|
|
120
|
+
/** Host-observed evidence (edited files, in-flight and background checks) feeding the gate. */
|
|
121
|
+
verificationTracker;
|
|
184
122
|
/** Mirror of the last `hook_armed` value, so the event fires only on an edge. */
|
|
185
123
|
preFinalArmed = false;
|
|
186
124
|
compactionOccurred = false;
|
|
@@ -246,9 +184,8 @@ export class AgentSession {
|
|
|
246
184
|
this.lspManager?.clearPendingDiagnostics();
|
|
247
185
|
void this.subAgentManager?.interruptAll();
|
|
248
186
|
};
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
mcpCatalog;
|
|
187
|
+
/** MCP client, deferred tool_search catalog and cached-stub reconcile state. */
|
|
188
|
+
mcp = new McpToolRegistry(() => this.tools, () => this.contextLimits);
|
|
252
189
|
/** Resolved prompt-injection byte budgets (contextLimits setting). */
|
|
253
190
|
contextLimits = CONTEXT_LIMITS;
|
|
254
191
|
/**
|
|
@@ -257,11 +194,6 @@ export class AgentSession {
|
|
|
257
194
|
* can discover and promote them; a promoted name drops out of this list.
|
|
258
195
|
*/
|
|
259
196
|
deferredBuiltinToolNames = [];
|
|
260
|
-
/** Live (connected) MCP tools by name — the reconcile target for cached stubs. */
|
|
261
|
-
liveMcpTools = new Map();
|
|
262
|
-
/** Server name for each cached-only tool, so a stub knows what to wait on. */
|
|
263
|
-
cachedMcpToolServers = new Map();
|
|
264
|
-
mcpCatalogCache = new McpCatalogCache();
|
|
265
197
|
provider;
|
|
266
198
|
model;
|
|
267
199
|
cwd;
|
|
@@ -334,6 +266,7 @@ export class AgentSession {
|
|
|
334
266
|
opts;
|
|
335
267
|
constructor(options) {
|
|
336
268
|
this.opts = options;
|
|
269
|
+
this.verificationTracker = new VerificationTracker(this.verificationGate, options.cwd, () => this.processManager?.list() ?? []);
|
|
337
270
|
this.provider = options.provider;
|
|
338
271
|
this.model = options.model;
|
|
339
272
|
this.cwd = options.cwd;
|
|
@@ -435,7 +368,7 @@ export class AgentSession {
|
|
|
435
368
|
authStorage: this.authStorage,
|
|
436
369
|
onFileMutated: (filePath) => {
|
|
437
370
|
const relative = path.relative(this.cwd, filePath) || path.basename(filePath);
|
|
438
|
-
this.
|
|
371
|
+
this.verificationTracker.recordFileMutated(relative);
|
|
439
372
|
},
|
|
440
373
|
// Lazy — sessionId/model/provider can change after createTools() runs, so
|
|
441
374
|
// sub-agent spawns read the current parent state at execution time.
|
|
@@ -482,6 +415,10 @@ export class AgentSession {
|
|
|
482
415
|
// every request. Allow-listed sessions keep the eager path — their fixed
|
|
483
416
|
// tool expectations predate the catalog, and tool_search isn't allow-listed.
|
|
484
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();
|
|
485
422
|
const { core, deferred } = partitionToolsByTier(this.tools);
|
|
486
423
|
if (deferred.length > 0) {
|
|
487
424
|
// Append-only: `core` preserves the original relative order and
|
|
@@ -489,10 +426,7 @@ export class AgentSession {
|
|
|
489
426
|
// sits inside the cached prefix stays byte-stable across turns.
|
|
490
427
|
this.tools = core;
|
|
491
428
|
this.deferredBuiltinToolNames = deferred.map((t) => t.name);
|
|
492
|
-
this.
|
|
493
|
-
this.mcpCatalog.add(deferred);
|
|
494
|
-
this.ensureToolSearchTool();
|
|
495
|
-
this.promoteWaitAgentAfterSpawn();
|
|
429
|
+
this.mcp.addDeferred(deferred);
|
|
496
430
|
}
|
|
497
431
|
}
|
|
498
432
|
this.rebuildReadTool = rebuildReadTool;
|
|
@@ -510,8 +444,7 @@ export class AgentSession {
|
|
|
510
444
|
// its listening handshake until this resolves), `backgroundMcpConnect`
|
|
511
445
|
// moves the connect off the critical path so the session becomes usable
|
|
512
446
|
// immediately and tools are appended whenever the servers come up.
|
|
513
|
-
this.
|
|
514
|
-
catalogCache: this.mcpCatalogCache,
|
|
447
|
+
this.mcp.createManager({
|
|
515
448
|
modernProtocol: this.settingsManager.get("mcpModernProtocol"),
|
|
516
449
|
onElicit: this.opts.onMcpElicit,
|
|
517
450
|
});
|
|
@@ -660,7 +593,8 @@ export class AgentSession {
|
|
|
660
593
|
* turn, so background-connected servers become available on the next prompt.
|
|
661
594
|
*/
|
|
662
595
|
async connectMcpServers() {
|
|
663
|
-
|
|
596
|
+
const mcpManager = this.mcp.manager;
|
|
597
|
+
if (!mcpManager)
|
|
664
598
|
return;
|
|
665
599
|
// Allow-listed (read-only advisory) sessions enforce a fixed tool set by
|
|
666
600
|
// name. An MCP server is only connected when its name is explicitly
|
|
@@ -696,7 +630,7 @@ export class AgentSession {
|
|
|
696
630
|
// empty catalog and tool_search would answer "the catalog is empty" for
|
|
697
631
|
// capabilities that genuinely exist — a wrong answer, not a slow one.
|
|
698
632
|
await this.seedMcpCatalogFromCache(servers);
|
|
699
|
-
const connected = await
|
|
633
|
+
const connected = await mcpManager.connectAll(servers);
|
|
700
634
|
// Defense-in-depth: even from a whitelisted server, only push tools that
|
|
701
635
|
// pass the allow-list (no-op when there's no allow-list).
|
|
702
636
|
const mcpTools = this.opts.allowedTools
|
|
@@ -735,170 +669,65 @@ export class AgentSession {
|
|
|
735
669
|
* pushed eagerly when the user opted out.
|
|
736
670
|
* Allow-listed sessions (Ken) always get the eager path — their fixed tool
|
|
737
671
|
* expectations predate the catalog, and tool_search isn't allow-listed.
|
|
738
|
-
* Promotion pushes onto the live `this.tools` array the running agent loop
|
|
739
|
-
* re-reads every turn, so promoted tools are callable on the next step.
|
|
740
672
|
*/
|
|
741
673
|
addMcpTools(mcpTools) {
|
|
742
|
-
if (mcpTools.length === 0)
|
|
743
|
-
return;
|
|
744
|
-
for (const tool of mcpTools) {
|
|
745
|
-
this.liveMcpTools.set(tool.name, tool);
|
|
746
|
-
this.cachedMcpToolServers.delete(tool.name);
|
|
747
|
-
}
|
|
748
674
|
const defer = !this.opts.allowedTools && this.settingsManager.get("deferredMcpTools");
|
|
749
|
-
|
|
750
|
-
// Eager path bypasses the catalog, so budget descriptions here too.
|
|
751
|
-
this.replaceOrPushTools(mcpTools.map((tool) => clampMcpToolDescription(tool, this.contextLimits)));
|
|
752
|
-
return;
|
|
753
|
-
}
|
|
754
|
-
this.mcpCatalog ??= new DeferredToolCatalog(this.contextLimits);
|
|
755
|
-
// `add` is name-keyed, so live definitions replace cached stubs in place.
|
|
756
|
-
this.mcpCatalog.add(mcpTools);
|
|
757
|
-
// A stub the model already promoted lives in `this.tools`; swap it for the
|
|
758
|
-
// live tool so later calls dispatch directly instead of through the stub.
|
|
759
|
-
this.replaceLivePromotedTools(mcpTools);
|
|
760
|
-
this.ensureToolSearchTool();
|
|
675
|
+
this.mcp.addMcpTools(mcpTools, defer);
|
|
761
676
|
}
|
|
762
677
|
/**
|
|
763
|
-
*
|
|
764
|
-
*
|
|
765
|
-
*
|
|
766
|
-
*
|
|
767
|
-
*
|
|
768
|
-
*
|
|
769
|
-
*
|
|
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.
|
|
770
686
|
*/
|
|
771
|
-
|
|
772
|
-
this.
|
|
773
|
-
|
|
774
|
-
return;
|
|
775
|
-
this.tools.push(createToolSearchTool(this.mcpCatalog, (promoted) => {
|
|
776
|
-
this.tools.push(...promoted);
|
|
777
|
-
}, async (toolName) => {
|
|
778
|
-
if (this.liveMcpTools.has(toolName))
|
|
779
|
-
return undefined;
|
|
780
|
-
const serverName = this.cachedMcpToolServers.get(toolName);
|
|
781
|
-
if (!serverName)
|
|
782
|
-
return undefined;
|
|
783
|
-
const outcome = (await this.mcpManager?.whenConnected(serverName)) ?? {
|
|
784
|
-
ok: false,
|
|
785
|
-
error: "MCP is disabled for this session",
|
|
786
|
-
};
|
|
787
|
-
return outcome.ok
|
|
788
|
-
? { serverName, ok: true }
|
|
789
|
-
: { serverName, ok: false, error: outcome.error };
|
|
790
|
-
}, 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);
|
|
791
690
|
}
|
|
792
|
-
/**
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
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)
|
|
803
707
|
return;
|
|
804
708
|
this.tools[index] = {
|
|
805
|
-
...
|
|
709
|
+
...tool,
|
|
806
710
|
execute: async (args, context) => {
|
|
807
|
-
const result = await
|
|
808
|
-
if (
|
|
809
|
-
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));
|
|
810
716
|
}
|
|
811
717
|
return result;
|
|
812
718
|
},
|
|
813
719
|
};
|
|
814
720
|
}
|
|
815
|
-
/** Append tools, replacing any same-named entry (cached stub → live tool). */
|
|
816
|
-
replaceOrPushTools(tools) {
|
|
817
|
-
for (const tool of tools) {
|
|
818
|
-
const index = this.tools.findIndex((t) => t.name === tool.name);
|
|
819
|
-
if (index >= 0)
|
|
820
|
-
this.tools[index] = tool;
|
|
821
|
-
else
|
|
822
|
-
this.tools.push(tool);
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
/** Swap already-promoted cached stubs for their live equivalents, in place. */
|
|
826
|
-
replaceLivePromotedTools(tools) {
|
|
827
|
-
for (const tool of tools) {
|
|
828
|
-
const index = this.tools.findIndex((t) => t.name === tool.name);
|
|
829
|
-
if (index >= 0)
|
|
830
|
-
this.tools[index] = tool;
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
721
|
/**
|
|
834
722
|
* Publish cached tool definitions into the deferred catalog so `tool_search`
|
|
835
|
-
* answers correctly on turn 1
|
|
836
|
-
* description and input schema; calling it waits for the live connection and
|
|
837
|
-
* then dispatches against the real client, or returns a clear error when that
|
|
838
|
-
* server ultimately failed. Live tools replace stubs on connect.
|
|
723
|
+
* answers correctly on turn 1 (see {@link McpToolRegistry.seedFromCache}).
|
|
839
724
|
*/
|
|
840
725
|
async seedMcpCatalogFromCache(servers) {
|
|
841
726
|
if (!this.opts.backgroundMcpConnect)
|
|
842
727
|
return;
|
|
843
728
|
if (this.opts.allowedTools || !this.settingsManager.get("deferredMcpTools"))
|
|
844
729
|
return;
|
|
845
|
-
|
|
846
|
-
try {
|
|
847
|
-
entries = await this.mcpCatalogCache.entriesFor(servers);
|
|
848
|
-
}
|
|
849
|
-
catch {
|
|
850
|
-
return;
|
|
851
|
-
}
|
|
852
|
-
const stubs = [];
|
|
853
|
-
for (const [serverName, entry] of entries) {
|
|
854
|
-
for (const cached of entry.tools) {
|
|
855
|
-
if (this.liveMcpTools.has(cached.name))
|
|
856
|
-
continue;
|
|
857
|
-
this.cachedMcpToolServers.set(cached.name, serverName);
|
|
858
|
-
stubs.push(this.buildCachedMcpTool(serverName, cached));
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
if (stubs.length === 0)
|
|
862
|
-
return;
|
|
863
|
-
log("INFO", "mcp", "Seeded deferred tool catalog from cache", {
|
|
864
|
-
tools: String(stubs.length),
|
|
865
|
-
servers: String(entries.size),
|
|
866
|
-
});
|
|
867
|
-
this.addCachedMcpTools(stubs);
|
|
868
|
-
}
|
|
869
|
-
/** Catalog-only registration for cached stubs — never marks them live. */
|
|
870
|
-
addCachedMcpTools(stubs) {
|
|
871
|
-
this.mcpCatalog ??= new DeferredToolCatalog(this.contextLimits);
|
|
872
|
-
this.mcpCatalog.add(stubs);
|
|
873
|
-
this.ensureToolSearchTool();
|
|
874
|
-
}
|
|
875
|
-
buildCachedMcpTool(serverName, cached) {
|
|
876
|
-
return {
|
|
877
|
-
name: cached.name,
|
|
878
|
-
description: cached.description,
|
|
879
|
-
parameters: z.record(z.string(), z.unknown()),
|
|
880
|
-
rawInputSchema: cached.rawInputSchema,
|
|
881
|
-
execute: async (args, context) => {
|
|
882
|
-
const live = this.liveMcpTools.get(cached.name);
|
|
883
|
-
if (live)
|
|
884
|
-
return live.execute(args, context);
|
|
885
|
-
const outcome = (await this.mcpManager?.whenConnected(serverName)) ?? {
|
|
886
|
-
ok: false,
|
|
887
|
-
error: "MCP is disabled for this session",
|
|
888
|
-
};
|
|
889
|
-
if (!outcome.ok) {
|
|
890
|
-
return (`MCP tool ${cached.name} is unavailable: server "${serverName}" did not connect ` +
|
|
891
|
-
`(${outcome.error}). This tool was offered from a cached catalog. ` +
|
|
892
|
-
`Use a different approach or ask the user to check their MCP configuration.`);
|
|
893
|
-
}
|
|
894
|
-
const connected = this.liveMcpTools.get(cached.name);
|
|
895
|
-
if (!connected) {
|
|
896
|
-
return (`MCP tool ${cached.name} no longer exists: server "${serverName}" connected but ` +
|
|
897
|
-
`does not expose it. The cached catalog entry was stale.`);
|
|
898
|
-
}
|
|
899
|
-
return connected.execute(args, context);
|
|
900
|
-
},
|
|
901
|
-
};
|
|
730
|
+
await this.mcp.seedFromCache(servers);
|
|
902
731
|
}
|
|
903
732
|
/**
|
|
904
733
|
* Resolve a `/name [args]` input to the prompt template it expands into, or
|
|
@@ -1087,33 +916,15 @@ export class AgentSession {
|
|
|
1087
916
|
resetHookState(originalRequest) {
|
|
1088
917
|
this.opts.completionReview?.begin(originalRequest);
|
|
1089
918
|
this.lspManager?.clearPendingDiagnostics();
|
|
1090
|
-
this.
|
|
1091
|
-
this.
|
|
1092
|
-
this.hookConsecutiveFailures = 0;
|
|
1093
|
-
this.hookRepeatedNoProgressCalls = 0;
|
|
1094
|
-
this.hookProgressTracker.reset();
|
|
1095
|
-
this.hookCycleDetector.reset();
|
|
1096
|
-
this.hookCyclicPattern = null;
|
|
1097
|
-
this.hookFileEditCounts.clear();
|
|
1098
|
-
this.hookToolCalls.clear();
|
|
1099
|
-
this.loopBreakInjected = 0;
|
|
919
|
+
this.loopMonitor.reset();
|
|
920
|
+
this.verificationTracker.resetRun();
|
|
1100
921
|
this.regroundingInjected = false;
|
|
1101
|
-
this.hookRecentCalls = [];
|
|
1102
|
-
this.semanticLoop = {
|
|
1103
|
-
checksUsed: 0,
|
|
1104
|
-
lastCheckTurn: 0,
|
|
1105
|
-
pending: false,
|
|
1106
|
-
verdict: null,
|
|
1107
|
-
injected: false,
|
|
1108
|
-
};
|
|
1109
922
|
this.runStartedAt = Date.now();
|
|
1110
923
|
this.processGateInjected = 0;
|
|
924
|
+
this.planSubmissionNudges = 0;
|
|
1111
925
|
this.verificationGate.beginRun();
|
|
1112
926
|
const processes = new Set(this.processManager?.list().map((p) => p.id) ?? []);
|
|
1113
|
-
|
|
1114
|
-
if (!processes.has(id))
|
|
1115
|
-
this.backgroundVerification.delete(id);
|
|
1116
|
-
}
|
|
927
|
+
this.verificationTracker.pruneBackground(processes);
|
|
1117
928
|
this.compactionOccurred = false;
|
|
1118
929
|
// Post-turn compaction may have landed between runs — adopt its armed
|
|
1119
930
|
// signal so the re-grounding hook fires for this run exactly as it would
|
|
@@ -1137,157 +948,20 @@ export class AgentSession {
|
|
|
1137
948
|
}
|
|
1138
949
|
switch (event.type) {
|
|
1139
950
|
case "text_delta":
|
|
1140
|
-
this.
|
|
951
|
+
this.loopMonitor.recordText(event.text);
|
|
1141
952
|
break;
|
|
1142
953
|
case "tool_call_start": {
|
|
1143
|
-
this.
|
|
1144
|
-
name: event.name,
|
|
1145
|
-
args: event.args ?? {},
|
|
1146
|
-
revision: this.verificationGate.revision,
|
|
1147
|
-
});
|
|
1148
|
-
const startClassification = event.name === "bash" && typeof event.args?.command === "string"
|
|
1149
|
-
? classifyVerificationCommand(event.args.command)
|
|
1150
|
-
: null;
|
|
1151
|
-
if (startClassification &&
|
|
1152
|
-
typeof event.args?.command === "string" &&
|
|
1153
|
-
(isVerificationCommand(event.args.command) ||
|
|
1154
|
-
startClassification.accepted ||
|
|
1155
|
-
// Must match the tool_call_end predicate: a snapshot-eligible
|
|
1156
|
-
// command that never captured a "before" snapshot is misread at
|
|
1157
|
-
// the end as an uncomparable workspace and re-arms the gate.
|
|
1158
|
-
startClassification.snapshotEligible === true)) {
|
|
1159
|
-
// A check that can rewrite files (--fix, build scripts, emitters)
|
|
1160
|
-
// invalidates earlier in-flight evidence AND marks the run as
|
|
1161
|
-
// touched. A check that is merely UNRECOGNIZED (`make test`, `deno
|
|
1162
|
-
// test`) rewrites nothing we can point to: bumping the revision for
|
|
1163
|
-
// it poisoned the gate on green output and re-armed the hook into
|
|
1164
|
-
// every later question turn.
|
|
1165
|
-
this.verificationGate.recordVerificationAttempt();
|
|
1166
|
-
const classification = startClassification;
|
|
1167
|
-
if (classification.snapshotEligible && event.args.persist !== true) {
|
|
1168
|
-
const call = this.hookToolCalls.get(event.toolCallId);
|
|
1169
|
-
call.sourceSnapshot = await captureVerificationSnapshot(this.opts.cwd, [
|
|
1170
|
-
...this.hookFileEditCounts.keys(),
|
|
1171
|
-
]);
|
|
1172
|
-
if (call.sourceSnapshot === null)
|
|
1173
|
-
this.verificationGate.requireFreshVerification(true);
|
|
1174
|
-
}
|
|
1175
|
-
else if ((classification.accepted && event.args.persist !== true) ||
|
|
1176
|
-
(!classification.accepted && classification.mayMutate)) {
|
|
1177
|
-
// Flag the workspace unknown only when tool_call_end can resolve
|
|
1178
|
-
// it: a bounded check records pass/fail, a file-rewriting command
|
|
1179
|
-
// bumps the revision. An unrecognized read-only check (`biome ci`)
|
|
1180
|
-
// or a persistent-shell run records nothing at the end, so
|
|
1181
|
-
// flagging it left verified work Unverified forever — and
|
|
1182
|
-
// autopilot silently refused every later turn.
|
|
1183
|
-
this.verificationGate.requireFreshVerification(!classification.accepted && classification.mayMutate);
|
|
1184
|
-
}
|
|
954
|
+
if (await this.verificationTracker.recordToolCallStart(event)) {
|
|
1185
955
|
await this.persistVerificationState();
|
|
1186
956
|
}
|
|
1187
957
|
break;
|
|
1188
958
|
}
|
|
1189
959
|
case "tool_call_end": {
|
|
1190
|
-
const call = this.
|
|
960
|
+
const call = this.verificationTracker.getToolCall(event.toolCallId);
|
|
1191
961
|
const name = call?.name ?? "";
|
|
1192
962
|
const args = call?.args;
|
|
1193
|
-
this.
|
|
1194
|
-
|
|
1195
|
-
this.hookStats.toolFailures += 1;
|
|
1196
|
-
this.hookConsecutiveFailures = event.isError ? this.hookConsecutiveFailures + 1 : 0;
|
|
1197
|
-
this.hookRepeatedNoProgressCalls = this.hookProgressTracker.record(name, args, event.result, event.isError);
|
|
1198
|
-
this.hookCyclicPattern = this.hookCycleDetector.record(name, args, event.result, event.isError);
|
|
1199
|
-
// Semantic-loop judge input: a bounded digest of WHAT was attempted and
|
|
1200
|
-
// HOW it came out. Args/results are sliced AT RECORD TIME — a write with
|
|
1201
|
-
// a 50 KiB payload or a bash dump must never inflate the ring, and the
|
|
1202
|
-
// judge needs shapes, not payloads.
|
|
1203
|
-
this.hookRecentCalls.push({
|
|
1204
|
-
tool: name,
|
|
1205
|
-
args: args === undefined ? "" : JSON.stringify(args).slice(0, 300),
|
|
1206
|
-
ok: !event.isError,
|
|
1207
|
-
result: event.result.slice(0, 400),
|
|
1208
|
-
});
|
|
1209
|
-
if (this.hookRecentCalls.length > MAX_SEMANTIC_LOOP_CALLS) {
|
|
1210
|
-
this.hookRecentCalls.splice(0, this.hookRecentCalls.length - MAX_SEMANTIC_LOOP_CALLS);
|
|
1211
|
-
}
|
|
1212
|
-
// Only host-observed successful mutations and trustworthy check results
|
|
1213
|
-
// affect approval. The model's text is never evidence.
|
|
1214
|
-
let verificationChanged = false;
|
|
1215
|
-
if (!event.isError && args) {
|
|
1216
|
-
if (name === "edit" || name === "write") {
|
|
1217
|
-
// Check-owning files (tsconfig.json, pytest.ini, vitest.config.ts …)
|
|
1218
|
-
// are tracked even when they are not source code: editing one
|
|
1219
|
-
// invalidates earlier check results. A multi-file edit records every file.
|
|
1220
|
-
for (const filePath of editTargetPaths(args)) {
|
|
1221
|
-
if (isCodeFilePath(filePath) || isCheckOwnFile(filePath)) {
|
|
1222
|
-
this.verificationGate.recordMutation(filePath);
|
|
1223
|
-
verificationChanged = true;
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
if (args && call && name === "bash") {
|
|
1229
|
-
const command = typeof args.command === "string" ? args.command : "";
|
|
1230
|
-
const classification = classifyVerificationCommand(command);
|
|
1231
|
-
if (args.review === true && event.result === REVIEW_REJECTED_BEFORE_START) {
|
|
1232
|
-
// The core tool rejected its arguments BEFORE spawning. Never mint
|
|
1233
|
-
// a pass, or poison the failed-check ledger with an unexecuted check.
|
|
1234
|
-
// A child's output cannot match: bash always prefixes it with Exit code.
|
|
1235
|
-
this.verificationGate.recordRejectedCheck(command, REVIEW_REJECTED_BEFORE_START);
|
|
1236
|
-
delete call.sourceSnapshot;
|
|
1237
|
-
verificationChanged = true;
|
|
1238
|
-
}
|
|
1239
|
-
else if (classification.accepted || classification.snapshotEligible) {
|
|
1240
|
-
// A foreground check that outlived the default budget was moved to
|
|
1241
|
-
// the background, not failed: track it to its real exit the same way.
|
|
1242
|
-
const autoBackgroundId = event.isError ? undefined : autoBackgroundedId(event.result);
|
|
1243
|
-
if ((autoBackgroundId !== undefined || args.run_in_background === true) &&
|
|
1244
|
-
!event.isError &&
|
|
1245
|
-
args.persist !== true) {
|
|
1246
|
-
const id = autoBackgroundId ?? /^ID:\s*(\S+)/m.exec(event.result)?.[1];
|
|
1247
|
-
// No parseable ID means the check cannot be tracked to a real exit
|
|
1248
|
-
// code — no evidence either way. Recording a FAILURE here made
|
|
1249
|
-
// every later green run of a different spelling look owed.
|
|
1250
|
-
if (id)
|
|
1251
|
-
this.backgroundVerification.set(id, {
|
|
1252
|
-
revision: call.revision,
|
|
1253
|
-
command,
|
|
1254
|
-
...(classification.snapshotEligible
|
|
1255
|
-
? { sourceSnapshot: call.sourceSnapshot ?? null }
|
|
1256
|
-
: {}),
|
|
1257
|
-
});
|
|
1258
|
-
else if (classification.snapshotEligible)
|
|
1259
|
-
this.verificationGate.requireFreshVerification(true);
|
|
1260
|
-
}
|
|
1261
|
-
else if (args.persist === true) {
|
|
1262
|
-
// Persistent-shell checks are not bounded evidence (steering can
|
|
1263
|
-
// interleave): neither a pass nor a failure. A recorded failure
|
|
1264
|
-
// here blocked approval for sessions that prefer the shell.
|
|
1265
|
-
}
|
|
1266
|
-
else if (classification.snapshotEligible) {
|
|
1267
|
-
await this.finishSnapshotVerification({ command, revision: call.revision, sourceSnapshot: call.sourceSnapshot ?? null }, !event.isError && /^Exit code:\s*0(?:\s|$)/i.test(event.result.trim()));
|
|
1268
|
-
verificationChanged = true;
|
|
1269
|
-
}
|
|
1270
|
-
else {
|
|
1271
|
-
if (!event.isError && /^Exit code:\s*0(?:\s|$)/i.test(event.result.trim())) {
|
|
1272
|
-
this.verificationGate.recordVerification(call.revision, command);
|
|
1273
|
-
}
|
|
1274
|
-
else {
|
|
1275
|
-
this.verificationGate.recordFailedVerification(command, call.revision);
|
|
1276
|
-
}
|
|
1277
|
-
verificationChanged = true;
|
|
1278
|
-
}
|
|
1279
|
-
delete call.sourceSnapshot;
|
|
1280
|
-
}
|
|
1281
|
-
else if (classification.candidate) {
|
|
1282
|
-
// Green but untrusted: remember WHY so the demand can tell the
|
|
1283
|
-
// agent which command shape actually clears the gate.
|
|
1284
|
-
this.verificationGate.recordRejectedCheck(command, classification.reason);
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1287
|
-
if (!event.isError && args && name === "task_output" && typeof args.id === "string") {
|
|
1288
|
-
verificationChanged =
|
|
1289
|
-
(await this.recordFinishedBackgroundVerification(args.id)) || verificationChanged;
|
|
1290
|
-
}
|
|
963
|
+
this.loopMonitor.recordToolEnd(name, args, event.result, event.isError);
|
|
964
|
+
const verificationChanged = await this.verificationTracker.recordToolCallEnd(event, call, this.planModeRef.current);
|
|
1291
965
|
if (verificationChanged)
|
|
1292
966
|
await this.persistVerificationState();
|
|
1293
967
|
// Tool results are what push the run over the review gate, and they all
|
|
@@ -1297,7 +971,7 @@ export class AgentSession {
|
|
|
1297
971
|
break;
|
|
1298
972
|
}
|
|
1299
973
|
case "turn_end":
|
|
1300
|
-
this.
|
|
974
|
+
this.loopMonitor.recordTurn(event.turn);
|
|
1301
975
|
this.refreshHookArming();
|
|
1302
976
|
for (let index = this.messages.length - 1; index >= 0; index--) {
|
|
1303
977
|
const anchor = this.messages[index];
|
|
@@ -1474,51 +1148,26 @@ export class AgentSession {
|
|
|
1474
1148
|
// Deterministic stuck verdict, computed once and shared: the semantic
|
|
1475
1149
|
// judge must not spend tokens on a burst the deterministic breaker is
|
|
1476
1150
|
// about to correct itself.
|
|
1477
|
-
const deterministicDecision = evaluateLoopBreak(
|
|
1478
|
-
consecutiveFailures: this.hookConsecutiveFailures,
|
|
1479
|
-
repeatedNoProgressCalls: this.hookRepeatedNoProgressCalls,
|
|
1480
|
-
textRepetitionDetected: detectTextRepetition(this.hookText),
|
|
1481
|
-
...(this.hookCyclicPattern ? { cyclicPattern: this.hookCyclicPattern } : {}),
|
|
1482
|
-
});
|
|
1151
|
+
const deterministicDecision = this.loopMonitor.evaluateLoopBreak();
|
|
1483
1152
|
this.maybeStartSemanticLoopCheck(deterministicDecision.shouldBreak);
|
|
1484
1153
|
// Two-stage loop-breaker: stage 1 nudges; a FRESH detection after that
|
|
1485
1154
|
// injects the harsher final stop-and-report prompt. Signals reset after
|
|
1486
1155
|
// each injection so stage 2 only fires on new evidence.
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
this.hookConsecutiveFailures = 0;
|
|
1496
|
-
this.hookRepeatedNoProgressCalls = 0;
|
|
1497
|
-
// The deterministic breaker owns this burst — a semantic verdict from
|
|
1498
|
-
// the same burst must not double-correct on the next poll.
|
|
1499
|
-
this.semanticLoop.verdict = null;
|
|
1500
|
-
// Clear the text buffer too — otherwise a stage-1 text-repetition
|
|
1501
|
-
// trigger still sees the same repeated tail on the next check and
|
|
1502
|
-
// escalates to stage 2 on stale evidence.
|
|
1503
|
-
this.hookText = "";
|
|
1504
|
-
log("INFO", "loop-break", "Injecting loop-break nudge", {
|
|
1505
|
-
stage: String(stage),
|
|
1506
|
-
reasons: decision.reasons.join(", "),
|
|
1507
|
-
});
|
|
1508
|
-
this.eventBus.emit("hook", { kind: "loop_break" });
|
|
1509
|
-
return [buildLoopBreakMessage(decision.reasons, stage === 2)];
|
|
1510
|
-
}
|
|
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)];
|
|
1511
1164
|
}
|
|
1512
1165
|
// Semantic loop-break: an LLM verdict (started by maybeStartSemanticLoopCheck
|
|
1513
1166
|
// on a suspicious-but-syntactically-quiet burst) is consumed here, exactly
|
|
1514
1167
|
// once, with the deterministic breaker getting priority above. Fail-open:
|
|
1515
1168
|
// no verdict or no-loop verdict injects nothing.
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
this.semanticLoop.injected = true;
|
|
1519
|
-
this.semanticLoop.verdict = null;
|
|
1520
|
-
// The judged burst has been addressed; a fresh burst must re-accumulate.
|
|
1521
|
-
this.hookConsecutiveFailures = 0;
|
|
1169
|
+
const verdict = this.loopMonitor.takeSemanticVerdict();
|
|
1170
|
+
if (verdict) {
|
|
1522
1171
|
log("INFO", "loop-break", "Injecting semantic loop-break steering", {
|
|
1523
1172
|
reason: verdict.reason,
|
|
1524
1173
|
});
|
|
@@ -1538,34 +1187,26 @@ export class AgentSession {
|
|
|
1538
1187
|
* Fire-and-forget: the call runs while the next turn streams, and a finished
|
|
1539
1188
|
* verdict is consumed by the NEXT steering poll — never blocking a turn. */
|
|
1540
1189
|
maybeStartSemanticLoopCheck(deterministicBreak) {
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
totalFailures: this.hookStats.toolFailures,
|
|
1544
|
-
turns: this.hookStats.turns,
|
|
1545
|
-
lastCheckTurn: this.semanticLoop.lastCheckTurn,
|
|
1546
|
-
checksUsed: this.semanticLoop.checksUsed,
|
|
1547
|
-
checkPending: this.semanticLoop.pending,
|
|
1548
|
-
deterministicBreak,
|
|
1549
|
-
})) {
|
|
1190
|
+
const monitor = this.loopMonitor;
|
|
1191
|
+
if (!monitor.shouldRunSemanticCheck(deterministicBreak)) {
|
|
1550
1192
|
return;
|
|
1551
1193
|
}
|
|
1552
|
-
// resetHookState replaces
|
|
1194
|
+
// resetHookState replaces the run state on every prompt. A late judge must
|
|
1553
1195
|
// not publish a verdict or consume the next run's budget/cooldown.
|
|
1554
|
-
const runState =
|
|
1555
|
-
runState.pending = true;
|
|
1196
|
+
const runState = monitor.beginSemanticCheck();
|
|
1556
1197
|
log("INFO", "loop-break", "Starting semantic loop judge", {
|
|
1557
|
-
turn: String(
|
|
1558
|
-
consecutiveFailures: String(
|
|
1559
|
-
recentCalls: String(
|
|
1198
|
+
turn: String(monitor.stats.turns),
|
|
1199
|
+
consecutiveFailures: String(monitor.consecutiveFailures),
|
|
1200
|
+
recentCalls: String(monitor.recentCalls.length),
|
|
1560
1201
|
});
|
|
1561
1202
|
void (async () => {
|
|
1562
1203
|
try {
|
|
1563
|
-
const prompt = buildSemanticLoopJudgePrompt(
|
|
1204
|
+
const prompt = buildSemanticLoopJudgePrompt(monitor.recentCalls, this.originalRequest);
|
|
1564
1205
|
const raw = await (this.opts.semanticLoopJudge?.(prompt) ??
|
|
1565
1206
|
this.callSemanticLoopJudge(prompt));
|
|
1566
1207
|
const verdict = parseSemanticLoopVerdict(raw);
|
|
1567
|
-
if (
|
|
1568
|
-
runState
|
|
1208
|
+
if (verdict?.loop)
|
|
1209
|
+
monitor.publishSemanticVerdict(runState, verdict);
|
|
1569
1210
|
}
|
|
1570
1211
|
catch (error) {
|
|
1571
1212
|
// Fail open: judge errors never stop a run. Budget and cooldown are
|
|
@@ -1575,11 +1216,7 @@ export class AgentSession {
|
|
|
1575
1216
|
});
|
|
1576
1217
|
}
|
|
1577
1218
|
finally {
|
|
1578
|
-
|
|
1579
|
-
runState.pending = false;
|
|
1580
|
-
runState.checksUsed += 1;
|
|
1581
|
-
runState.lastCheckTurn = this.hookStats.turns;
|
|
1582
|
-
}
|
|
1219
|
+
monitor.settleSemanticCheck(runState);
|
|
1583
1220
|
}
|
|
1584
1221
|
})();
|
|
1585
1222
|
}
|
|
@@ -1675,23 +1312,7 @@ export class AgentSession {
|
|
|
1675
1312
|
* with — so reuse the same stuck signals that drive the loop-breaker.
|
|
1676
1313
|
*/
|
|
1677
1314
|
shouldExtendTurnBudget(ctx) {
|
|
1678
|
-
const refusals =
|
|
1679
|
-
const stuck = evaluateLoopBreak({
|
|
1680
|
-
consecutiveFailures: this.hookConsecutiveFailures,
|
|
1681
|
-
repeatedNoProgressCalls: this.hookRepeatedNoProgressCalls,
|
|
1682
|
-
textRepetitionDetected: detectTextRepetition(this.hookText),
|
|
1683
|
-
...(this.hookCyclicPattern ? { cyclicPattern: this.hookCyclicPattern } : {}),
|
|
1684
|
-
});
|
|
1685
|
-
if (stuck.shouldBreak)
|
|
1686
|
-
refusals.push(...stuck.reasons);
|
|
1687
|
-
// Stage 2 means the loop-breaker already detected spinning twice and told
|
|
1688
|
-
// the agent to stop and report. Do not overrule that with more turns.
|
|
1689
|
-
if (this.loopBreakInjected >= 2)
|
|
1690
|
-
refusals.push("loop-breaker already escalated");
|
|
1691
|
-
const { toolCalls, toolFailures } = this.hookStats;
|
|
1692
|
-
if (toolCalls > 0 && toolFailures / toolCalls > TURN_EXTENSION_MAX_FAILURE_RATIO) {
|
|
1693
|
-
refusals.push(`${toolFailures}/${toolCalls} tool calls failed`);
|
|
1694
|
-
}
|
|
1315
|
+
const refusals = this.loopMonitor.turnExtensionRefusals();
|
|
1695
1316
|
const granted = refusals.length === 0;
|
|
1696
1317
|
log("INFO", "turn-budget", granted ? "Extending turn budget" : "Refusing turn-budget extension", {
|
|
1697
1318
|
turn: String(ctx.turn),
|
|
@@ -1727,11 +1348,7 @@ export class AgentSession {
|
|
|
1727
1348
|
async getHookFollowUpMessages() {
|
|
1728
1349
|
// Exit notifications and task_output refer to the same host process record.
|
|
1729
1350
|
// Do not force an extra polling tool/turn merely to acknowledge a known exit.
|
|
1730
|
-
|
|
1731
|
-
for (const id of this.backgroundVerification.keys()) {
|
|
1732
|
-
backgroundChanged =
|
|
1733
|
-
(await this.recordFinishedBackgroundVerification(id)) || backgroundChanged;
|
|
1734
|
-
}
|
|
1351
|
+
const backgroundChanged = await this.verificationTracker.settleFinishedBackground();
|
|
1735
1352
|
if (backgroundChanged)
|
|
1736
1353
|
await this.persistVerificationState();
|
|
1737
1354
|
// Edits return immediately; only the completion boundary waits for remaining
|
|
@@ -1770,6 +1387,22 @@ export class AgentSession {
|
|
|
1770
1387
|
}
|
|
1771
1388
|
if (diagnosticMessages.length > 0)
|
|
1772
1389
|
return diagnosticMessages;
|
|
1390
|
+
if (shouldNudgePlanSubmission({
|
|
1391
|
+
enabled: this.opts.planSubmissionGate === true,
|
|
1392
|
+
planMode: this.planModeRef.current,
|
|
1393
|
+
aborted: this.opts.signal?.aborted === true,
|
|
1394
|
+
nudgesThisRun: this.planSubmissionNudges,
|
|
1395
|
+
})) {
|
|
1396
|
+
this.planSubmissionNudges += 1;
|
|
1397
|
+
log("INFO", "plan-gate", "Run stopping in plan mode without a submitted plan; reminding");
|
|
1398
|
+
return [
|
|
1399
|
+
{
|
|
1400
|
+
role: "user",
|
|
1401
|
+
content: buildNotificationSteeringText([PLAN_SUBMISSION_NUDGE]),
|
|
1402
|
+
provenance: { source: "runtime", kind: "notification", visibility: "hidden" },
|
|
1403
|
+
},
|
|
1404
|
+
];
|
|
1405
|
+
}
|
|
1773
1406
|
if (this.opts.completionReview) {
|
|
1774
1407
|
const followUp = await this.opts.completionReview.followUp((request, signal) => this.callCompletionReviewer(request, signal), this.opts.signal);
|
|
1775
1408
|
await this.persistCompletionReviewState();
|
|
@@ -1992,6 +1625,11 @@ export class AgentSession {
|
|
|
1992
1625
|
maxTurnToolResultChars: resolveSessionTurnToolResultCharLimit(this.model, this.provider, accountId),
|
|
1993
1626
|
// Warn when web/MCP output contains instruction-like text (see injection-detect.ts).
|
|
1994
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) }),
|
|
1995
1633
|
// Self-correction hooks (same as the TUI): loop-break + re-grounding are
|
|
1996
1634
|
// polled mid-loop; the ideal review is polled when the agent would stop.
|
|
1997
1635
|
getSteeringMessages: () => this.getHookSteeringMessages(),
|
|
@@ -2313,11 +1951,12 @@ export class AgentSession {
|
|
|
2313
1951
|
// there avoids tearing down a live stdio child and
|
|
2314
1952
|
// gambling on a `npx` re-spawn that could fail and drop the tools.
|
|
2315
1953
|
const glmInvolved = this.provider === "glm" || prevProvider === "glm";
|
|
2316
|
-
|
|
1954
|
+
const mcpManager = this.mcp.manager;
|
|
1955
|
+
if (mcpManager && glmInvolved) {
|
|
2317
1956
|
// Remove old MCP tools
|
|
2318
1957
|
this.tools = this.tools.filter((t) => !t.name.startsWith("mcp__"));
|
|
2319
1958
|
// Disconnect old MCP servers
|
|
2320
|
-
await
|
|
1959
|
+
await mcpManager.dispose();
|
|
2321
1960
|
// Connect new MCP servers for the new provider
|
|
2322
1961
|
try {
|
|
2323
1962
|
let apiKey;
|
|
@@ -2334,13 +1973,11 @@ export class AgentSession {
|
|
|
2334
1973
|
const servers = await getAllMcpServers(this.provider, apiKey, this.cwd, {
|
|
2335
1974
|
allowProjectScope: this.settingsManager.isProjectTrusted(this.cwd),
|
|
2336
1975
|
});
|
|
2337
|
-
const mcpTools = await
|
|
1976
|
+
const mcpTools = await mcpManager.connectAll(servers);
|
|
2338
1977
|
// Drop stale MCP tools from both the live set and deferred catalog before
|
|
2339
1978
|
// re-adding. Some tools may already have been promoted out of the catalog.
|
|
2340
1979
|
this.tools = this.tools.filter((t) => !t.name.startsWith("mcp__"));
|
|
2341
|
-
this.
|
|
2342
|
-
this.liveMcpTools.clear();
|
|
2343
|
-
this.cachedMcpToolServers.clear();
|
|
1980
|
+
this.mcp.clearMcpTools();
|
|
2344
1981
|
this.addMcpTools(mcpTools);
|
|
2345
1982
|
}
|
|
2346
1983
|
catch (err) {
|
|
@@ -2383,10 +2020,7 @@ export class AgentSession {
|
|
|
2383
2020
|
this.checkpointGeneration = loaded.header.generation ?? 0;
|
|
2384
2021
|
this.currentLeafId = loaded.header.leafId;
|
|
2385
2022
|
this.setSessionPath(loaded.path);
|
|
2386
|
-
this.
|
|
2387
|
-
this.autopilotMarkers = this.sessionManager.getAutopilotMarkers(loaded.entries, loaded.header.leafId);
|
|
2388
|
-
this.appMarkers = this.sessionManager.getAppMarkers(loaded.entries, loaded.header.leafId);
|
|
2389
|
-
this.turnMetrics = this.sessionManager.getTurnMetrics(loaded.entries);
|
|
2023
|
+
this.markers.restore(loaded.entries, loaded.header.leafId);
|
|
2390
2024
|
this.lastPersistedIndex = this.messages.length;
|
|
2391
2025
|
this.providerContext = null;
|
|
2392
2026
|
await this.subAgentManager?.rebindParentSession(this.sessionId);
|
|
@@ -2426,10 +2060,7 @@ export class AgentSession {
|
|
|
2426
2060
|
await this.persistMessage(message);
|
|
2427
2061
|
}
|
|
2428
2062
|
this.lastPersistedIndex = this.messages.length;
|
|
2429
|
-
await this.
|
|
2430
|
-
await this.rePersistKenTurns();
|
|
2431
|
-
await this.rePersistAutopilotMarkers();
|
|
2432
|
-
await this.rePersistAppMarkers();
|
|
2063
|
+
await this.markers.rePersistAll();
|
|
2433
2064
|
await this.persistVerificationState();
|
|
2434
2065
|
await this.persistAppMarker("compaction", {
|
|
2435
2066
|
originalCount: result.originalCount,
|
|
@@ -2788,12 +2419,17 @@ export class AgentSession {
|
|
|
2788
2419
|
});
|
|
2789
2420
|
}
|
|
2790
2421
|
async newSession(preserveConversation = false) {
|
|
2422
|
+
// A background post-turn compaction replaces `this.messages` when it
|
|
2423
|
+
// lands. Letting it finish after the reset would swap the old history back
|
|
2424
|
+
// into the fresh session (e.g. an accepted plan's build would inherit the
|
|
2425
|
+
// whole planning conversation and lose the approved-plan prompt).
|
|
2426
|
+
await this.settlePostTurnCompaction();
|
|
2791
2427
|
this.cacheDiagnostics.reset();
|
|
2792
2428
|
// Approved-plan execution is a clean checkpoint of the same conversation;
|
|
2793
2429
|
// explicit new sessions reset the conversation identity.
|
|
2794
2430
|
if (!preserveConversation) {
|
|
2795
2431
|
this.verificationGate.reset();
|
|
2796
|
-
this.
|
|
2432
|
+
this.verificationTracker.clearBackground();
|
|
2797
2433
|
this.conversationId = "";
|
|
2798
2434
|
this.checkpointGeneration = 0;
|
|
2799
2435
|
this.sessionPreview = "";
|
|
@@ -2805,10 +2441,7 @@ export class AgentSession {
|
|
|
2805
2441
|
// turns / autopilot verdicts / app markers linger in memory, show up in the
|
|
2806
2442
|
// new session's /history, and get re-persisted into the new file by the
|
|
2807
2443
|
// next compaction — the cross-session duplicate-marker propagation bug.
|
|
2808
|
-
this.
|
|
2809
|
-
this.autopilotMarkers = [];
|
|
2810
|
-
this.appMarkers = [];
|
|
2811
|
-
this.turnMetrics = [];
|
|
2444
|
+
this.markers.clear();
|
|
2812
2445
|
const basePrompt = await this.buildBasePrompt(false, undefined);
|
|
2813
2446
|
this.baseSystemPrompt = basePrompt;
|
|
2814
2447
|
this.messages = [{ role: "system", content: this.withSystemPromptTail(basePrompt) }];
|
|
@@ -2930,9 +2563,7 @@ export class AgentSession {
|
|
|
2930
2563
|
else {
|
|
2931
2564
|
used = calculateActiveContextTokens(this.messages);
|
|
2932
2565
|
}
|
|
2933
|
-
const costUsd = this.
|
|
2934
|
-
? this.turnMetrics.reduce((sum, m) => sum + (m.cost.status === "known" ? m.cost.usd : 0), 0)
|
|
2935
|
-
: undefined;
|
|
2566
|
+
const costUsd = this.markers.knownCostUsd();
|
|
2936
2567
|
return costUsd === undefined ? { used, size } : { used, size, costUsd };
|
|
2937
2568
|
}
|
|
2938
2569
|
/**
|
|
@@ -3056,6 +2687,11 @@ export class AgentSession {
|
|
|
3056
2687
|
*/
|
|
3057
2688
|
async setPlanMode(active) {
|
|
3058
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
|
+
}
|
|
3059
2695
|
// Entering plan mode discards any prior approved-plan contract (a new plan
|
|
3060
2696
|
// is about to be drafted); exiting keeps it (set explicitly via accept).
|
|
3061
2697
|
if (active)
|
|
@@ -3259,7 +2895,10 @@ export class AgentSession {
|
|
|
3259
2895
|
orchestrationPolicyTail() {
|
|
3260
2896
|
if (this.opts.orchestrationPrompt === false)
|
|
3261
2897
|
return "";
|
|
3262
|
-
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();
|
|
3263
2902
|
}
|
|
3264
2903
|
/**
|
|
3265
2904
|
* Re-render the uncached tail of messages[0] in place. The cached prefix is
|
|
@@ -3281,12 +2920,7 @@ export class AgentSession {
|
|
|
3281
2920
|
return this.messages;
|
|
3282
2921
|
}
|
|
3283
2922
|
getTurnMetrics() {
|
|
3284
|
-
return this.
|
|
3285
|
-
...metric,
|
|
3286
|
-
usage: { ...metric.usage },
|
|
3287
|
-
timing: { ...metric.timing },
|
|
3288
|
-
cost: { ...metric.cost },
|
|
3289
|
-
}));
|
|
2923
|
+
return this.markers.getTurnMetrics();
|
|
3290
2924
|
}
|
|
3291
2925
|
async persistTurnMetric(event) {
|
|
3292
2926
|
if (event.stopReason === "error")
|
|
@@ -3313,31 +2947,23 @@ export class AgentSession {
|
|
|
3313
2947
|
reason: "No authoritative effective-dated provider pricing is available",
|
|
3314
2948
|
},
|
|
3315
2949
|
};
|
|
3316
|
-
this.
|
|
2950
|
+
this.markers.addTurnMetric(payload);
|
|
3317
2951
|
// Internal diagnostics piggyback on the authoritative per-turn metric —
|
|
3318
2952
|
// one source of truth, and the record flushes to disk every turn.
|
|
3319
2953
|
this.diagnosticsRecorder?.recordTurnMetric(payload);
|
|
3320
|
-
|
|
3321
|
-
await this.sessionManager.appendTurnMetric(this.sessionPath, payload);
|
|
3322
|
-
}
|
|
3323
|
-
async rePersistTurnMetrics() {
|
|
3324
|
-
if (!this.sessionPath)
|
|
3325
|
-
return;
|
|
3326
|
-
for (const metric of this.turnMetrics) {
|
|
3327
|
-
await this.sessionManager.appendTurnMetric(this.sessionPath, metric);
|
|
3328
|
-
}
|
|
2954
|
+
await this.markers.persistTurnMetric(payload);
|
|
3329
2955
|
}
|
|
3330
2956
|
/** Ken Kai (mentor) turns recorded against this session, in record order. Used
|
|
3331
2957
|
* by the host to interleave Ken's advisory exchanges back into the transcript
|
|
3332
2958
|
* on resume. Never part of the LLM message history. */
|
|
3333
2959
|
getKenTurns() {
|
|
3334
|
-
return this.
|
|
2960
|
+
return this.markers.getKenTurns();
|
|
3335
2961
|
}
|
|
3336
2962
|
/** Autopilot verdict markers recorded against this session, in record order.
|
|
3337
2963
|
* Used by the host to interleave the auto-review loop's markers back into
|
|
3338
2964
|
* the transcript on resume, mirroring `getKenTurns`. */
|
|
3339
2965
|
getAutopilotMarkers() {
|
|
3340
|
-
return this.
|
|
2966
|
+
return this.markers.getAutopilotMarkers();
|
|
3341
2967
|
}
|
|
3342
2968
|
/** Non-system messages that are actually on disk. Transcript markers anchor
|
|
3343
2969
|
* against this (not the in-memory list, which can run ahead after a failed
|
|
@@ -3352,15 +2978,7 @@ export class AgentSession {
|
|
|
3352
2978
|
* file, so the new file carries positions that match its own transcript.
|
|
3353
2979
|
*/
|
|
3354
2980
|
remapMarkerAnchors(remap) {
|
|
3355
|
-
|
|
3356
|
-
return;
|
|
3357
|
-
const move = (payload) => ({
|
|
3358
|
-
...payload,
|
|
3359
|
-
afterMessageCount: remapAnchorForCompaction(payload.afterMessageCount, remap),
|
|
3360
|
-
});
|
|
3361
|
-
this.kenTurns = this.kenTurns.map(move);
|
|
3362
|
-
this.autopilotMarkers = this.autopilotMarkers.map(move);
|
|
3363
|
-
this.appMarkers = this.appMarkers.map(move);
|
|
2981
|
+
this.markers.remapAnchors(remap);
|
|
3364
2982
|
}
|
|
3365
2983
|
/**
|
|
3366
2984
|
* Record one Ken Kai (mentor agent) turn against this build session: the
|
|
@@ -3373,39 +2991,7 @@ export class AgentSession {
|
|
|
3373
2991
|
* appendEntry's own handling.
|
|
3374
2992
|
*/
|
|
3375
2993
|
async persistKenTurn(question, reply) {
|
|
3376
|
-
|
|
3377
|
-
const payload = { version: 1, question, reply, afterMessageCount };
|
|
3378
|
-
this.kenTurns.push(payload);
|
|
3379
|
-
if (!this.sessionPath)
|
|
3380
|
-
return;
|
|
3381
|
-
const entry = {
|
|
3382
|
-
type: "custom",
|
|
3383
|
-
kind: KEN_TURN_CUSTOM_KIND,
|
|
3384
|
-
id: crypto.randomUUID(),
|
|
3385
|
-
parentId: null,
|
|
3386
|
-
timestamp: new Date().toISOString(),
|
|
3387
|
-
data: payload,
|
|
3388
|
-
};
|
|
3389
|
-
await this.sessionManager.appendEntry(this.sessionPath, entry);
|
|
3390
|
-
}
|
|
3391
|
-
/** Re-append the in-memory Ken turns to the current session file. Called after
|
|
3392
|
-
* a continuation/compaction file is created so Ken's advisory history isn't
|
|
3393
|
-
* lost when the session is rewritten (those rewrites only re-persist
|
|
3394
|
-
* messages). Each turn keeps its original `afterMessageCount` anchor. */
|
|
3395
|
-
async rePersistKenTurns() {
|
|
3396
|
-
if (!this.sessionPath)
|
|
3397
|
-
return;
|
|
3398
|
-
for (const payload of this.kenTurns) {
|
|
3399
|
-
const entry = {
|
|
3400
|
-
type: "custom",
|
|
3401
|
-
kind: KEN_TURN_CUSTOM_KIND,
|
|
3402
|
-
id: crypto.randomUUID(),
|
|
3403
|
-
parentId: null,
|
|
3404
|
-
timestamp: new Date().toISOString(),
|
|
3405
|
-
data: stripRecordedPosition(payload),
|
|
3406
|
-
};
|
|
3407
|
-
await this.sessionManager.appendEntry(this.sessionPath, entry);
|
|
3408
|
-
}
|
|
2994
|
+
await this.markers.recordKenTurn(question, reply, this.persistedTranscriptCount());
|
|
3409
2995
|
}
|
|
3410
2996
|
/**
|
|
3411
2997
|
* Record one autopilot verdict marker (prompted / done / human / capped)
|
|
@@ -3415,23 +3001,6 @@ export class AgentSession {
|
|
|
3415
3001
|
* instead of dropping the marker or falling back to a raw verdict string.
|
|
3416
3002
|
* No-op persistence for transient sessions (kept in memory only).
|
|
3417
3003
|
*/
|
|
3418
|
-
async recordFinishedBackgroundVerification(id) {
|
|
3419
|
-
const started = this.backgroundVerification.get(id);
|
|
3420
|
-
const proc = this.processManager?.list().find((entry) => entry.id === id);
|
|
3421
|
-
if (!started || !proc || proc.exitCode === null)
|
|
3422
|
-
return false;
|
|
3423
|
-
if (started.sourceSnapshot !== undefined) {
|
|
3424
|
-
await this.finishSnapshotVerification(started, proc.exitCode === 0);
|
|
3425
|
-
}
|
|
3426
|
-
else if (proc.exitCode === 0) {
|
|
3427
|
-
this.verificationGate.recordVerification(started.revision, started.command);
|
|
3428
|
-
}
|
|
3429
|
-
else {
|
|
3430
|
-
this.verificationGate.recordFailedVerification(started.command, started.revision);
|
|
3431
|
-
}
|
|
3432
|
-
this.backgroundVerification.delete(id);
|
|
3433
|
-
return true;
|
|
3434
|
-
}
|
|
3435
3004
|
getVerificationEvidence() {
|
|
3436
3005
|
return this.verificationGate.evidence();
|
|
3437
3006
|
}
|
|
@@ -3443,39 +3012,8 @@ export class AgentSession {
|
|
|
3443
3012
|
evidence: this.verificationGate.evidence("run"),
|
|
3444
3013
|
};
|
|
3445
3014
|
}
|
|
3446
|
-
async finishSnapshotVerification(check, passed) {
|
|
3447
|
-
const after = check.sourceSnapshot
|
|
3448
|
-
? await captureVerificationSnapshot(this.opts.cwd, [...this.hookFileEditCounts.keys()])
|
|
3449
|
-
: null;
|
|
3450
|
-
if (after === null || after !== check.sourceSnapshot) {
|
|
3451
|
-
this.verificationGate.requireFreshVerification(true);
|
|
3452
|
-
this.verificationGate.recordRejectedCheck(check.command, after === null
|
|
3453
|
-
? "Workspace inputs could not be compared; run a read-only check after the command"
|
|
3454
|
-
: "Command changed workspace inputs; run checks against the changed source");
|
|
3455
|
-
if (!passed)
|
|
3456
|
-
this.verificationGate.recordFailedVerification(check.command);
|
|
3457
|
-
}
|
|
3458
|
-
else if (passed) {
|
|
3459
|
-
const classification = classifyVerificationCommand(check.command);
|
|
3460
|
-
if (classification.snapshotPreserveOnly) {
|
|
3461
|
-
this.verificationGate.recordRejectedCheck(check.command, classification.reason);
|
|
3462
|
-
}
|
|
3463
|
-
else {
|
|
3464
|
-
this.verificationGate.recordVerification(check.revision, check.command);
|
|
3465
|
-
}
|
|
3466
|
-
}
|
|
3467
|
-
else {
|
|
3468
|
-
this.verificationGate.recordFailedVerification(check.command, check.revision);
|
|
3469
|
-
}
|
|
3470
|
-
}
|
|
3471
3015
|
getVerificationProblem() {
|
|
3472
|
-
|
|
3473
|
-
return "Unverified: a build is still running or its workspace comparison is pending.";
|
|
3474
|
-
}
|
|
3475
|
-
return (this.verificationGate.verificationProblem() ??
|
|
3476
|
-
(this.backgroundVerification.size > 0
|
|
3477
|
-
? "Unverified: a background check is still running or its result has not been confirmed."
|
|
3478
|
-
: null));
|
|
3016
|
+
return this.verificationTracker.verificationProblem();
|
|
3479
3017
|
}
|
|
3480
3018
|
async persistCompletionReviewState() {
|
|
3481
3019
|
if (!this.sessionPath || !this.opts.completionReview)
|
|
@@ -3508,50 +3046,13 @@ export class AgentSession {
|
|
|
3508
3046
|
await this.sessionManager.appendEntry(this.sessionPath, entry);
|
|
3509
3047
|
}
|
|
3510
3048
|
async persistAutopilotMarker(phase, extra) {
|
|
3511
|
-
|
|
3512
|
-
const payload = {
|
|
3513
|
-
version: 1,
|
|
3514
|
-
phase,
|
|
3515
|
-
afterMessageCount,
|
|
3516
|
-
...(extra?.reason !== undefined ? { reason: extra.reason } : {}),
|
|
3517
|
-
...(extra?.body !== undefined ? { body: extra.body } : {}),
|
|
3518
|
-
};
|
|
3519
|
-
this.autopilotMarkers.push(payload);
|
|
3520
|
-
if (!this.sessionPath)
|
|
3521
|
-
return;
|
|
3522
|
-
const entry = {
|
|
3523
|
-
type: "custom",
|
|
3524
|
-
kind: AUTOPILOT_MARKER_CUSTOM_KIND,
|
|
3525
|
-
id: crypto.randomUUID(),
|
|
3526
|
-
parentId: null,
|
|
3527
|
-
timestamp: new Date().toISOString(),
|
|
3528
|
-
data: payload,
|
|
3529
|
-
};
|
|
3530
|
-
await this.sessionManager.appendEntry(this.sessionPath, entry);
|
|
3531
|
-
}
|
|
3532
|
-
/** Re-append the in-memory autopilot markers to the current session file.
|
|
3533
|
-
* Mirrors `rePersistKenTurns` — called after a continuation/compaction file
|
|
3534
|
-
* is created so the auto-review history survives the rewrite. */
|
|
3535
|
-
async rePersistAutopilotMarkers() {
|
|
3536
|
-
if (!this.sessionPath)
|
|
3537
|
-
return;
|
|
3538
|
-
for (const payload of this.autopilotMarkers) {
|
|
3539
|
-
const entry = {
|
|
3540
|
-
type: "custom",
|
|
3541
|
-
kind: AUTOPILOT_MARKER_CUSTOM_KIND,
|
|
3542
|
-
id: crypto.randomUUID(),
|
|
3543
|
-
parentId: null,
|
|
3544
|
-
timestamp: new Date().toISOString(),
|
|
3545
|
-
data: stripRecordedPosition(payload),
|
|
3546
|
-
};
|
|
3547
|
-
await this.sessionManager.appendEntry(this.sessionPath, entry);
|
|
3548
|
-
}
|
|
3049
|
+
await this.markers.recordAutopilotMarker(phase, extra, this.persistedTranscriptCount());
|
|
3549
3050
|
}
|
|
3550
3051
|
/** App transcript markers recorded against this session, in record order.
|
|
3551
3052
|
* Used by the host to interleave display-only rows (plan banner, task
|
|
3552
3053
|
* header, errors, user-bubble hints) back into the transcript on resume. */
|
|
3553
3054
|
getAppMarkers() {
|
|
3554
|
-
return this.
|
|
3055
|
+
return this.markers.getAppMarkers();
|
|
3555
3056
|
}
|
|
3556
3057
|
/**
|
|
3557
3058
|
* Record one app transcript marker (display-only row) against this session.
|
|
@@ -3563,20 +3064,7 @@ export class AgentSession {
|
|
|
3563
3064
|
* No-op persistence for transient sessions.
|
|
3564
3065
|
*/
|
|
3565
3066
|
async persistAppMarker(kind, data, anchorOffset = 0) {
|
|
3566
|
-
|
|
3567
|
-
const payload = { version: 1, kind, afterMessageCount, data };
|
|
3568
|
-
this.appMarkers.push(payload);
|
|
3569
|
-
if (!this.sessionPath)
|
|
3570
|
-
return;
|
|
3571
|
-
const entry = {
|
|
3572
|
-
type: "custom",
|
|
3573
|
-
kind: APP_MARKER_CUSTOM_KIND,
|
|
3574
|
-
id: crypto.randomUUID(),
|
|
3575
|
-
parentId: null,
|
|
3576
|
-
timestamp: new Date().toISOString(),
|
|
3577
|
-
data: payload,
|
|
3578
|
-
};
|
|
3579
|
-
await this.sessionManager.appendEntry(this.sessionPath, entry);
|
|
3067
|
+
await this.markers.recordAppMarker(kind, data, this.persistedTranscriptCount() + anchorOffset);
|
|
3580
3068
|
}
|
|
3581
3069
|
/**
|
|
3582
3070
|
* Open the run journal for one `RunLifecycle` generation.
|
|
@@ -3605,24 +3093,6 @@ export class AgentSession {
|
|
|
3605
3093
|
outcome,
|
|
3606
3094
|
});
|
|
3607
3095
|
}
|
|
3608
|
-
/** Re-append the in-memory app markers to the current session file. Mirrors
|
|
3609
|
-
* `rePersistKenTurns` — called after a continuation/compaction file is
|
|
3610
|
-
* created so display-only rows survive the rewrite. */
|
|
3611
|
-
async rePersistAppMarkers() {
|
|
3612
|
-
if (!this.sessionPath)
|
|
3613
|
-
return;
|
|
3614
|
-
for (const payload of this.appMarkers) {
|
|
3615
|
-
const entry = {
|
|
3616
|
-
type: "custom",
|
|
3617
|
-
kind: APP_MARKER_CUSTOM_KIND,
|
|
3618
|
-
id: crypto.randomUUID(),
|
|
3619
|
-
parentId: null,
|
|
3620
|
-
timestamp: new Date().toISOString(),
|
|
3621
|
-
data: stripRecordedPosition(payload),
|
|
3622
|
-
};
|
|
3623
|
-
await this.sessionManager.appendEntry(this.sessionPath, entry);
|
|
3624
|
-
}
|
|
3625
|
-
}
|
|
3626
3096
|
/**
|
|
3627
3097
|
* Rewrite a draft prompt into a tighter, terminology-correct version using
|
|
3628
3098
|
* the ACTIVE provider/model. A stateless one-off LLM call (no agent loop, no
|
|
@@ -3864,7 +3334,7 @@ export class AgentSession {
|
|
|
3864
3334
|
this.stopBackgroundProcesses();
|
|
3865
3335
|
this.lspManager?.shutdownAll();
|
|
3866
3336
|
this.debugManager?.shutdown();
|
|
3867
|
-
await Promise.all([this.subAgentManager?.shutdownAll(), this.
|
|
3337
|
+
await Promise.all([this.subAgentManager?.shutdownAll(), this.mcp.manager?.dispose()]);
|
|
3868
3338
|
await this.extensionLoader.deactivateAll();
|
|
3869
3339
|
this.setSessionPath("");
|
|
3870
3340
|
this.eventBus.removeAllListeners();
|
|
@@ -3909,7 +3379,7 @@ export class AgentSession {
|
|
|
3909
3379
|
.reverse()
|
|
3910
3380
|
.find((entry) => entry.type === "custom" && entry.kind === COMPLETION_REVIEW_STATE_KIND);
|
|
3911
3381
|
this.opts.completionReview?.restore(savedCompletionReview?.type === "custom" ? savedCompletionReview.data : null);
|
|
3912
|
-
this.
|
|
3382
|
+
this.verificationTracker.clearBackground();
|
|
3913
3383
|
const savedVerification = [...loaded.entries]
|
|
3914
3384
|
.reverse()
|
|
3915
3385
|
.find((entry) => entry.type === "custom" && entry.kind === VERIFICATION_STATE_KIND);
|
|
@@ -3941,12 +3411,8 @@ export class AgentSession {
|
|
|
3941
3411
|
// The leaf is passed so each marker also carries its FILE-order position,
|
|
3942
3412
|
// the fallback used when a legacy anchor is out of range (see
|
|
3943
3413
|
// RecordedPosition).
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
this.autopilotMarkers = this.sessionManager.getAutopilotMarkers(loaded.entries, loaded.header.leafId);
|
|
3947
|
-
// Restore app transcript markers (plan banner / task header / errors / hints).
|
|
3948
|
-
this.appMarkers = this.sessionManager.getAppMarkers(loaded.entries, loaded.header.leafId);
|
|
3949
|
-
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);
|
|
3950
3416
|
// A run that opened the journal and never closed it died mid-flight. Read
|
|
3951
3417
|
// it here, before anything rewrites the file, and report it once the
|
|
3952
3418
|
// transcript is in place.
|
|
@@ -4146,13 +3612,4 @@ export class AgentSession {
|
|
|
4146
3612
|
}
|
|
4147
3613
|
}
|
|
4148
3614
|
}
|
|
4149
|
-
/** Expand a leading `~` so `/import ~/.codex/...` works from any shell. */
|
|
4150
|
-
function resolveHomePath(filePath) {
|
|
4151
|
-
if (filePath === "~")
|
|
4152
|
-
return os.homedir();
|
|
4153
|
-
if (filePath.startsWith("~/") || filePath.startsWith("~\\")) {
|
|
4154
|
-
return path.join(os.homedir(), filePath.slice(2));
|
|
4155
|
-
}
|
|
4156
|
-
return filePath;
|
|
4157
|
-
}
|
|
4158
3615
|
//# sourceMappingURL=agent-session.js.map
|