@prestyj/cli 5.29.1 → 5.30.1
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 +11 -12
- 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/compliance-guard/references/us.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 +3 -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-error.d.ts +21 -0
- package/dist/app-error.js +39 -0
- package/dist/app-error.js.map +1 -0
- package/dist/app-sidecar/app-settings.d.ts +63 -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 +62 -0
- package/dist/app-sidecar/autopilot-runner.js +376 -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/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/nolan-context.d.ts +20 -0
- package/dist/app-sidecar/nolan-context.js +55 -0
- package/dist/app-sidecar/nolan-context.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 +355 -0
- package/dist/app-sidecar/prompt-routes.js.map +1 -0
- package/dist/app-sidecar/repo-polls.d.ts +15 -0
- package/dist/app-sidecar/repo-polls.js +25 -0
- package/dist/app-sidecar/repo-polls.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 +437 -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 +232 -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 +374 -0
- package/dist/app-sidecar/workspace-routes.js.map +1 -0
- package/dist/app-sidecar.js +518 -3978
- package/dist/app-sidecar.js.map +1 -1
- package/dist/cli.js +6 -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 +250 -0
- package/dist/core/agent-session/verification-tracker.js.map +1 -0
- package/dist/core/agent-session.d.ts +29 -89
- package/dist/core/agent-session.js +167 -705
- 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-detection.d.ts +11 -0
- package/dist/core/checklist-detection.js +238 -0
- package/dist/core/checklist-detection.js.map +1 -0
- 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 +189 -0
- package/dist/core/checklist-items.js +226 -0
- package/dist/core/checklist-items.js.map +1 -0
- package/dist/core/checklist-prompt.d.ts +7 -0
- package/dist/core/checklist-prompt.js +45 -0
- package/dist/core/checklist-prompt.js.map +1 -0
- package/dist/core/checklist-snapshot.d.ts +17 -0
- package/dist/core/checklist-snapshot.js +46 -0
- package/dist/core/checklist-snapshot.js.map +1 -0
- package/dist/core/checklist-store.d.ts +59 -0
- package/dist/core/checklist-store.js +242 -0
- package/dist/core/checklist-store.js.map +1 -0
- package/dist/core/context-limits.js +1 -1
- package/dist/core/context-limits.js.map +1 -1
- package/dist/core/lsp/client.d.ts +4 -0
- package/dist/core/lsp/client.js +71 -3
- package/dist/core/lsp/client.js.map +1 -1
- package/dist/core/lsp/manager.d.ts +1 -0
- package/dist/core/lsp/manager.js +9 -3
- package/dist/core/lsp/manager.js.map +1 -1
- package/dist/core/lsp/project-root.d.ts +9 -0
- package/dist/core/lsp/project-root.js +75 -0
- package/dist/core/lsp/project-root.js.map +1 -0
- package/dist/core/lsp/reference-project.d.ts +6 -0
- package/dist/core/lsp/reference-project.js +110 -0
- package/dist/core/lsp/reference-project.js.map +1 -0
- package/dist/core/lsp/servers.js +1 -0
- package/dist/core/lsp/servers.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/prompt-commands.js +95 -0
- package/dist/core/prompt-commands.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/core/subagent-manager.d.ts +2 -1
- package/dist/core/subagent-manager.js +8 -5
- package/dist/core/subagent-manager.js.map +1 -1
- package/dist/core/verification-evidence.d.ts +8 -0
- package/dist/core/verification-evidence.js +27 -5
- package/dist/core/verification-evidence.js.map +1 -1
- package/dist/modes/acp-mode.d.ts +2 -1
- package/dist/modes/acp-mode.js +4 -1
- package/dist/modes/acp-mode.js.map +1 -1
- package/dist/modes/subagent-worker-mode.d.ts +2 -0
- package/dist/modes/subagent-worker-mode.js +1 -3
- package/dist/modes/subagent-worker-mode.js.map +1 -1
- package/dist/system-prompt.js +52 -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.d.ts +4 -1
- package/dist/tools/bash.js +46 -81
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/checklist.d.ts +48 -0
- package/dist/tools/checklist.js +146 -0
- package/dist/tools/checklist.js.map +1 -0
- package/dist/tools/code-nav.d.ts +1 -1
- package/dist/tools/code-nav.js +9 -30
- package/dist/tools/code-nav.js.map +1 -1
- package/dist/tools/edit.d.ts +3 -9
- package/dist/tools/edit.js +92 -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.d.ts +2 -2
- package/dist/tools/exit-plan.js +9 -4
- 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/index.d.ts +5 -3
- package/dist/tools/index.js +5 -1
- package/dist/tools/index.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 +35 -46
- 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-shared.d.ts +6 -8
- package/dist/tools/subagent-shared.js +17 -11
- package/dist/tools/subagent-shared.js.map +1 -1
- package/dist/tools/subagent.d.ts +2 -1
- package/dist/tools/subagent.js +7 -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/tasks.d.ts +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 +21 -4
- package/dist/tools/tool-tiers.js +45 -17
- package/dist/tools/tool-tiers.js.map +1 -1
- package/dist/tools/verification-review.d.ts +2 -0
- package/dist/tools/verification-review.js +41 -0
- package/dist/tools/verification-review.js.map +1 -0
- 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/App.js +1 -0
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/components/Footer.js +1 -2
- package/dist/ui/components/Footer.js.map +1 -1
- package/dist/ui/hooks/useModeState.js +15 -0
- package/dist/ui/hooks/useModeState.js.map +1 -1
- package/dist/ui/tool-line-summary.js +11 -1
- package/dist/ui/tool-line-summary.js.map +1 -1
- package/dist/ui/transcript/tool-presentation.js +1 -1
- package/dist/ui/transcript/tool-presentation.js.map +1 -1
- package/dist/utils/shared-poll.d.ts +37 -0
- package/dist/utils/shared-poll.js +104 -0
- package/dist/utils/shared-poll.js.map +1 -0
- package/package.json +8 -5
- 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,27 @@ import { dualAuthProvider } from "@prestyj/core";
|
|
|
13
13
|
import { getClaudeCliUserAgent, recordRequiredClaudeCodeVersion } 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
27
|
import { formatImpactForVerification } from "./test-impact.js";
|
|
28
|
-
import { autoBackgroundedId } from "../tools/bash.js";
|
|
29
28
|
import { buildProcessCompletionFollowUp } from "./process-gate.js";
|
|
29
|
+
import { PLAN_SUBMISSION_NUDGE, shouldNudgePlanSubmission } from "./plan-submission-gate.js";
|
|
30
30
|
import { buildSubAgentCompletionFollowUp } from "./subagent-manager.js";
|
|
31
31
|
import { applyAsyncSubagentPolicy } from "./subagent-policy.js";
|
|
32
32
|
import { resolveResponsesLite, resolveStrictTools, } from "./codex-request-shape.js";
|
|
33
33
|
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";
|
|
34
|
+
import { getAllMcpServers } from "./mcp/index.js";
|
|
37
35
|
import { CONTEXT_LIMITS, resolveSessionContextLimits, } from "./context-limits.js";
|
|
38
|
-
import { McpCatalogCache } from "./mcp/catalog-cache.js";
|
|
39
36
|
import { describeDropped, importForeignSession, } from "./foreign-session-import.js";
|
|
40
|
-
import { createToolSearchTool } from "../tools/tool-search.js";
|
|
41
37
|
import { createSessionStatsTool } from "../tools/session-stats.js";
|
|
42
38
|
import { createDiagnoseCommand, isInternalDiagnosticsEnabled, SessionDiagnosticsRecorder, } from "./internal-diagnostics.js";
|
|
43
39
|
import { log } from "./logger.js";
|
|
@@ -54,15 +50,13 @@ import { pruneStaleToolResults } from "./compaction/tool-result-pruner.js";
|
|
|
54
50
|
import { discoverAgents } from "./agents.js";
|
|
55
51
|
import { enhancePrompt } from "../utils/prompt-enhancer.js";
|
|
56
52
|
import { detectLanguages, detectProjectStack } from "./language-detector.js";
|
|
57
|
-
import {
|
|
53
|
+
import { buildLoopBreakMessage } from "./loop-breaker.js";
|
|
58
54
|
import { buildRegroundingMessage, requestTextForRegrounding } from "./regrounding.js";
|
|
59
|
-
import { buildSemanticLoopJudgePrompt, buildSemanticLoopMessage,
|
|
55
|
+
import { buildSemanticLoopJudgePrompt, buildSemanticLoopMessage, parseSemanticLoopVerdict, SEMANTIC_LOOP_JUDGE_TIMEOUT_MS, withJudgeTimeout, } from "./semantic-loop-check.js";
|
|
60
56
|
import { buildEnvDeltaMessage } from "./env-delta.js";
|
|
61
57
|
import { wrapSteeringText, buildNotificationSteeringText, STEERING_PREFIX } from "./steering.js";
|
|
62
58
|
import { AgentNotificationQueue } from "./agent-notifications.js";
|
|
63
|
-
import { VerificationGate,
|
|
64
|
-
import { classifyVerificationCommand } from "./verification-evidence.js";
|
|
65
|
-
import { captureVerificationSnapshot } from "./verification-snapshot.js";
|
|
59
|
+
import { VerificationGate, VERIFICATION_STATE_KIND } from "./verification-gate.js";
|
|
66
60
|
import { findUserSessionPrompt, getUserSessionPrompt } from "./session-preview.js";
|
|
67
61
|
import { normalizeMessageImages } from "./message-images.js";
|
|
68
62
|
import { loadStreamRules } from "./stream-rules.js";
|
|
@@ -70,41 +64,12 @@ import crypto from "node:crypto";
|
|
|
70
64
|
import fs from "node:fs/promises";
|
|
71
65
|
import os from "node:os";
|
|
72
66
|
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
|
-
}
|
|
67
|
+
import { resolveSessionToolResultCharLimit, resolveSessionTurnToolResultCharLimit, hasUnresolvedToolCalls, resolveHomePath, } from "./agent-session/helpers.js";
|
|
68
|
+
import { RunLoopMonitor } from "./agent-session/run-loop-monitor.js";
|
|
69
|
+
import { VerificationTracker } from "./agent-session/verification-tracker.js";
|
|
70
|
+
import { McpToolRegistry } from "./agent-session/mcp-tools.js";
|
|
71
|
+
import { TranscriptMarkers } from "./agent-session/transcript-markers.js";
|
|
72
|
+
export { resolveSessionToolResultCharLimit, resolveSessionTurnToolResultCharLimit, } from "./agent-session/helpers.js";
|
|
108
73
|
// ── Agent Session ──────────────────────────────────────────
|
|
109
74
|
export class AgentSession {
|
|
110
75
|
eventBus = new EventBus();
|
|
@@ -114,24 +79,10 @@ export class AgentSession {
|
|
|
114
79
|
sessionManager;
|
|
115
80
|
extensionLoader = new ExtensionLoader();
|
|
116
81
|
messages = [];
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
// alongside the session and reloaded on resume
|
|
120
|
-
|
|
121
|
-
// webview can interleave them chronologically.
|
|
122
|
-
nolanTurns = [];
|
|
123
|
-
// Autopilot Nolan (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 nolanTurns — advisory only,
|
|
126
|
-
// persisted + reloaded so a resumed session shows the identical Nolan 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 nolanTurns —
|
|
131
|
-
// display only, persisted + reloaded so a resumed session shows the same
|
|
132
|
-
// transcript rows the live run showed.
|
|
133
|
-
appMarkers = [];
|
|
134
|
-
turnMetrics = [];
|
|
82
|
+
// Display-only transcript history (Nolan turns, autopilot verdicts, app markers,
|
|
83
|
+
// turn metrics). NEVER part of `messages` (EZ Coder must not see them), but
|
|
84
|
+
// persisted alongside the session and reloaded on resume.
|
|
85
|
+
markers = new TranscriptMarkers(() => this.sessionManager, () => this.sessionPath);
|
|
135
86
|
cacheDiagnostics = new CacheDiagnostics();
|
|
136
87
|
/** Internal-only (EZ_INTERNAL): live per-session cost/reliability recorder.
|
|
137
88
|
* Absent entirely in public builds — see core/internal-diagnostics.ts. */
|
|
@@ -150,25 +101,8 @@ export class AgentSession {
|
|
|
150
101
|
// ── Self-correction hook state (mirrors the TUI's useAgentLoop refs) ──
|
|
151
102
|
// Reset at the start of every run; observed from the event stream; read by
|
|
152
103
|
// 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;
|
|
104
|
+
loopMonitor = new RunLoopMonitor();
|
|
165
105
|
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
106
|
/**
|
|
173
107
|
* The environment as the cached system prompt currently describes it.
|
|
174
108
|
* Re-recorded on every prompt build, so a rebuild (e.g. `/add-dir`) needs no
|
|
@@ -179,10 +113,14 @@ export class AgentSession {
|
|
|
179
113
|
runStartedAt = 0;
|
|
180
114
|
/** Gate injections spent this run, capped by MAX_PROCESS_GATE_INJECTIONS. */
|
|
181
115
|
processGateInjected = 0;
|
|
116
|
+
/** Plan-submission reminders spent this run (plan-submission-gate.ts). */
|
|
117
|
+
planSubmissionNudges = 0;
|
|
182
118
|
/** Verification gate: code edited this run, nothing proved it since. Always
|
|
183
119
|
* tracks evidence for run status; the `verificationGateEnabled` setting
|
|
184
120
|
* decides whether an unverified stop is also continued once. */
|
|
185
121
|
verificationGate = new VerificationGate();
|
|
122
|
+
/** Host-observed evidence (edited files, in-flight and background checks) feeding the gate. */
|
|
123
|
+
verificationTracker;
|
|
186
124
|
/** Mirror of the last `hook_armed` value, so the event fires only on an edge. */
|
|
187
125
|
preFinalArmed = false;
|
|
188
126
|
compactionOccurred = false;
|
|
@@ -249,9 +187,8 @@ export class AgentSession {
|
|
|
249
187
|
this.lspManager?.clearPendingDiagnostics();
|
|
250
188
|
void this.subAgentManager?.interruptAll();
|
|
251
189
|
};
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
mcpCatalog;
|
|
190
|
+
/** MCP client, deferred tool_search catalog and cached-stub reconcile state. */
|
|
191
|
+
mcp = new McpToolRegistry(() => this.tools, () => this.contextLimits);
|
|
255
192
|
/** Resolved prompt-injection byte budgets (contextLimits setting). */
|
|
256
193
|
contextLimits = CONTEXT_LIMITS;
|
|
257
194
|
/**
|
|
@@ -260,11 +197,6 @@ export class AgentSession {
|
|
|
260
197
|
* can discover and promote them; a promoted name drops out of this list.
|
|
261
198
|
*/
|
|
262
199
|
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
200
|
provider;
|
|
269
201
|
model;
|
|
270
202
|
cwd;
|
|
@@ -337,6 +269,7 @@ export class AgentSession {
|
|
|
337
269
|
opts;
|
|
338
270
|
constructor(options) {
|
|
339
271
|
this.opts = options;
|
|
272
|
+
this.verificationTracker = new VerificationTracker(this.verificationGate, options.cwd, () => this.processManager?.list() ?? []);
|
|
340
273
|
this.provider = options.provider;
|
|
341
274
|
this.model = options.model;
|
|
342
275
|
this.cwd = options.cwd;
|
|
@@ -438,12 +371,13 @@ export class AgentSession {
|
|
|
438
371
|
authStorage: this.authStorage,
|
|
439
372
|
onFileMutated: (filePath) => {
|
|
440
373
|
const relative = path.relative(this.cwd, filePath) || path.basename(filePath);
|
|
441
|
-
this.
|
|
374
|
+
this.verificationTracker.recordFileMutated(relative);
|
|
442
375
|
},
|
|
443
376
|
// Lazy — sessionId/model/provider can change after createTools() runs, so
|
|
444
377
|
// sub-agent spawns read the current parent state at execution time.
|
|
445
378
|
getProvider: () => this.provider,
|
|
446
379
|
getModel: () => this.model,
|
|
380
|
+
getThinkingLevel: () => this.thinkingLevel,
|
|
447
381
|
getBaseUrl: () => this.baseUrl,
|
|
448
382
|
getCacheKey: () => this.getPromptCacheKey(),
|
|
449
383
|
getMaxPerModel: () => this.settingsManager.get("subagentMaxPerModel"),
|
|
@@ -484,6 +418,10 @@ export class AgentSession {
|
|
|
484
418
|
// every request. Allow-listed sessions keep the eager path — their fixed
|
|
485
419
|
// tool expectations predate the catalog, and tool_search isn't allow-listed.
|
|
486
420
|
if (!this.opts.allowedTools && this.settingsManager.get("deferredBuiltinTools")) {
|
|
421
|
+
// Wrap before partitioning: spawn_agent itself may be deferred, and the
|
|
422
|
+
// catalog must hold the wrapped tool so a promoted spawn still loads
|
|
423
|
+
// wait_agent.
|
|
424
|
+
this.promoteFollowUpTools();
|
|
487
425
|
const { core, deferred } = partitionToolsByTier(this.tools);
|
|
488
426
|
if (deferred.length > 0) {
|
|
489
427
|
// Append-only: `core` preserves the original relative order and
|
|
@@ -491,10 +429,7 @@ export class AgentSession {
|
|
|
491
429
|
// sits inside the cached prefix stays byte-stable across turns.
|
|
492
430
|
this.tools = core;
|
|
493
431
|
this.deferredBuiltinToolNames = deferred.map((t) => t.name);
|
|
494
|
-
this.
|
|
495
|
-
this.mcpCatalog.add(deferred);
|
|
496
|
-
this.ensureToolSearchTool();
|
|
497
|
-
this.promoteWaitAgentAfterSpawn();
|
|
432
|
+
this.mcp.addDeferred(deferred);
|
|
498
433
|
}
|
|
499
434
|
}
|
|
500
435
|
this.rebuildReadTool = rebuildReadTool;
|
|
@@ -509,8 +444,7 @@ export class AgentSession {
|
|
|
509
444
|
// Connect MCP servers. Child sessions skip user-configured servers to avoid
|
|
510
445
|
// cold-spawning every server per short-lived worker. Background connection
|
|
511
446
|
// keeps slow first-run stdio servers off the host readiness path.
|
|
512
|
-
this.
|
|
513
|
-
catalogCache: this.mcpCatalogCache,
|
|
447
|
+
this.mcp.createManager({
|
|
514
448
|
modernProtocol: this.settingsManager.get("mcpModernProtocol"),
|
|
515
449
|
onElicit: this.opts.onMcpElicit,
|
|
516
450
|
});
|
|
@@ -659,7 +593,8 @@ export class AgentSession {
|
|
|
659
593
|
* turn, so background-connected servers become available on the next prompt.
|
|
660
594
|
*/
|
|
661
595
|
async connectMcpServers() {
|
|
662
|
-
|
|
596
|
+
const mcpManager = this.mcp.manager;
|
|
597
|
+
if (!mcpManager)
|
|
663
598
|
return;
|
|
664
599
|
// Allow-listed (read-only advisory) sessions enforce a fixed tool set by
|
|
665
600
|
// name. An MCP server is only connected when its name is explicitly
|
|
@@ -695,7 +630,7 @@ export class AgentSession {
|
|
|
695
630
|
// empty catalog and tool_search would answer "the catalog is empty" for
|
|
696
631
|
// capabilities that genuinely exist — a wrong answer, not a slow one.
|
|
697
632
|
await this.seedMcpCatalogFromCache(servers);
|
|
698
|
-
const connected = await
|
|
633
|
+
const connected = await mcpManager.connectAll(servers);
|
|
699
634
|
// Defense-in-depth: even from a whitelisted server, only push tools that
|
|
700
635
|
// pass the allow-list (no-op when there's no allow-list).
|
|
701
636
|
const mcpTools = this.opts.allowedTools
|
|
@@ -734,170 +669,65 @@ export class AgentSession {
|
|
|
734
669
|
* pushed eagerly when the user opted out.
|
|
735
670
|
* Allow-listed sessions (Nolan) always get the eager path — their fixed tool
|
|
736
671
|
* expectations predate the catalog, and tool_search isn't allow-listed.
|
|
737
|
-
* Promotion pushes onto the live `this.tools` array the running agent loop
|
|
738
|
-
* re-reads every turn, so promoted tools are callable on the next step.
|
|
739
672
|
*/
|
|
740
673
|
addMcpTools(mcpTools) {
|
|
741
|
-
if (mcpTools.length === 0)
|
|
742
|
-
return;
|
|
743
|
-
for (const tool of mcpTools) {
|
|
744
|
-
this.liveMcpTools.set(tool.name, tool);
|
|
745
|
-
this.cachedMcpToolServers.delete(tool.name);
|
|
746
|
-
}
|
|
747
674
|
const defer = !this.opts.allowedTools && this.settingsManager.get("deferredMcpTools");
|
|
748
|
-
|
|
749
|
-
// Eager path bypasses the catalog, so budget descriptions here too.
|
|
750
|
-
this.replaceOrPushTools(mcpTools.map((tool) => clampMcpToolDescription(tool, this.contextLimits)));
|
|
751
|
-
return;
|
|
752
|
-
}
|
|
753
|
-
this.mcpCatalog ??= new DeferredToolCatalog(this.contextLimits);
|
|
754
|
-
// `add` is name-keyed, so live definitions replace cached stubs in place.
|
|
755
|
-
this.mcpCatalog.add(mcpTools);
|
|
756
|
-
// A stub the model already promoted lives in `this.tools`; swap it for the
|
|
757
|
-
// live tool so later calls dispatch directly instead of through the stub.
|
|
758
|
-
this.replaceLivePromotedTools(mcpTools);
|
|
759
|
-
this.ensureToolSearchTool();
|
|
675
|
+
this.mcp.addMcpTools(mcpTools, defer);
|
|
760
676
|
}
|
|
761
677
|
/**
|
|
762
|
-
*
|
|
763
|
-
*
|
|
764
|
-
*
|
|
765
|
-
*
|
|
766
|
-
*
|
|
767
|
-
*
|
|
768
|
-
*
|
|
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.
|
|
769
686
|
*/
|
|
770
|
-
|
|
771
|
-
this.
|
|
772
|
-
|
|
773
|
-
return;
|
|
774
|
-
this.tools.push(createToolSearchTool(this.mcpCatalog, (promoted) => {
|
|
775
|
-
this.tools.push(...promoted);
|
|
776
|
-
}, async (toolName) => {
|
|
777
|
-
if (this.liveMcpTools.has(toolName))
|
|
778
|
-
return undefined;
|
|
779
|
-
const serverName = this.cachedMcpToolServers.get(toolName);
|
|
780
|
-
if (!serverName)
|
|
781
|
-
return undefined;
|
|
782
|
-
const outcome = (await this.mcpManager?.whenConnected(serverName)) ?? {
|
|
783
|
-
ok: false,
|
|
784
|
-
error: "MCP is disabled for this session",
|
|
785
|
-
};
|
|
786
|
-
return outcome.ok
|
|
787
|
-
? { serverName, ok: true }
|
|
788
|
-
: { serverName, ok: false, error: outcome.error };
|
|
789
|
-
}, 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);
|
|
790
690
|
}
|
|
791
|
-
/**
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
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)
|
|
802
707
|
return;
|
|
803
708
|
this.tools[index] = {
|
|
804
|
-
...
|
|
709
|
+
...tool,
|
|
805
710
|
execute: async (args, context) => {
|
|
806
|
-
const result = await
|
|
807
|
-
if (
|
|
808
|
-
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));
|
|
809
716
|
}
|
|
810
717
|
return result;
|
|
811
718
|
},
|
|
812
719
|
};
|
|
813
720
|
}
|
|
814
|
-
/** Append tools, replacing any same-named entry (cached stub → live tool). */
|
|
815
|
-
replaceOrPushTools(tools) {
|
|
816
|
-
for (const tool of tools) {
|
|
817
|
-
const index = this.tools.findIndex((t) => t.name === tool.name);
|
|
818
|
-
if (index >= 0)
|
|
819
|
-
this.tools[index] = tool;
|
|
820
|
-
else
|
|
821
|
-
this.tools.push(tool);
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
/** Swap already-promoted cached stubs for their live equivalents, in place. */
|
|
825
|
-
replaceLivePromotedTools(tools) {
|
|
826
|
-
for (const tool of tools) {
|
|
827
|
-
const index = this.tools.findIndex((t) => t.name === tool.name);
|
|
828
|
-
if (index >= 0)
|
|
829
|
-
this.tools[index] = tool;
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
721
|
/**
|
|
833
722
|
* Publish cached tool definitions into the deferred catalog so `tool_search`
|
|
834
|
-
* answers correctly on turn 1
|
|
835
|
-
* description and input schema; calling it waits for the live connection and
|
|
836
|
-
* then dispatches against the real client, or returns a clear error when that
|
|
837
|
-
* server ultimately failed. Live tools replace stubs on connect.
|
|
723
|
+
* answers correctly on turn 1 (see {@link McpToolRegistry.seedFromCache}).
|
|
838
724
|
*/
|
|
839
725
|
async seedMcpCatalogFromCache(servers) {
|
|
840
726
|
if (!this.opts.backgroundMcpConnect)
|
|
841
727
|
return;
|
|
842
728
|
if (this.opts.allowedTools || !this.settingsManager.get("deferredMcpTools"))
|
|
843
729
|
return;
|
|
844
|
-
|
|
845
|
-
try {
|
|
846
|
-
entries = await this.mcpCatalogCache.entriesFor(servers);
|
|
847
|
-
}
|
|
848
|
-
catch {
|
|
849
|
-
return;
|
|
850
|
-
}
|
|
851
|
-
const stubs = [];
|
|
852
|
-
for (const [serverName, entry] of entries) {
|
|
853
|
-
for (const cached of entry.tools) {
|
|
854
|
-
if (this.liveMcpTools.has(cached.name))
|
|
855
|
-
continue;
|
|
856
|
-
this.cachedMcpToolServers.set(cached.name, serverName);
|
|
857
|
-
stubs.push(this.buildCachedMcpTool(serverName, cached));
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
if (stubs.length === 0)
|
|
861
|
-
return;
|
|
862
|
-
log("INFO", "mcp", "Seeded deferred tool catalog from cache", {
|
|
863
|
-
tools: String(stubs.length),
|
|
864
|
-
servers: String(entries.size),
|
|
865
|
-
});
|
|
866
|
-
this.addCachedMcpTools(stubs);
|
|
867
|
-
}
|
|
868
|
-
/** Catalog-only registration for cached stubs — never marks them live. */
|
|
869
|
-
addCachedMcpTools(stubs) {
|
|
870
|
-
this.mcpCatalog ??= new DeferredToolCatalog(this.contextLimits);
|
|
871
|
-
this.mcpCatalog.add(stubs);
|
|
872
|
-
this.ensureToolSearchTool();
|
|
873
|
-
}
|
|
874
|
-
buildCachedMcpTool(serverName, cached) {
|
|
875
|
-
return {
|
|
876
|
-
name: cached.name,
|
|
877
|
-
description: cached.description,
|
|
878
|
-
parameters: z.record(z.string(), z.unknown()),
|
|
879
|
-
rawInputSchema: cached.rawInputSchema,
|
|
880
|
-
execute: async (args, context) => {
|
|
881
|
-
const live = this.liveMcpTools.get(cached.name);
|
|
882
|
-
if (live)
|
|
883
|
-
return live.execute(args, context);
|
|
884
|
-
const outcome = (await this.mcpManager?.whenConnected(serverName)) ?? {
|
|
885
|
-
ok: false,
|
|
886
|
-
error: "MCP is disabled for this session",
|
|
887
|
-
};
|
|
888
|
-
if (!outcome.ok) {
|
|
889
|
-
return (`MCP tool ${cached.name} is unavailable: server "${serverName}" did not connect ` +
|
|
890
|
-
`(${outcome.error}). This tool was offered from a cached catalog. ` +
|
|
891
|
-
`Use a different approach or ask the user to check their MCP configuration.`);
|
|
892
|
-
}
|
|
893
|
-
const connected = this.liveMcpTools.get(cached.name);
|
|
894
|
-
if (!connected) {
|
|
895
|
-
return (`MCP tool ${cached.name} no longer exists: server "${serverName}" connected but ` +
|
|
896
|
-
`does not expose it. The cached catalog entry was stale.`);
|
|
897
|
-
}
|
|
898
|
-
return connected.execute(args, context);
|
|
899
|
-
},
|
|
900
|
-
};
|
|
730
|
+
await this.mcp.seedFromCache(servers);
|
|
901
731
|
}
|
|
902
732
|
/**
|
|
903
733
|
* Resolve a `/name [args]` input to the prompt template it expands into, or
|
|
@@ -1084,33 +914,15 @@ export class AgentSession {
|
|
|
1084
914
|
resetHookState(originalRequest) {
|
|
1085
915
|
this.opts.completionReview?.begin(originalRequest);
|
|
1086
916
|
this.lspManager?.clearPendingDiagnostics();
|
|
1087
|
-
this.
|
|
1088
|
-
this.
|
|
1089
|
-
this.hookConsecutiveFailures = 0;
|
|
1090
|
-
this.hookRepeatedNoProgressCalls = 0;
|
|
1091
|
-
this.hookProgressTracker.reset();
|
|
1092
|
-
this.hookCycleDetector.reset();
|
|
1093
|
-
this.hookCyclicPattern = null;
|
|
1094
|
-
this.hookFileEditCounts.clear();
|
|
1095
|
-
this.hookToolCalls.clear();
|
|
1096
|
-
this.loopBreakInjected = 0;
|
|
917
|
+
this.loopMonitor.reset();
|
|
918
|
+
this.verificationTracker.resetRun();
|
|
1097
919
|
this.regroundingInjected = false;
|
|
1098
|
-
this.hookRecentCalls = [];
|
|
1099
|
-
this.semanticLoop = {
|
|
1100
|
-
checksUsed: 0,
|
|
1101
|
-
lastCheckTurn: 0,
|
|
1102
|
-
pending: false,
|
|
1103
|
-
verdict: null,
|
|
1104
|
-
injected: false,
|
|
1105
|
-
};
|
|
1106
920
|
this.runStartedAt = Date.now();
|
|
1107
921
|
this.processGateInjected = 0;
|
|
922
|
+
this.planSubmissionNudges = 0;
|
|
1108
923
|
this.verificationGate.beginRun();
|
|
1109
924
|
const processes = new Set(this.processManager?.list().map((p) => p.id) ?? []);
|
|
1110
|
-
|
|
1111
|
-
if (!processes.has(id))
|
|
1112
|
-
this.backgroundVerification.delete(id);
|
|
1113
|
-
}
|
|
925
|
+
this.verificationTracker.pruneBackground(processes);
|
|
1114
926
|
this.compactionOccurred = false;
|
|
1115
927
|
// Post-turn compaction may have landed between runs — adopt its armed
|
|
1116
928
|
// signal so the re-grounding hook fires for this run exactly as it would
|
|
@@ -1134,153 +946,20 @@ export class AgentSession {
|
|
|
1134
946
|
}
|
|
1135
947
|
switch (event.type) {
|
|
1136
948
|
case "text_delta":
|
|
1137
|
-
this.
|
|
949
|
+
this.loopMonitor.recordText(event.text);
|
|
1138
950
|
break;
|
|
1139
951
|
case "tool_call_start": {
|
|
1140
|
-
this.
|
|
1141
|
-
name: event.name,
|
|
1142
|
-
args: event.args ?? {},
|
|
1143
|
-
revision: this.verificationGate.revision,
|
|
1144
|
-
});
|
|
1145
|
-
const startClassification = event.name === "bash" && typeof event.args?.command === "string"
|
|
1146
|
-
? classifyVerificationCommand(event.args.command)
|
|
1147
|
-
: null;
|
|
1148
|
-
if (startClassification &&
|
|
1149
|
-
typeof event.args?.command === "string" &&
|
|
1150
|
-
(isVerificationCommand(event.args.command) ||
|
|
1151
|
-
startClassification.accepted ||
|
|
1152
|
-
// Must match the tool_call_end predicate: a snapshot-eligible
|
|
1153
|
-
// command that never captured a "before" snapshot is misread at
|
|
1154
|
-
// the end as an uncomparable workspace and re-arms the gate.
|
|
1155
|
-
startClassification.snapshotEligible === true)) {
|
|
1156
|
-
// A check that can rewrite files (--fix, build scripts, emitters)
|
|
1157
|
-
// invalidates earlier in-flight evidence AND marks the run as
|
|
1158
|
-
// touched. A check that is merely UNRECOGNIZED (`make test`, `deno
|
|
1159
|
-
// test`) rewrites nothing we can point to: bumping the revision for
|
|
1160
|
-
// it poisoned the gate on green output and re-armed the hook into
|
|
1161
|
-
// every later question turn.
|
|
1162
|
-
this.verificationGate.recordVerificationAttempt();
|
|
1163
|
-
const classification = startClassification;
|
|
1164
|
-
if (classification.snapshotEligible && event.args.persist !== true) {
|
|
1165
|
-
const call = this.hookToolCalls.get(event.toolCallId);
|
|
1166
|
-
call.sourceSnapshot = await captureVerificationSnapshot(this.opts.cwd, [
|
|
1167
|
-
...this.hookFileEditCounts.keys(),
|
|
1168
|
-
]);
|
|
1169
|
-
if (call.sourceSnapshot === null)
|
|
1170
|
-
this.verificationGate.requireFreshVerification(true, event.args.command);
|
|
1171
|
-
}
|
|
1172
|
-
else if ((classification.accepted && event.args.persist !== true) ||
|
|
1173
|
-
(!classification.accepted && classification.mayMutate)) {
|
|
1174
|
-
// Flag the workspace unknown only when tool_call_end can resolve
|
|
1175
|
-
// it: a bounded check records pass/fail, a file-rewriting command
|
|
1176
|
-
// bumps the revision. An unrecognized read-only check (`biome ci`)
|
|
1177
|
-
// or a persistent-shell run records nothing at the end, so
|
|
1178
|
-
// flagging it left verified work Unverified forever — and
|
|
1179
|
-
// autopilot silently refused every later turn.
|
|
1180
|
-
this.verificationGate.requireFreshVerification(!classification.accepted && classification.mayMutate, event.args.command);
|
|
1181
|
-
}
|
|
952
|
+
if (await this.verificationTracker.recordToolCallStart(event)) {
|
|
1182
953
|
await this.persistVerificationState();
|
|
1183
954
|
}
|
|
1184
955
|
break;
|
|
1185
956
|
}
|
|
1186
957
|
case "tool_call_end": {
|
|
1187
|
-
const call = this.
|
|
958
|
+
const call = this.verificationTracker.getToolCall(event.toolCallId);
|
|
1188
959
|
const name = call?.name ?? "";
|
|
1189
960
|
const args = call?.args;
|
|
1190
|
-
this.
|
|
1191
|
-
|
|
1192
|
-
this.hookStats.toolFailures += 1;
|
|
1193
|
-
this.hookConsecutiveFailures = event.isError ? this.hookConsecutiveFailures + 1 : 0;
|
|
1194
|
-
this.hookRepeatedNoProgressCalls = this.hookProgressTracker.record(name, args, event.result, event.isError);
|
|
1195
|
-
this.hookCyclicPattern = this.hookCycleDetector.record(name, args, event.result, event.isError);
|
|
1196
|
-
// Semantic-loop judge input: a bounded digest of WHAT was attempted and
|
|
1197
|
-
// HOW it came out. Args/results are sliced AT RECORD TIME — a write with
|
|
1198
|
-
// a 50 KiB payload or a bash dump must never inflate the ring, and the
|
|
1199
|
-
// judge needs shapes, not payloads.
|
|
1200
|
-
this.hookRecentCalls.push({
|
|
1201
|
-
tool: name,
|
|
1202
|
-
args: args === undefined ? "" : JSON.stringify(args).slice(0, 300),
|
|
1203
|
-
ok: !event.isError,
|
|
1204
|
-
result: event.result.slice(0, 400),
|
|
1205
|
-
});
|
|
1206
|
-
if (this.hookRecentCalls.length > MAX_SEMANTIC_LOOP_CALLS) {
|
|
1207
|
-
this.hookRecentCalls.splice(0, this.hookRecentCalls.length - MAX_SEMANTIC_LOOP_CALLS);
|
|
1208
|
-
}
|
|
1209
|
-
// Only host-observed successful mutations and trustworthy check results
|
|
1210
|
-
// affect approval. The model's text is never evidence.
|
|
1211
|
-
let verificationChanged = false;
|
|
1212
|
-
if (!event.isError && args) {
|
|
1213
|
-
if (name === "edit" || name === "write") {
|
|
1214
|
-
// Check-owning files (tsconfig.json, pytest.ini, vitest.config.ts …)
|
|
1215
|
-
// are tracked even when they are not source code: editing one is how
|
|
1216
|
-
// a red suite is turned green without fixing anything. A multi-file
|
|
1217
|
-
// edit records every file; its combined diff is scanned for each.
|
|
1218
|
-
const addedText = name === "write"
|
|
1219
|
-
? String(args.content ?? "")
|
|
1220
|
-
: extractAddedLines(event.details?.diff ?? event.result);
|
|
1221
|
-
for (const filePath of editTargetPaths(args)) {
|
|
1222
|
-
if (isCodeFilePath(filePath) || isCheckOwnFile(filePath)) {
|
|
1223
|
-
this.verificationGate.recordMutation(filePath, addedText);
|
|
1224
|
-
verificationChanged = true;
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
if (args && call && name === "bash") {
|
|
1230
|
-
const command = typeof args.command === "string" ? args.command : "";
|
|
1231
|
-
const classification = classifyVerificationCommand(command);
|
|
1232
|
-
if (classification.accepted || classification.snapshotEligible) {
|
|
1233
|
-
// A foreground check that outlived the default budget was moved to
|
|
1234
|
-
// the background, not failed: track it to its real exit the same way.
|
|
1235
|
-
const autoBackgroundId = event.isError ? undefined : autoBackgroundedId(event.result);
|
|
1236
|
-
if ((autoBackgroundId !== undefined || args.run_in_background === true) &&
|
|
1237
|
-
!event.isError &&
|
|
1238
|
-
args.persist !== true) {
|
|
1239
|
-
const id = autoBackgroundId ?? /^ID:\s*(\S+)/m.exec(event.result)?.[1];
|
|
1240
|
-
// No parseable ID means the check cannot be tracked to a real exit
|
|
1241
|
-
// code — no evidence either way. Recording a FAILURE here made
|
|
1242
|
-
// every later green run of a different spelling look owed.
|
|
1243
|
-
if (id)
|
|
1244
|
-
this.backgroundVerification.set(id, {
|
|
1245
|
-
revision: call.revision,
|
|
1246
|
-
command,
|
|
1247
|
-
...(classification.snapshotEligible
|
|
1248
|
-
? { sourceSnapshot: call.sourceSnapshot ?? null }
|
|
1249
|
-
: {}),
|
|
1250
|
-
});
|
|
1251
|
-
else if (classification.snapshotEligible)
|
|
1252
|
-
this.verificationGate.requireFreshVerification(true, command);
|
|
1253
|
-
}
|
|
1254
|
-
else if (args.persist === true) {
|
|
1255
|
-
// Persistent-shell checks are not bounded evidence (steering can
|
|
1256
|
-
// interleave): neither a pass nor a failure. A recorded failure
|
|
1257
|
-
// here blocked approval for sessions that prefer the shell.
|
|
1258
|
-
}
|
|
1259
|
-
else if (classification.snapshotEligible) {
|
|
1260
|
-
await this.finishSnapshotVerification({ command, revision: call.revision, sourceSnapshot: call.sourceSnapshot ?? null }, !event.isError && /^Exit code:\s*0(?:\s|$)/i.test(event.result.trim()));
|
|
1261
|
-
verificationChanged = true;
|
|
1262
|
-
}
|
|
1263
|
-
else {
|
|
1264
|
-
if (!event.isError && /^Exit code:\s*0(?:\s|$)/i.test(event.result.trim())) {
|
|
1265
|
-
this.verificationGate.recordVerification(call.revision, command);
|
|
1266
|
-
}
|
|
1267
|
-
else {
|
|
1268
|
-
this.verificationGate.recordFailedVerification(command, call.revision);
|
|
1269
|
-
}
|
|
1270
|
-
verificationChanged = true;
|
|
1271
|
-
}
|
|
1272
|
-
delete call.sourceSnapshot;
|
|
1273
|
-
}
|
|
1274
|
-
else if (classification.candidate) {
|
|
1275
|
-
// Green but untrusted: remember WHY so the demand can tell the
|
|
1276
|
-
// agent which command shape actually clears the gate.
|
|
1277
|
-
this.verificationGate.recordRejectedCheck(command, classification.reason);
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
|
-
if (!event.isError && args && name === "task_output" && typeof args.id === "string") {
|
|
1281
|
-
verificationChanged =
|
|
1282
|
-
(await this.recordFinishedBackgroundVerification(args.id)) || verificationChanged;
|
|
1283
|
-
}
|
|
961
|
+
this.loopMonitor.recordToolEnd(name, args, event.result, event.isError);
|
|
962
|
+
const verificationChanged = await this.verificationTracker.recordToolCallEnd(event, call, this.planModeRef.current);
|
|
1284
963
|
if (verificationChanged)
|
|
1285
964
|
await this.persistVerificationState();
|
|
1286
965
|
// Tool results are what push the run over the review gate, and they all
|
|
@@ -1290,7 +969,7 @@ export class AgentSession {
|
|
|
1290
969
|
break;
|
|
1291
970
|
}
|
|
1292
971
|
case "turn_end":
|
|
1293
|
-
this.
|
|
972
|
+
this.loopMonitor.recordTurn(event.turn);
|
|
1294
973
|
this.refreshHookArming();
|
|
1295
974
|
for (let index = this.messages.length - 1; index >= 0; index--) {
|
|
1296
975
|
const anchor = this.messages[index];
|
|
@@ -1467,51 +1146,26 @@ export class AgentSession {
|
|
|
1467
1146
|
// Deterministic stuck verdict, computed once and shared: the semantic
|
|
1468
1147
|
// judge must not spend tokens on a burst the deterministic breaker is
|
|
1469
1148
|
// about to correct itself.
|
|
1470
|
-
const deterministicDecision = evaluateLoopBreak(
|
|
1471
|
-
consecutiveFailures: this.hookConsecutiveFailures,
|
|
1472
|
-
repeatedNoProgressCalls: this.hookRepeatedNoProgressCalls,
|
|
1473
|
-
textRepetitionDetected: detectTextRepetition(this.hookText),
|
|
1474
|
-
...(this.hookCyclicPattern ? { cyclicPattern: this.hookCyclicPattern } : {}),
|
|
1475
|
-
});
|
|
1149
|
+
const deterministicDecision = this.loopMonitor.evaluateLoopBreak();
|
|
1476
1150
|
this.maybeStartSemanticLoopCheck(deterministicDecision.shouldBreak);
|
|
1477
1151
|
// Two-stage loop-breaker: stage 1 nudges; a FRESH detection after that
|
|
1478
1152
|
// injects the harsher final stop-and-report prompt. Signals reset after
|
|
1479
1153
|
// each injection so stage 2 only fires on new evidence.
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
this.hookConsecutiveFailures = 0;
|
|
1489
|
-
this.hookRepeatedNoProgressCalls = 0;
|
|
1490
|
-
// The deterministic breaker owns this burst — a semantic verdict from
|
|
1491
|
-
// the same burst must not double-correct on the next poll.
|
|
1492
|
-
this.semanticLoop.verdict = null;
|
|
1493
|
-
// Clear the text buffer too — otherwise a stage-1 text-repetition
|
|
1494
|
-
// trigger still sees the same repeated tail on the next check and
|
|
1495
|
-
// escalates to stage 2 on stale evidence.
|
|
1496
|
-
this.hookText = "";
|
|
1497
|
-
log("INFO", "loop-break", "Injecting loop-break nudge", {
|
|
1498
|
-
stage: String(stage),
|
|
1499
|
-
reasons: decision.reasons.join(", "),
|
|
1500
|
-
});
|
|
1501
|
-
this.eventBus.emit("hook", { kind: "loop_break" });
|
|
1502
|
-
return [buildLoopBreakMessage(decision.reasons, stage === 2)];
|
|
1503
|
-
}
|
|
1154
|
+
const stage = this.loopMonitor.takeLoopBreakStage(deterministicDecision);
|
|
1155
|
+
if (stage !== null) {
|
|
1156
|
+
log("INFO", "loop-break", "Injecting loop-break nudge", {
|
|
1157
|
+
stage: String(stage),
|
|
1158
|
+
reasons: deterministicDecision.reasons.join(", "),
|
|
1159
|
+
});
|
|
1160
|
+
this.eventBus.emit("hook", { kind: "loop_break" });
|
|
1161
|
+
return [buildLoopBreakMessage(deterministicDecision.reasons, stage === 2)];
|
|
1504
1162
|
}
|
|
1505
1163
|
// Semantic loop-break: an LLM verdict (started by maybeStartSemanticLoopCheck
|
|
1506
1164
|
// on a suspicious-but-syntactically-quiet burst) is consumed here, exactly
|
|
1507
1165
|
// once, with the deterministic breaker getting priority above. Fail-open:
|
|
1508
1166
|
// no verdict or no-loop verdict injects nothing.
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
this.semanticLoop.injected = true;
|
|
1512
|
-
this.semanticLoop.verdict = null;
|
|
1513
|
-
// The judged burst has been addressed; a fresh burst must re-accumulate.
|
|
1514
|
-
this.hookConsecutiveFailures = 0;
|
|
1167
|
+
const verdict = this.loopMonitor.takeSemanticVerdict();
|
|
1168
|
+
if (verdict) {
|
|
1515
1169
|
log("INFO", "loop-break", "Injecting semantic loop-break steering", {
|
|
1516
1170
|
reason: verdict.reason,
|
|
1517
1171
|
});
|
|
@@ -1531,34 +1185,26 @@ export class AgentSession {
|
|
|
1531
1185
|
* Fire-and-forget: the call runs while the next turn streams, and a finished
|
|
1532
1186
|
* verdict is consumed by the NEXT steering poll — never blocking a turn. */
|
|
1533
1187
|
maybeStartSemanticLoopCheck(deterministicBreak) {
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
totalFailures: this.hookStats.toolFailures,
|
|
1537
|
-
turns: this.hookStats.turns,
|
|
1538
|
-
lastCheckTurn: this.semanticLoop.lastCheckTurn,
|
|
1539
|
-
checksUsed: this.semanticLoop.checksUsed,
|
|
1540
|
-
checkPending: this.semanticLoop.pending,
|
|
1541
|
-
deterministicBreak,
|
|
1542
|
-
})) {
|
|
1188
|
+
const monitor = this.loopMonitor;
|
|
1189
|
+
if (!monitor.shouldRunSemanticCheck(deterministicBreak)) {
|
|
1543
1190
|
return;
|
|
1544
1191
|
}
|
|
1545
|
-
// resetHookState replaces
|
|
1192
|
+
// resetHookState replaces the run state on every prompt. A late judge must
|
|
1546
1193
|
// not publish a verdict or consume the next run's budget/cooldown.
|
|
1547
|
-
const runState =
|
|
1548
|
-
runState.pending = true;
|
|
1194
|
+
const runState = monitor.beginSemanticCheck();
|
|
1549
1195
|
log("INFO", "loop-break", "Starting semantic loop judge", {
|
|
1550
|
-
turn: String(
|
|
1551
|
-
consecutiveFailures: String(
|
|
1552
|
-
recentCalls: String(
|
|
1196
|
+
turn: String(monitor.stats.turns),
|
|
1197
|
+
consecutiveFailures: String(monitor.consecutiveFailures),
|
|
1198
|
+
recentCalls: String(monitor.recentCalls.length),
|
|
1553
1199
|
});
|
|
1554
1200
|
void (async () => {
|
|
1555
1201
|
try {
|
|
1556
|
-
const prompt = buildSemanticLoopJudgePrompt(
|
|
1202
|
+
const prompt = buildSemanticLoopJudgePrompt(monitor.recentCalls, this.originalRequest);
|
|
1557
1203
|
const raw = await (this.opts.semanticLoopJudge?.(prompt) ??
|
|
1558
1204
|
this.callSemanticLoopJudge(prompt));
|
|
1559
1205
|
const verdict = parseSemanticLoopVerdict(raw);
|
|
1560
|
-
if (
|
|
1561
|
-
runState
|
|
1206
|
+
if (verdict?.loop)
|
|
1207
|
+
monitor.publishSemanticVerdict(runState, verdict);
|
|
1562
1208
|
}
|
|
1563
1209
|
catch (error) {
|
|
1564
1210
|
// Fail open: judge errors never stop a run. Budget and cooldown are
|
|
@@ -1568,11 +1214,7 @@ export class AgentSession {
|
|
|
1568
1214
|
});
|
|
1569
1215
|
}
|
|
1570
1216
|
finally {
|
|
1571
|
-
|
|
1572
|
-
runState.pending = false;
|
|
1573
|
-
runState.checksUsed += 1;
|
|
1574
|
-
runState.lastCheckTurn = this.hookStats.turns;
|
|
1575
|
-
}
|
|
1217
|
+
monitor.settleSemanticCheck(runState);
|
|
1576
1218
|
}
|
|
1577
1219
|
})();
|
|
1578
1220
|
}
|
|
@@ -1668,23 +1310,7 @@ export class AgentSession {
|
|
|
1668
1310
|
* with — so reuse the same stuck signals that drive the loop-breaker.
|
|
1669
1311
|
*/
|
|
1670
1312
|
shouldExtendTurnBudget(ctx) {
|
|
1671
|
-
const refusals =
|
|
1672
|
-
const stuck = evaluateLoopBreak({
|
|
1673
|
-
consecutiveFailures: this.hookConsecutiveFailures,
|
|
1674
|
-
repeatedNoProgressCalls: this.hookRepeatedNoProgressCalls,
|
|
1675
|
-
textRepetitionDetected: detectTextRepetition(this.hookText),
|
|
1676
|
-
...(this.hookCyclicPattern ? { cyclicPattern: this.hookCyclicPattern } : {}),
|
|
1677
|
-
});
|
|
1678
|
-
if (stuck.shouldBreak)
|
|
1679
|
-
refusals.push(...stuck.reasons);
|
|
1680
|
-
// Stage 2 means the loop-breaker already detected spinning twice and told
|
|
1681
|
-
// the agent to stop and report. Do not overrule that with more turns.
|
|
1682
|
-
if (this.loopBreakInjected >= 2)
|
|
1683
|
-
refusals.push("loop-breaker already escalated");
|
|
1684
|
-
const { toolCalls, toolFailures } = this.hookStats;
|
|
1685
|
-
if (toolCalls > 0 && toolFailures / toolCalls > TURN_EXTENSION_MAX_FAILURE_RATIO) {
|
|
1686
|
-
refusals.push(`${toolFailures}/${toolCalls} tool calls failed`);
|
|
1687
|
-
}
|
|
1313
|
+
const refusals = this.loopMonitor.turnExtensionRefusals();
|
|
1688
1314
|
const granted = refusals.length === 0;
|
|
1689
1315
|
log("INFO", "turn-budget", granted ? "Extending turn budget" : "Refusing turn-budget extension", {
|
|
1690
1316
|
turn: String(ctx.turn),
|
|
@@ -1737,11 +1363,7 @@ export class AgentSession {
|
|
|
1737
1363
|
async getHookFollowUpMessages() {
|
|
1738
1364
|
// Exit notifications and task_output refer to the same host process record.
|
|
1739
1365
|
// Do not force an extra polling tool/turn merely to acknowledge a known exit.
|
|
1740
|
-
|
|
1741
|
-
for (const id of this.backgroundVerification.keys()) {
|
|
1742
|
-
backgroundChanged =
|
|
1743
|
-
(await this.recordFinishedBackgroundVerification(id)) || backgroundChanged;
|
|
1744
|
-
}
|
|
1366
|
+
const backgroundChanged = await this.verificationTracker.settleFinishedBackground();
|
|
1745
1367
|
if (backgroundChanged)
|
|
1746
1368
|
await this.persistVerificationState();
|
|
1747
1369
|
// Edits return immediately; only the completion boundary waits for remaining
|
|
@@ -1817,6 +1439,22 @@ export class AgentSession {
|
|
|
1817
1439
|
// verification demand above instead of manufacturing a separate hook.
|
|
1818
1440
|
if (diagnosticMessages.length > 0)
|
|
1819
1441
|
return diagnosticMessages;
|
|
1442
|
+
if (shouldNudgePlanSubmission({
|
|
1443
|
+
enabled: this.opts.planSubmissionGate === true,
|
|
1444
|
+
planMode: this.planModeRef.current,
|
|
1445
|
+
aborted: this.opts.signal?.aborted === true,
|
|
1446
|
+
nudgesThisRun: this.planSubmissionNudges,
|
|
1447
|
+
})) {
|
|
1448
|
+
this.planSubmissionNudges += 1;
|
|
1449
|
+
log("INFO", "plan-gate", "Run stopping in plan mode without a submitted plan; reminding");
|
|
1450
|
+
return [
|
|
1451
|
+
{
|
|
1452
|
+
role: "user",
|
|
1453
|
+
content: buildNotificationSteeringText([PLAN_SUBMISSION_NUDGE]),
|
|
1454
|
+
provenance: { source: "runtime", kind: "notification", visibility: "hidden" },
|
|
1455
|
+
},
|
|
1456
|
+
];
|
|
1457
|
+
}
|
|
1820
1458
|
if (this.opts.completionReview) {
|
|
1821
1459
|
const followUp = await this.opts.completionReview.followUp((request, signal) => this.callCompletionReviewer(request, signal), this.opts.signal);
|
|
1822
1460
|
await this.persistCompletionReviewState();
|
|
@@ -2043,6 +1681,11 @@ export class AgentSession {
|
|
|
2043
1681
|
maxTurnToolResultChars: resolveSessionTurnToolResultCharLimit(this.model, this.provider, accountId),
|
|
2044
1682
|
// Warn when web/MCP output contains instruction-like text (see injection-detect.ts).
|
|
2045
1683
|
transformToolResult: flagUntrustedToolResult,
|
|
1684
|
+
// A deferred built-in called by its advertised name runs directly
|
|
1685
|
+
// instead of costing a turn on "Unknown tool" then tool_search.
|
|
1686
|
+
...(options.disableTools
|
|
1687
|
+
? {}
|
|
1688
|
+
: { resolveTool: (name) => this.promoteDeferredBuiltin(name) }),
|
|
2046
1689
|
// Self-correction hooks (same as the TUI): loop-break + re-grounding are
|
|
2047
1690
|
// polled mid-loop; the ideal review is polled when the agent would stop.
|
|
2048
1691
|
getSteeringMessages: () => this.getHookSteeringMessages(),
|
|
@@ -2376,11 +2019,12 @@ export class AgentSession {
|
|
|
2376
2019
|
// there avoids tearing down a live stdio child and
|
|
2377
2020
|
// gambling on a `npx` re-spawn that could fail and drop the tools.
|
|
2378
2021
|
const glmInvolved = this.provider === "glm" || prevProvider === "glm";
|
|
2379
|
-
|
|
2022
|
+
const mcpManager = this.mcp.manager;
|
|
2023
|
+
if (mcpManager && glmInvolved) {
|
|
2380
2024
|
// Remove old MCP tools
|
|
2381
2025
|
this.tools = this.tools.filter((t) => !t.name.startsWith("mcp__"));
|
|
2382
2026
|
// Disconnect old MCP servers
|
|
2383
|
-
await
|
|
2027
|
+
await mcpManager.dispose();
|
|
2384
2028
|
// Connect new MCP servers for the new provider
|
|
2385
2029
|
try {
|
|
2386
2030
|
let apiKey;
|
|
@@ -2397,13 +2041,11 @@ export class AgentSession {
|
|
|
2397
2041
|
const servers = await getAllMcpServers(this.provider, apiKey, this.cwd, {
|
|
2398
2042
|
allowProjectScope: this.settingsManager.isProjectTrusted(this.cwd),
|
|
2399
2043
|
});
|
|
2400
|
-
const mcpTools = await
|
|
2044
|
+
const mcpTools = await mcpManager.connectAll(servers);
|
|
2401
2045
|
// Drop stale MCP tools from both the live set and deferred catalog before
|
|
2402
2046
|
// re-adding. Some tools may already have been promoted out of the catalog.
|
|
2403
2047
|
this.tools = this.tools.filter((t) => !t.name.startsWith("mcp__"));
|
|
2404
|
-
this.
|
|
2405
|
-
this.liveMcpTools.clear();
|
|
2406
|
-
this.cachedMcpToolServers.clear();
|
|
2048
|
+
this.mcp.clearMcpTools();
|
|
2407
2049
|
this.addMcpTools(mcpTools);
|
|
2408
2050
|
}
|
|
2409
2051
|
catch (err) {
|
|
@@ -2446,10 +2088,7 @@ export class AgentSession {
|
|
|
2446
2088
|
this.checkpointGeneration = loaded.header.generation ?? 0;
|
|
2447
2089
|
this.currentLeafId = loaded.header.leafId;
|
|
2448
2090
|
this.setSessionPath(loaded.path);
|
|
2449
|
-
this.
|
|
2450
|
-
this.autopilotMarkers = this.sessionManager.getAutopilotMarkers(loaded.entries, loaded.header.leafId);
|
|
2451
|
-
this.appMarkers = this.sessionManager.getAppMarkers(loaded.entries, loaded.header.leafId);
|
|
2452
|
-
this.turnMetrics = this.sessionManager.getTurnMetrics(loaded.entries);
|
|
2091
|
+
this.markers.restore(loaded.entries, loaded.header.leafId);
|
|
2453
2092
|
this.lastPersistedIndex = this.messages.length;
|
|
2454
2093
|
this.providerContext = null;
|
|
2455
2094
|
await this.subAgentManager?.rebindParentSession(this.sessionId);
|
|
@@ -2489,10 +2128,7 @@ export class AgentSession {
|
|
|
2489
2128
|
await this.persistMessage(message);
|
|
2490
2129
|
}
|
|
2491
2130
|
this.lastPersistedIndex = this.messages.length;
|
|
2492
|
-
await this.
|
|
2493
|
-
await this.rePersistNolanTurns();
|
|
2494
|
-
await this.rePersistAutopilotMarkers();
|
|
2495
|
-
await this.rePersistAppMarkers();
|
|
2131
|
+
await this.markers.rePersistAll();
|
|
2496
2132
|
await this.persistVerificationState();
|
|
2497
2133
|
await this.persistAppMarker("compaction", {
|
|
2498
2134
|
originalCount: result.originalCount,
|
|
@@ -2851,12 +2487,17 @@ export class AgentSession {
|
|
|
2851
2487
|
});
|
|
2852
2488
|
}
|
|
2853
2489
|
async newSession(preserveConversation = false) {
|
|
2490
|
+
// A background post-turn compaction replaces `this.messages` when it
|
|
2491
|
+
// lands. Letting it finish after the reset would swap the old history back
|
|
2492
|
+
// into the fresh session (e.g. an accepted plan's build would inherit the
|
|
2493
|
+
// whole planning conversation and lose the approved-plan prompt).
|
|
2494
|
+
await this.settlePostTurnCompaction();
|
|
2854
2495
|
this.cacheDiagnostics.reset();
|
|
2855
2496
|
// Approved-plan execution is a clean checkpoint of the same conversation;
|
|
2856
2497
|
// explicit new sessions reset the conversation identity.
|
|
2857
2498
|
if (!preserveConversation) {
|
|
2858
2499
|
this.verificationGate.reset();
|
|
2859
|
-
this.
|
|
2500
|
+
this.verificationTracker.clearBackground();
|
|
2860
2501
|
this.conversationId = "";
|
|
2861
2502
|
this.checkpointGeneration = 0;
|
|
2862
2503
|
this.sessionPreview = "";
|
|
@@ -2868,10 +2509,7 @@ export class AgentSession {
|
|
|
2868
2509
|
// turns / autopilot verdicts / app markers linger in memory, show up in the
|
|
2869
2510
|
// new session's /history, and get re-persisted into the new file by the
|
|
2870
2511
|
// next compaction — the cross-session duplicate-marker propagation bug.
|
|
2871
|
-
this.
|
|
2872
|
-
this.autopilotMarkers = [];
|
|
2873
|
-
this.appMarkers = [];
|
|
2874
|
-
this.turnMetrics = [];
|
|
2512
|
+
this.markers.clear();
|
|
2875
2513
|
const basePrompt = await this.buildBasePrompt(false, undefined);
|
|
2876
2514
|
this.baseSystemPrompt = basePrompt;
|
|
2877
2515
|
this.messages = [{ role: "system", content: this.withSystemPromptTail(basePrompt) }];
|
|
@@ -2993,9 +2631,7 @@ export class AgentSession {
|
|
|
2993
2631
|
else {
|
|
2994
2632
|
used = calculateActiveContextTokens(this.messages);
|
|
2995
2633
|
}
|
|
2996
|
-
const costUsd = this.
|
|
2997
|
-
? this.turnMetrics.reduce((sum, m) => sum + (m.cost.status === "known" ? m.cost.usd : 0), 0)
|
|
2998
|
-
: undefined;
|
|
2634
|
+
const costUsd = this.markers.knownCostUsd();
|
|
2999
2635
|
return costUsd === undefined ? { used, size } : { used, size, costUsd };
|
|
3000
2636
|
}
|
|
3001
2637
|
/**
|
|
@@ -3119,6 +2755,11 @@ export class AgentSession {
|
|
|
3119
2755
|
*/
|
|
3120
2756
|
async setPlanMode(active) {
|
|
3121
2757
|
this.planModeRef.current = active;
|
|
2758
|
+
// Plan mode always ends with `exit_plan`; a deferred one must be callable
|
|
2759
|
+
// without a tool_search turn. Append-only, like any promotion.
|
|
2760
|
+
if (active && !this.tools.some((t) => t.name === "exit_plan")) {
|
|
2761
|
+
this.tools.push(...this.mcp.promote(["exit_plan"]));
|
|
2762
|
+
}
|
|
3122
2763
|
// Entering plan mode discards any prior approved-plan contract (a new plan
|
|
3123
2764
|
// is about to be drafted); exiting keeps it (set explicitly via accept).
|
|
3124
2765
|
if (active)
|
|
@@ -3322,7 +2963,10 @@ export class AgentSession {
|
|
|
3322
2963
|
orchestrationPolicyTail() {
|
|
3323
2964
|
if (this.opts.orchestrationPrompt === false)
|
|
3324
2965
|
return "";
|
|
3325
|
-
return applyAsyncSubagentPolicy("", this.provider, this.model, this.thinkingLevel,
|
|
2966
|
+
return applyAsyncSubagentPolicy("", this.provider, this.model, this.thinkingLevel,
|
|
2967
|
+
// Deferred spawn_agent/wait_agent are one call away (resolveTool loads
|
|
2968
|
+
// them on first use), so the policy must still see them.
|
|
2969
|
+
[...this.tools.map((tool) => tool.name), ...this.deferredBuiltinToolNames]).trim();
|
|
3326
2970
|
}
|
|
3327
2971
|
/**
|
|
3328
2972
|
* Re-render the uncached tail of messages[0] in place. The cached prefix is
|
|
@@ -3344,12 +2988,7 @@ export class AgentSession {
|
|
|
3344
2988
|
return this.messages;
|
|
3345
2989
|
}
|
|
3346
2990
|
getTurnMetrics() {
|
|
3347
|
-
return this.
|
|
3348
|
-
...metric,
|
|
3349
|
-
usage: { ...metric.usage },
|
|
3350
|
-
timing: { ...metric.timing },
|
|
3351
|
-
cost: { ...metric.cost },
|
|
3352
|
-
}));
|
|
2991
|
+
return this.markers.getTurnMetrics();
|
|
3353
2992
|
}
|
|
3354
2993
|
async persistTurnMetric(event) {
|
|
3355
2994
|
if (event.stopReason === "error")
|
|
@@ -3376,31 +3015,23 @@ export class AgentSession {
|
|
|
3376
3015
|
reason: "No authoritative effective-dated provider pricing is available",
|
|
3377
3016
|
},
|
|
3378
3017
|
};
|
|
3379
|
-
this.
|
|
3018
|
+
this.markers.addTurnMetric(payload);
|
|
3380
3019
|
// Internal diagnostics piggyback on the authoritative per-turn metric —
|
|
3381
3020
|
// one source of truth, and the record flushes to disk every turn.
|
|
3382
3021
|
this.diagnosticsRecorder?.recordTurnMetric(payload);
|
|
3383
|
-
|
|
3384
|
-
await this.sessionManager.appendTurnMetric(this.sessionPath, payload);
|
|
3385
|
-
}
|
|
3386
|
-
async rePersistTurnMetrics() {
|
|
3387
|
-
if (!this.sessionPath)
|
|
3388
|
-
return;
|
|
3389
|
-
for (const metric of this.turnMetrics) {
|
|
3390
|
-
await this.sessionManager.appendTurnMetric(this.sessionPath, metric);
|
|
3391
|
-
}
|
|
3022
|
+
await this.markers.persistTurnMetric(payload);
|
|
3392
3023
|
}
|
|
3393
3024
|
/** Nolan Grout (mentor) turns recorded against this session, in record order. Used
|
|
3394
3025
|
* by the host to interleave Nolan's advisory exchanges back into the transcript
|
|
3395
3026
|
* on resume. Never part of the LLM message history. */
|
|
3396
3027
|
getNolanTurns() {
|
|
3397
|
-
return this.
|
|
3028
|
+
return this.markers.getNolanTurns();
|
|
3398
3029
|
}
|
|
3399
3030
|
/** Autopilot verdict markers recorded against this session, in record order.
|
|
3400
3031
|
* Used by the host to interleave the auto-review loop's markers back into
|
|
3401
3032
|
* the transcript on resume, mirroring `getNolanTurns`. */
|
|
3402
3033
|
getAutopilotMarkers() {
|
|
3403
|
-
return this.
|
|
3034
|
+
return this.markers.getAutopilotMarkers();
|
|
3404
3035
|
}
|
|
3405
3036
|
/** Non-system messages that are actually on disk. Transcript markers anchor
|
|
3406
3037
|
* against this (not the in-memory list, which can run ahead after a failed
|
|
@@ -3415,15 +3046,7 @@ export class AgentSession {
|
|
|
3415
3046
|
* file, so the new file carries positions that match its own transcript.
|
|
3416
3047
|
*/
|
|
3417
3048
|
remapMarkerAnchors(remap) {
|
|
3418
|
-
|
|
3419
|
-
return;
|
|
3420
|
-
const move = (payload) => ({
|
|
3421
|
-
...payload,
|
|
3422
|
-
afterMessageCount: remapAnchorForCompaction(payload.afterMessageCount, remap),
|
|
3423
|
-
});
|
|
3424
|
-
this.nolanTurns = this.nolanTurns.map(move);
|
|
3425
|
-
this.autopilotMarkers = this.autopilotMarkers.map(move);
|
|
3426
|
-
this.appMarkers = this.appMarkers.map(move);
|
|
3049
|
+
this.markers.remapAnchors(remap);
|
|
3427
3050
|
}
|
|
3428
3051
|
/**
|
|
3429
3052
|
* Record one Nolan Grout (mentor agent) turn against this build session: the
|
|
@@ -3436,39 +3059,7 @@ export class AgentSession {
|
|
|
3436
3059
|
* appendEntry's own handling.
|
|
3437
3060
|
*/
|
|
3438
3061
|
async persistNolanTurn(question, reply) {
|
|
3439
|
-
|
|
3440
|
-
const payload = { version: 1, question, reply, afterMessageCount };
|
|
3441
|
-
this.nolanTurns.push(payload);
|
|
3442
|
-
if (!this.sessionPath)
|
|
3443
|
-
return;
|
|
3444
|
-
const entry = {
|
|
3445
|
-
type: "custom",
|
|
3446
|
-
kind: NOLAN_TURN_CUSTOM_KIND,
|
|
3447
|
-
id: crypto.randomUUID(),
|
|
3448
|
-
parentId: null,
|
|
3449
|
-
timestamp: new Date().toISOString(),
|
|
3450
|
-
data: payload,
|
|
3451
|
-
};
|
|
3452
|
-
await this.sessionManager.appendEntry(this.sessionPath, entry);
|
|
3453
|
-
}
|
|
3454
|
-
/** Re-append the in-memory Nolan turns to the current session file. Called after
|
|
3455
|
-
* a continuation/compaction file is created so Nolan's advisory history isn't
|
|
3456
|
-
* lost when the session is rewritten (those rewrites only re-persist
|
|
3457
|
-
* messages). Each turn keeps its original `afterMessageCount` anchor. */
|
|
3458
|
-
async rePersistNolanTurns() {
|
|
3459
|
-
if (!this.sessionPath)
|
|
3460
|
-
return;
|
|
3461
|
-
for (const payload of this.nolanTurns) {
|
|
3462
|
-
const entry = {
|
|
3463
|
-
type: "custom",
|
|
3464
|
-
kind: NOLAN_TURN_CUSTOM_KIND,
|
|
3465
|
-
id: crypto.randomUUID(),
|
|
3466
|
-
parentId: null,
|
|
3467
|
-
timestamp: new Date().toISOString(),
|
|
3468
|
-
data: stripRecordedPosition(payload),
|
|
3469
|
-
};
|
|
3470
|
-
await this.sessionManager.appendEntry(this.sessionPath, entry);
|
|
3471
|
-
}
|
|
3062
|
+
await this.markers.recordNolanTurn(question, reply, this.persistedTranscriptCount());
|
|
3472
3063
|
}
|
|
3473
3064
|
/**
|
|
3474
3065
|
* Record one autopilot verdict marker (prompted / done / human / capped)
|
|
@@ -3478,23 +3069,6 @@ export class AgentSession {
|
|
|
3478
3069
|
* instead of dropping the marker or falling back to a raw verdict string.
|
|
3479
3070
|
* No-op persistence for transient sessions (kept in memory only).
|
|
3480
3071
|
*/
|
|
3481
|
-
async recordFinishedBackgroundVerification(id) {
|
|
3482
|
-
const started = this.backgroundVerification.get(id);
|
|
3483
|
-
const proc = this.processManager?.list().find((entry) => entry.id === id);
|
|
3484
|
-
if (!started || !proc || proc.exitCode === null)
|
|
3485
|
-
return false;
|
|
3486
|
-
if (started.sourceSnapshot !== undefined) {
|
|
3487
|
-
await this.finishSnapshotVerification(started, proc.exitCode === 0);
|
|
3488
|
-
}
|
|
3489
|
-
else if (proc.exitCode === 0) {
|
|
3490
|
-
this.verificationGate.recordVerification(started.revision, started.command);
|
|
3491
|
-
}
|
|
3492
|
-
else {
|
|
3493
|
-
this.verificationGate.recordFailedVerification(started.command, started.revision);
|
|
3494
|
-
}
|
|
3495
|
-
this.backgroundVerification.delete(id);
|
|
3496
|
-
return true;
|
|
3497
|
-
}
|
|
3498
3072
|
getVerificationEvidence() {
|
|
3499
3073
|
return this.verificationGate.evidence();
|
|
3500
3074
|
}
|
|
@@ -3506,39 +3080,8 @@ export class AgentSession {
|
|
|
3506
3080
|
evidence: this.verificationGate.evidence("run"),
|
|
3507
3081
|
};
|
|
3508
3082
|
}
|
|
3509
|
-
async finishSnapshotVerification(check, passed) {
|
|
3510
|
-
const after = check.sourceSnapshot
|
|
3511
|
-
? await captureVerificationSnapshot(this.opts.cwd, [...this.hookFileEditCounts.keys()])
|
|
3512
|
-
: null;
|
|
3513
|
-
if (after === null || after !== check.sourceSnapshot) {
|
|
3514
|
-
this.verificationGate.requireFreshVerification(true, check.command);
|
|
3515
|
-
this.verificationGate.recordRejectedCheck(check.command, after === null
|
|
3516
|
-
? "Workspace inputs could not be compared; run a read-only check after the command"
|
|
3517
|
-
: "Command changed workspace inputs; run checks against the changed source");
|
|
3518
|
-
if (!passed)
|
|
3519
|
-
this.verificationGate.recordFailedVerification(check.command);
|
|
3520
|
-
}
|
|
3521
|
-
else if (passed) {
|
|
3522
|
-
const classification = classifyVerificationCommand(check.command);
|
|
3523
|
-
if (classification.snapshotPreserveOnly) {
|
|
3524
|
-
this.verificationGate.recordRejectedCheck(check.command, classification.reason);
|
|
3525
|
-
}
|
|
3526
|
-
else {
|
|
3527
|
-
this.verificationGate.recordVerification(check.revision, check.command);
|
|
3528
|
-
}
|
|
3529
|
-
}
|
|
3530
|
-
else {
|
|
3531
|
-
this.verificationGate.recordFailedVerification(check.command, check.revision);
|
|
3532
|
-
}
|
|
3533
|
-
}
|
|
3534
3083
|
getVerificationProblem() {
|
|
3535
|
-
|
|
3536
|
-
return "Unverified: a build is still running or its workspace comparison is pending.";
|
|
3537
|
-
}
|
|
3538
|
-
return (this.verificationGate.verificationProblem() ??
|
|
3539
|
-
(this.backgroundVerification.size > 0
|
|
3540
|
-
? "Unverified: a background check is still running or its result has not been confirmed."
|
|
3541
|
-
: null));
|
|
3084
|
+
return this.verificationTracker.verificationProblem();
|
|
3542
3085
|
}
|
|
3543
3086
|
async persistCompletionReviewState() {
|
|
3544
3087
|
if (!this.sessionPath || !this.opts.completionReview)
|
|
@@ -3571,50 +3114,13 @@ export class AgentSession {
|
|
|
3571
3114
|
await this.sessionManager.appendEntry(this.sessionPath, entry);
|
|
3572
3115
|
}
|
|
3573
3116
|
async persistAutopilotMarker(phase, extra) {
|
|
3574
|
-
|
|
3575
|
-
const payload = {
|
|
3576
|
-
version: 1,
|
|
3577
|
-
phase,
|
|
3578
|
-
afterMessageCount,
|
|
3579
|
-
...(extra?.reason !== undefined ? { reason: extra.reason } : {}),
|
|
3580
|
-
...(extra?.body !== undefined ? { body: extra.body } : {}),
|
|
3581
|
-
};
|
|
3582
|
-
this.autopilotMarkers.push(payload);
|
|
3583
|
-
if (!this.sessionPath)
|
|
3584
|
-
return;
|
|
3585
|
-
const entry = {
|
|
3586
|
-
type: "custom",
|
|
3587
|
-
kind: AUTOPILOT_MARKER_CUSTOM_KIND,
|
|
3588
|
-
id: crypto.randomUUID(),
|
|
3589
|
-
parentId: null,
|
|
3590
|
-
timestamp: new Date().toISOString(),
|
|
3591
|
-
data: payload,
|
|
3592
|
-
};
|
|
3593
|
-
await this.sessionManager.appendEntry(this.sessionPath, entry);
|
|
3594
|
-
}
|
|
3595
|
-
/** Re-append the in-memory autopilot markers to the current session file.
|
|
3596
|
-
* Mirrors `rePersistNolanTurns` — called after a continuation/compaction file
|
|
3597
|
-
* is created so the auto-review history survives the rewrite. */
|
|
3598
|
-
async rePersistAutopilotMarkers() {
|
|
3599
|
-
if (!this.sessionPath)
|
|
3600
|
-
return;
|
|
3601
|
-
for (const payload of this.autopilotMarkers) {
|
|
3602
|
-
const entry = {
|
|
3603
|
-
type: "custom",
|
|
3604
|
-
kind: AUTOPILOT_MARKER_CUSTOM_KIND,
|
|
3605
|
-
id: crypto.randomUUID(),
|
|
3606
|
-
parentId: null,
|
|
3607
|
-
timestamp: new Date().toISOString(),
|
|
3608
|
-
data: stripRecordedPosition(payload),
|
|
3609
|
-
};
|
|
3610
|
-
await this.sessionManager.appendEntry(this.sessionPath, entry);
|
|
3611
|
-
}
|
|
3117
|
+
await this.markers.recordAutopilotMarker(phase, extra, this.persistedTranscriptCount());
|
|
3612
3118
|
}
|
|
3613
3119
|
/** App transcript markers recorded against this session, in record order.
|
|
3614
3120
|
* Used by the host to interleave display-only rows (plan banner, task
|
|
3615
3121
|
* header, errors, user-bubble hints) back into the transcript on resume. */
|
|
3616
3122
|
getAppMarkers() {
|
|
3617
|
-
return this.
|
|
3123
|
+
return this.markers.getAppMarkers();
|
|
3618
3124
|
}
|
|
3619
3125
|
/**
|
|
3620
3126
|
* Record one app transcript marker (display-only row) against this session.
|
|
@@ -3626,20 +3132,7 @@ export class AgentSession {
|
|
|
3626
3132
|
* No-op persistence for transient sessions.
|
|
3627
3133
|
*/
|
|
3628
3134
|
async persistAppMarker(kind, data, anchorOffset = 0) {
|
|
3629
|
-
|
|
3630
|
-
const payload = { version: 1, kind, afterMessageCount, data };
|
|
3631
|
-
this.appMarkers.push(payload);
|
|
3632
|
-
if (!this.sessionPath)
|
|
3633
|
-
return;
|
|
3634
|
-
const entry = {
|
|
3635
|
-
type: "custom",
|
|
3636
|
-
kind: APP_MARKER_CUSTOM_KIND,
|
|
3637
|
-
id: crypto.randomUUID(),
|
|
3638
|
-
parentId: null,
|
|
3639
|
-
timestamp: new Date().toISOString(),
|
|
3640
|
-
data: payload,
|
|
3641
|
-
};
|
|
3642
|
-
await this.sessionManager.appendEntry(this.sessionPath, entry);
|
|
3135
|
+
await this.markers.recordAppMarker(kind, data, this.persistedTranscriptCount() + anchorOffset);
|
|
3643
3136
|
}
|
|
3644
3137
|
/**
|
|
3645
3138
|
* Open the run journal for one `RunLifecycle` generation.
|
|
@@ -3668,24 +3161,6 @@ export class AgentSession {
|
|
|
3668
3161
|
outcome,
|
|
3669
3162
|
});
|
|
3670
3163
|
}
|
|
3671
|
-
/** Re-append the in-memory app markers to the current session file. Mirrors
|
|
3672
|
-
* `rePersistNolanTurns` — called after a continuation/compaction file is
|
|
3673
|
-
* created so display-only rows survive the rewrite. */
|
|
3674
|
-
async rePersistAppMarkers() {
|
|
3675
|
-
if (!this.sessionPath)
|
|
3676
|
-
return;
|
|
3677
|
-
for (const payload of this.appMarkers) {
|
|
3678
|
-
const entry = {
|
|
3679
|
-
type: "custom",
|
|
3680
|
-
kind: APP_MARKER_CUSTOM_KIND,
|
|
3681
|
-
id: crypto.randomUUID(),
|
|
3682
|
-
parentId: null,
|
|
3683
|
-
timestamp: new Date().toISOString(),
|
|
3684
|
-
data: stripRecordedPosition(payload),
|
|
3685
|
-
};
|
|
3686
|
-
await this.sessionManager.appendEntry(this.sessionPath, entry);
|
|
3687
|
-
}
|
|
3688
|
-
}
|
|
3689
3164
|
/**
|
|
3690
3165
|
* Rewrite a draft prompt into a tighter, terminology-correct version using
|
|
3691
3166
|
* the ACTIVE provider/model. A stateless one-off LLM call (no agent loop, no
|
|
@@ -3927,7 +3402,7 @@ export class AgentSession {
|
|
|
3927
3402
|
this.stopBackgroundProcesses();
|
|
3928
3403
|
this.lspManager?.shutdownAll();
|
|
3929
3404
|
this.debugManager?.shutdown();
|
|
3930
|
-
await Promise.all([this.subAgentManager?.shutdownAll(), this.
|
|
3405
|
+
await Promise.all([this.subAgentManager?.shutdownAll(), this.mcp.manager?.dispose()]);
|
|
3931
3406
|
await this.extensionLoader.deactivateAll();
|
|
3932
3407
|
this.setSessionPath("");
|
|
3933
3408
|
this.eventBus.removeAllListeners();
|
|
@@ -3972,7 +3447,7 @@ export class AgentSession {
|
|
|
3972
3447
|
.reverse()
|
|
3973
3448
|
.find((entry) => entry.type === "custom" && entry.kind === COMPLETION_REVIEW_STATE_KIND);
|
|
3974
3449
|
this.opts.completionReview?.restore(savedCompletionReview?.type === "custom" ? savedCompletionReview.data : null);
|
|
3975
|
-
this.
|
|
3450
|
+
this.verificationTracker.clearBackground();
|
|
3976
3451
|
const savedVerification = [...loaded.entries]
|
|
3977
3452
|
.reverse()
|
|
3978
3453
|
.find((entry) => entry.type === "custom" && entry.kind === VERIFICATION_STATE_KIND);
|
|
@@ -4002,12 +3477,8 @@ export class AgentSession {
|
|
|
4002
3477
|
// Restore Nolan's advisory turns (custom entries, not on the message branch) so
|
|
4003
3478
|
// they reappear in the transcript and survive into the continuation file.
|
|
4004
3479
|
// The leaf lets marker replay retain its independent file-order position.
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
this.autopilotMarkers = this.sessionManager.getAutopilotMarkers(loaded.entries, loaded.header.leafId);
|
|
4008
|
-
// Restore app transcript markers (plan banner / task header / errors / hints).
|
|
4009
|
-
this.appMarkers = this.sessionManager.getAppMarkers(loaded.entries, loaded.header.leafId);
|
|
4010
|
-
this.turnMetrics = this.sessionManager.getTurnMetrics(loaded.entries);
|
|
3480
|
+
// Autopilot verdicts, app markers and turn metrics are restored the same way.
|
|
3481
|
+
this.markers.restore(loaded.entries, loaded.header.leafId);
|
|
4011
3482
|
// A run that opened the journal and never closed it died mid-flight. Read
|
|
4012
3483
|
// it here, before anything rewrites the file, and report it once the
|
|
4013
3484
|
// transcript is in place.
|
|
@@ -4207,13 +3678,4 @@ export class AgentSession {
|
|
|
4207
3678
|
}
|
|
4208
3679
|
}
|
|
4209
3680
|
}
|
|
4210
|
-
/** Expand a leading `~` so `/import ~/.codex/...` works from any shell. */
|
|
4211
|
-
function resolveHomePath(filePath) {
|
|
4212
|
-
if (filePath === "~")
|
|
4213
|
-
return os.homedir();
|
|
4214
|
-
if (filePath.startsWith("~/") || filePath.startsWith("~\\")) {
|
|
4215
|
-
return path.join(os.homedir(), filePath.slice(2));
|
|
4216
|
-
}
|
|
4217
|
-
return filePath;
|
|
4218
|
-
}
|
|
4219
3681
|
//# sourceMappingURL=agent-session.js.map
|