@prestyj/cli 5.28.1 → 5.29.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/assets/motion/bin/contact-sheet.mjs +9 -3
- package/assets/motion/bin/cues.mjs +337 -0
- package/assets/motion/bin/library.mjs +53 -10
- package/assets/motion/bin/motion-blur.mjs +943 -0
- package/assets/motion/bin/motion-check.mjs +354 -3
- package/assets/motion/bin/music-fit.mjs +436 -0
- package/assets/motion/bin/pdf-extract.mjs +22 -10
- package/assets/motion/bin/reference-study.mjs +346 -0
- package/assets/motion/bin/score-synth.mjs +1052 -93
- package/assets/motion/library/README.md +50 -14
- package/assets/motion/library/kit/moves.js +1981 -0
- package/assets/motion/library/library.json +232 -0
- package/assets/motion/library/pieces/camera-rig/meta.json +13 -0
- package/assets/motion/library/pieces/camera-rig/piece.html +153 -0
- package/assets/motion/library/pieces/camera-rig/preview.jpg +0 -0
- package/assets/motion/library/pieces/chain-knock/meta.json +13 -0
- package/assets/motion/library/pieces/chain-knock/piece.html +195 -0
- package/assets/motion/library/pieces/chain-knock/preview.jpg +0 -0
- package/assets/motion/library/pieces/gather-to-logo/meta.json +13 -0
- package/assets/motion/library/pieces/gather-to-logo/piece.html +159 -0
- package/assets/motion/library/pieces/gather-to-logo/preview.jpg +0 -0
- package/assets/motion/library/pieces/morph-carry/meta.json +13 -0
- package/assets/motion/library/pieces/morph-carry/piece.html +173 -0
- package/assets/motion/library/pieces/morph-carry/preview.jpg +0 -0
- package/assets/motion/library/pieces/one-shape-journey/meta.json +13 -0
- package/assets/motion/library/pieces/one-shape-journey/piece.html +195 -0
- package/assets/motion/library/pieces/one-shape-journey/preview.jpg +0 -0
- package/assets/motion/library/pieces/open-from-subject/meta.json +13 -0
- package/assets/motion/library/pieces/open-from-subject/piece.html +168 -0
- package/assets/motion/library/pieces/open-from-subject/preview.jpg +0 -0
- package/assets/motion/library/pieces/request-to-result/meta.json +13 -0
- package/assets/motion/library/pieces/request-to-result/piece.html +212 -0
- package/assets/motion/library/pieces/request-to-result/preview.jpg +0 -0
- package/assets/motion/library/pieces/scale-dive/meta.json +13 -0
- package/assets/motion/library/pieces/scale-dive/piece.html +321 -0
- package/assets/motion/library/pieces/scale-dive/preview.jpg +0 -0
- package/assets/motion/library/pieces/screen-replica-steps/meta.json +13 -0
- package/assets/motion/library/pieces/screen-replica-steps/piece.html +366 -0
- package/assets/motion/library/pieces/screen-replica-steps/preview.jpg +0 -0
- package/assets/motion/library/pieces/zoom-into-card/meta.json +13 -0
- package/assets/motion/library/pieces/zoom-into-card/piece.html +179 -0
- package/assets/motion/library/pieces/zoom-into-card/preview.jpg +0 -0
- package/assets/motion/library/sheets/diagram.jpg +0 -0
- package/assets/motion/library/sheets/frame.jpg +0 -0
- package/assets/motion/library/sheets/transition.jpg +0 -0
- package/assets/motion/library/sheets/ui.jpg +0 -0
- package/assets/motion/references/build-sheet.md +206 -0
- package/assets/motion/references/runtime/determinism-rules.md +1 -1
- package/assets/motion/references/runtime/gsap-easing-and-stagger.md +29 -29
- package/assets/motion/references/runtime/inputs-and-assets.md +7 -12
- package/assets/motion/references/runtime/lint-validate-inspect.md +3 -3
- package/assets/motion/references/runtime/minimal-composition.md +1 -1
- package/assets/motion/references/runtime/preview-render.md +3 -3
- package/assets/motion/skills/app-walkthrough/SKILL.md +70 -0
- package/assets/motion/skills/before-after/SKILL.md +57 -0
- package/assets/motion/skills/brand-kit/SKILL.md +3 -3
- package/assets/motion/skills/dev-tool-video/SKILL.md +61 -0
- package/assets/motion/skills/launch-video/SKILL.md +66 -0
- package/assets/motion/skills/match-reference/SKILL.md +62 -0
- package/assets/motion/skills/motion/SKILL.md +148 -98
- package/assets/motion/skills/source-ingest/SKILL.md +17 -6
- package/assets/motion/skills/website-video/SKILL.md +76 -0
- package/assets/skills/bulletproof/SKILL.md +36 -11
- package/assets/skills/bulletproof/references/agent-surface.md +19 -9
- package/assets/skills/bulletproof/references/audit-protocol.md +20 -5
- package/assets/skills/bulletproof/references/platform-playbooks.md +5 -4
- package/assets/skills/bulletproof/references/provenance.md +26 -1
- package/assets/skills/bulletproof/references/secure-defaults.md +6 -5
- package/assets/skills/bulletproof/references/supply-chain.md +21 -17
- package/assets/skills/bulletproof/references/threat-landscape.md +28 -26
- package/assets/skills/bulletproof/references/verification.md +2 -0
- package/assets/skills/clarify/SKILL.md +25 -16
- package/assets/skills/code-review/SKILL.md +71 -13
- package/assets/skills/code-review/references/agent-diffs.md +27 -0
- package/assets/skills/code-review/references/tests.md +19 -0
- package/assets/skills/compliance-guard/SKILL.md +20 -5
- package/assets/skills/compliance-guard/references/artifacts.md +1 -1
- package/assets/skills/compliance-guard/references/eu-uk.md +16 -16
- package/assets/skills/compliance-guard/references/lawsuit-vectors.md +5 -5
- package/assets/skills/compliance-guard/references/provenance.md +41 -2
- package/assets/skills/compliance-guard/references/sector-gates.md +3 -3
- package/assets/skills/compliance-guard/references/security-baseline.md +2 -2
- package/assets/skills/compliance-guard/references/trigger-map.md +5 -5
- package/assets/skills/compliance-guard/references/us.md +27 -21
- package/assets/skills/durable/SKILL.md +87 -79
- package/assets/skills/durable/references/agent-db-safety.md +69 -0
- package/assets/skills/durable/references/backups-and-runtime.md +19 -12
- package/assets/skills/durable/references/migrations-and-schema.md +13 -6
- package/assets/skills/evidence-led-ui/SKILL.md +69 -127
- package/assets/skills/evidence-led-ui/references/anti-defaults.md +107 -208
- package/assets/skills/evidence-led-ui/references/direction.md +124 -0
- package/assets/skills/evidence-led-ui/references/production-contract.md +8 -0
- package/assets/skills/evidence-led-ui/references/provenance.md +24 -1
- package/assets/skills/lean/SKILL.md +90 -71
- package/assets/skills/lean/references/memory-and-processes.md +3 -2
- package/assets/skills/lean/references/playbooks.md +37 -12
- package/assets/skills/refactoring/SKILL.md +24 -3
- package/assets/skills/refactoring/references/agent-pitfalls.md +4 -1
- package/assets/skills/refactoring/references/legacy.md +21 -0
- package/assets/skills/root-cause/SKILL.md +20 -10
- package/assets/skills/shared-language/SKILL.md +16 -14
- package/assets/skills/tdd/SKILL.md +27 -15
- package/dist/app-sidecar.js +206 -47
- package/dist/app-sidecar.js.map +1 -1
- package/dist/cli/thinking-arg.d.ts +6 -0
- package/dist/cli/thinking-arg.js +13 -0
- package/dist/cli/thinking-arg.js.map +1 -0
- package/dist/cli.d.ts +1 -2
- package/dist/cli.js +18 -34
- package/dist/cli.js.map +1 -1
- package/dist/core/acceptance-checks.d.ts +48 -0
- package/dist/core/acceptance-checks.js +144 -0
- package/dist/core/acceptance-checks.js.map +1 -0
- package/dist/core/agent-session.d.ts +107 -72
- package/dist/core/agent-session.js +562 -407
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/agents.d.ts +6 -5
- package/dist/core/agents.js.map +1 -1
- package/dist/core/ask-user.d.ts +90 -8
- package/dist/core/ask-user.js +124 -13
- package/dist/core/ask-user.js.map +1 -1
- package/dist/core/bundled-agents.js +1 -3
- package/dist/core/bundled-agents.js.map +1 -1
- package/dist/core/cache-diagnostics.d.ts +68 -0
- package/dist/core/cache-diagnostics.js +196 -0
- package/dist/core/cache-diagnostics.js.map +1 -0
- package/dist/core/cache-expiry.d.ts +87 -0
- package/dist/core/cache-expiry.js +111 -0
- package/dist/core/cache-expiry.js.map +1 -0
- package/dist/core/codex-request-shape.d.ts +12 -0
- package/dist/core/codex-request-shape.js +31 -0
- package/dist/core/codex-request-shape.js.map +1 -0
- package/dist/core/compaction/compactor.js +84 -48
- package/dist/core/compaction/compactor.js.map +1 -1
- package/dist/core/compaction/plan-step-policy.d.ts +46 -0
- package/dist/core/compaction/plan-step-policy.js +57 -0
- package/dist/core/compaction/plan-step-policy.js.map +1 -0
- package/dist/core/destructive-git-guard.d.ts +90 -0
- package/dist/core/destructive-git-guard.js +871 -0
- package/dist/core/destructive-git-guard.js.map +1 -0
- package/dist/core/event-bus.d.ts +3 -0
- package/dist/core/event-bus.js +5 -0
- package/dist/core/event-bus.js.map +1 -1
- package/dist/core/injection-detect.d.ts +38 -0
- package/dist/core/injection-detect.js +232 -0
- package/dist/core/injection-detect.js.map +1 -0
- package/dist/core/keep-awake.d.ts +88 -0
- package/dist/core/keep-awake.js +251 -0
- package/dist/core/keep-awake.js.map +1 -0
- package/dist/core/mcp/client.d.ts +72 -0
- package/dist/core/mcp/client.js +264 -41
- package/dist/core/mcp/client.js.map +1 -1
- package/dist/core/mcp/content.js +6 -2
- package/dist/core/mcp/content.js.map +1 -1
- package/dist/core/mcp/store.d.ts +6 -1
- package/dist/core/mcp/store.js +12 -1
- package/dist/core/mcp/store.js.map +1 -1
- package/dist/core/mcp/types.d.ts +18 -0
- package/dist/core/model-unavailable.d.ts +14 -0
- package/dist/core/model-unavailable.js +23 -0
- package/dist/core/model-unavailable.js.map +1 -0
- package/dist/core/node-debugger.d.ts +148 -0
- package/dist/core/node-debugger.js +642 -0
- package/dist/core/node-debugger.js.map +1 -0
- package/dist/core/nolan-context.js +7 -3
- package/dist/core/nolan-context.js.map +1 -1
- package/dist/core/package-threats.d.ts +18 -0
- package/dist/core/package-threats.js +168 -0
- package/dist/core/package-threats.js.map +1 -0
- package/dist/core/persistent-shell.d.ts +58 -6
- package/dist/core/persistent-shell.js +331 -49
- package/dist/core/persistent-shell.js.map +1 -1
- package/dist/core/process-manager.d.ts +14 -0
- package/dist/core/process-manager.js +61 -0
- package/dist/core/process-manager.js.map +1 -1
- package/dist/core/progress/git-xp.js +8 -14
- package/dist/core/progress/git-xp.js.map +1 -1
- package/dist/core/session-history.d.ts +12 -0
- package/dist/core/session-history.js +27 -0
- package/dist/core/session-history.js.map +1 -1
- package/dist/core/session-manager.d.ts +13 -1
- package/dist/core/session-manager.js +38 -18
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-summary-index.d.ts +37 -0
- package/dist/core/session-summary-index.js +172 -0
- package/dist/core/session-summary-index.js.map +1 -0
- package/dist/core/settings-manager.d.ts +12 -0
- package/dist/core/settings-manager.js +20 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/shell-threats-popular-packages.d.ts +11 -0
- package/dist/core/shell-threats-popular-packages.js +675 -0
- package/dist/core/shell-threats-popular-packages.js.map +1 -0
- package/dist/core/shell-threats.d.ts +8 -0
- package/dist/core/shell-threats.js +186 -0
- package/dist/core/shell-threats.js.map +1 -0
- package/dist/core/skills.js +3 -1
- package/dist/core/skills.js.map +1 -1
- package/dist/core/stream-rules.d.ts +30 -0
- package/dist/core/stream-rules.js +151 -0
- package/dist/core/stream-rules.js.map +1 -0
- package/dist/core/subagent-manager.d.ts +20 -5
- package/dist/core/subagent-manager.js +22 -8
- package/dist/core/subagent-manager.js.map +1 -1
- package/dist/core/subagent-receipt.d.ts +54 -0
- package/dist/core/subagent-receipt.js +278 -0
- package/dist/core/subagent-receipt.js.map +1 -0
- package/dist/core/subagent-turn-record.d.ts +2 -0
- package/dist/core/subagent-turn-record.js.map +1 -1
- package/dist/core/test-impact.d.ts +73 -0
- package/dist/core/test-impact.js +467 -0
- package/dist/core/test-impact.js.map +1 -0
- package/dist/core/thinking-level.d.ts +1 -1
- package/dist/core/thinking-level.js +1 -1
- package/dist/core/thinking-level.js.map +1 -1
- package/dist/core/verification-gate.d.ts +2 -0
- package/dist/core/verification-gate.js +4 -0
- package/dist/core/verification-gate.js.map +1 -1
- package/dist/core/verification-snapshot.js +3 -5
- package/dist/core/verification-snapshot.js.map +1 -1
- package/dist/core/workspace-guard.d.ts +18 -7
- package/dist/core/workspace-guard.js +227 -60
- package/dist/core/workspace-guard.js.map +1 -1
- package/dist/interactive.js +2 -1
- package/dist/interactive.js.map +1 -1
- package/dist/modes/subagent-worker-mode.js +34 -6
- package/dist/modes/subagent-worker-mode.js.map +1 -1
- package/dist/motion-agent/motion-agent.d.ts +6 -2
- package/dist/motion-agent/motion-agent.js +5 -7
- package/dist/motion-agent/motion-agent.js.map +1 -1
- package/dist/motion-agent/motion-prompt.d.ts +1 -1
- package/dist/motion-agent/motion-prompt.js +16 -20
- package/dist/motion-agent/motion-prompt.js.map +1 -1
- package/dist/motion-agent/motion-review-session.js +7 -3
- package/dist/motion-agent/motion-review-session.js.map +1 -1
- package/dist/motion-agent/motion-review.d.ts +10 -3
- package/dist/motion-agent/motion-review.js +15 -7
- package/dist/motion-agent/motion-review.js.map +1 -1
- package/dist/motion-agent/motion-studio-context.js +1 -1
- package/dist/motion-agent/motion-studio-context.js.map +1 -1
- package/dist/system-prompt.js +15 -11
- package/dist/system-prompt.js.map +1 -1
- package/dist/test-support/keep-alive.d.ts +14 -0
- package/dist/test-support/keep-alive.js +17 -0
- package/dist/test-support/keep-alive.js.map +1 -0
- package/dist/tools/ask-user.js +3 -3
- package/dist/tools/ask-user.js.map +1 -1
- package/dist/tools/bash-read-evidence.d.ts +10 -0
- package/dist/tools/bash-read-evidence.js +133 -0
- package/dist/tools/bash-read-evidence.js.map +1 -0
- package/dist/tools/bash.d.ts +10 -1
- package/dist/tools/bash.js +115 -7
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/debug.d.ts +54 -0
- package/dist/tools/debug.js +233 -0
- package/dist/tools/debug.js.map +1 -0
- package/dist/tools/edit-targets.d.ts +10 -0
- package/dist/tools/edit-targets.js +31 -0
- package/dist/tools/edit-targets.js.map +1 -0
- package/dist/tools/edit.d.ts +11 -3
- package/dist/tools/edit.js +387 -305
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/goals.d.ts +1 -1
- package/dist/tools/index.d.ts +19 -2
- package/dist/tools/index.js +47 -7
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/prompt-hints.js +6 -1
- package/dist/tools/prompt-hints.js.map +1 -1
- package/dist/tools/read-tracker.d.ts +5 -0
- package/dist/tools/read-tracker.js +19 -9
- package/dist/tools/read-tracker.js.map +1 -1
- package/dist/tools/read.js +3 -2
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/skill.js +11 -3
- package/dist/tools/skill.js.map +1 -1
- package/dist/tools/subagent-control.js +44 -8
- package/dist/tools/subagent-control.js.map +1 -1
- package/dist/tools/subagent-shared.d.ts +29 -8
- package/dist/tools/subagent-shared.js +45 -14
- package/dist/tools/subagent-shared.js.map +1 -1
- package/dist/tools/subagent.d.ts +14 -2
- package/dist/tools/subagent.js +43 -13
- package/dist/tools/subagent.js.map +1 -1
- package/dist/tools/task-output.js +3 -2
- package/dist/tools/task-output.js.map +1 -1
- package/dist/tools/task-send.d.ts +1 -1
- package/dist/tools/task-send.js +15 -1
- package/dist/tools/task-send.js.map +1 -1
- package/dist/tools/tool-tiers.d.ts +8 -2
- package/dist/tools/tool-tiers.js +10 -3
- package/dist/tools/tool-tiers.js.map +1 -1
- package/dist/tools/truncate.d.ts +21 -0
- package/dist/tools/truncate.js +187 -0
- package/dist/tools/truncate.js.map +1 -1
- package/dist/tools/ui-adopt.js +2 -0
- package/dist/tools/ui-adopt.js.map +1 -1
- package/dist/ui/App.d.ts +0 -4
- package/dist/ui/App.js +12 -31
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/components/ActivityIndicator.js +1 -0
- package/dist/ui/components/ActivityIndicator.js.map +1 -1
- package/dist/ui/components/ToolExecution.js +3 -2
- package/dist/ui/components/ToolExecution.js.map +1 -1
- package/dist/ui/hooks/useAgentLoop.d.ts +1 -8
- package/dist/ui/hooks/useAgentLoop.js +1 -119
- package/dist/ui/hooks/useAgentLoop.js.map +1 -1
- package/dist/ui/render.d.ts +0 -4
- package/dist/ui/render.js +0 -2
- package/dist/ui/render.js.map +1 -1
- package/dist/ui/terminal-history.js +3 -1
- package/dist/ui/terminal-history.js.map +1 -1
- package/dist/ui/tool-line-summary.js +3 -1
- package/dist/ui/tool-line-summary.js.map +1 -1
- package/dist/utils/format.js +5 -1
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/git.d.ts +77 -0
- package/dist/utils/git.js +285 -21
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/github-ci.js +2 -1
- package/dist/utils/github-ci.js.map +1 -1
- package/dist/utils/github.js +11 -9
- package/dist/utils/github.js.map +1 -1
- package/dist/utils/image.d.ts +14 -0
- package/dist/utils/image.js +16 -0
- package/dist/utils/image.js.map +1 -1
- package/dist/utils/process.d.ts +20 -0
- package/dist/utils/process.js +98 -0
- package/dist/utils/process.js.map +1 -1
- package/package.json +4 -4
- package/assets/motion/references/motion-language.md +0 -128
- package/assets/motion/skills/video-qa/SKILL.md +0 -89
- package/dist/core/ideal-review-subagent.d.ts +0 -56
- package/dist/core/ideal-review-subagent.js +0 -112
- package/dist/core/ideal-review-subagent.js.map +0 -1
- package/dist/core/ideal-review.d.ts +0 -82
- package/dist/core/ideal-review.js +0 -242
- package/dist/core/ideal-review.js.map +0 -1
- package/dist/motion-agent/motion-check-tool.d.ts +0 -35
- package/dist/motion-agent/motion-check-tool.js +0 -514
- package/dist/motion-agent/motion-check-tool.js.map +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { agentLoop, isAbortError, isUsageLimitError, } from "@prestyj/agent";
|
|
1
|
+
import { agentLoop, isAbortError, isUsageLimitError, repairToolPairingAdjacent, } from "@prestyj/agent";
|
|
2
2
|
import { ProviderError, stream, } from "@prestyj/ai";
|
|
3
3
|
import { EventBus } from "./event-bus.js";
|
|
4
|
+
import { flagUntrustedToolResult } from "./injection-detect.js";
|
|
4
5
|
import { COMPLETION_REVIEW_STATE_KIND, } from "./completion-review.js";
|
|
5
6
|
import { SlashCommandRegistry, createBuiltinCommands, } from "./slash-commands.js";
|
|
6
7
|
import { PROMPT_COMMANDS, getPromptCommand } from "./prompt-commands.js";
|
|
@@ -23,9 +24,13 @@ import { ensureAppDirs } from "../config.js";
|
|
|
23
24
|
import { buildSubAgentSystemPrompt, buildSystemPrompt, } from "../system-prompt.js";
|
|
24
25
|
import { createTools, createWebSearchTool, } from "../tools/index.js";
|
|
25
26
|
import { partitionToolsByTier } from "../tools/tool-tiers.js";
|
|
27
|
+
import { formatImpactForVerification } from "./test-impact.js";
|
|
28
|
+
import { autoBackgroundedId } from "../tools/bash.js";
|
|
26
29
|
import { buildProcessCompletionFollowUp } from "./process-gate.js";
|
|
27
|
-
import { buildSubAgentCompletionFollowUp
|
|
30
|
+
import { buildSubAgentCompletionFollowUp } from "./subagent-manager.js";
|
|
28
31
|
import { applyAsyncSubagentPolicy } from "./subagent-policy.js";
|
|
32
|
+
import { resolveResponsesLite, resolveStrictTools, } from "./codex-request-shape.js";
|
|
33
|
+
import { subAgentDescription } from "../tools/subagent.js";
|
|
29
34
|
import { z } from "zod";
|
|
30
35
|
import { MCPClientManager, getAllMcpServers } from "./mcp/index.js";
|
|
31
36
|
import { clampMcpToolDescription, DeferredToolCatalog } from "./mcp/deferred-catalog.js";
|
|
@@ -36,44 +41,41 @@ import { createToolSearchTool } from "../tools/tool-search.js";
|
|
|
36
41
|
import { createSessionStatsTool } from "../tools/session-stats.js";
|
|
37
42
|
import { createDiagnoseCommand, isInternalDiagnosticsEnabled, SessionDiagnosticsRecorder, } from "./internal-diagnostics.js";
|
|
38
43
|
import { log } from "./logger.js";
|
|
39
|
-
import {
|
|
44
|
+
import { CacheDiagnostics } from "./cache-diagnostics.js";
|
|
45
|
+
import { assessCacheExpiry, resolveCacheTtl } from "./cache-expiry.js";
|
|
46
|
+
import { setEstimatorModel, calibrateEstimatorFromUsage, estimateConversationTokens, } from "./compaction/token-estimator.js";
|
|
40
47
|
import { calculateActiveContextTokens } from "./compaction/active-context.js";
|
|
41
48
|
import { resolveCompactionPolicy } from "./compaction/policy.js";
|
|
49
|
+
import { decidePlanStepCompaction, DEFAULT_CACHE_WRITE_READ_RATIO, PLAN_STEP_KEEP_TOKENS, } from "./compaction/plan-step-policy.js";
|
|
50
|
+
import { extractPlanSteps, findCompletedMarkers } from "../utils/plan-steps.js";
|
|
51
|
+
import { readFileSync } from "node:fs";
|
|
42
52
|
import { clampThinkingForPlanMode } from "./thinking-level.js";
|
|
43
53
|
import { pruneStaleToolResults } from "./compaction/tool-result-pruner.js";
|
|
44
54
|
import { discoverAgents } from "./agents.js";
|
|
45
55
|
import { enhancePrompt } from "../utils/prompt-enhancer.js";
|
|
46
56
|
import { detectLanguages, detectProjectStack } from "./language-detector.js";
|
|
47
|
-
import { evaluateIdealReview, buildIdealReviewMessage, buildReviewCoverageEscalationMessage, buildReviewCoverageMessage, MAX_REVIEW_COVERAGE_INJECTIONS, withReviewCoverageRequirements, detectTestDrift, ReviewCoverageTracker, } from "./ideal-review.js";
|
|
48
57
|
import { evaluateLoopBreak, buildLoopBreakMessage, CycleDetector, ToolCallProgressTracker, detectTextRepetition, } from "./loop-breaker.js";
|
|
49
58
|
import { buildRegroundingMessage, requestTextForRegrounding } from "./regrounding.js";
|
|
50
59
|
import { buildSemanticLoopJudgePrompt, buildSemanticLoopMessage, MAX_SEMANTIC_LOOP_CALLS, parseSemanticLoopVerdict, shouldRunSemanticLoopCheck, SEMANTIC_LOOP_JUDGE_TIMEOUT_MS, withJudgeTimeout, } from "./semantic-loop-check.js";
|
|
51
|
-
import { buildIndependentReviewMessage, buildReviewerTask, INDEPENDENT_REVIEW_SCORE_THRESHOLD, parseReviewerFindings, REVIEWER_TOOLS, REVIEWER_TURN_TIMEOUT_MS, REVIEWER_WAIT_MS, } from "./ideal-review-subagent.js";
|
|
52
60
|
import { buildEnvDeltaMessage } from "./env-delta.js";
|
|
53
61
|
import { wrapSteeringText, buildNotificationSteeringText, STEERING_PREFIX } from "./steering.js";
|
|
54
62
|
import { AgentNotificationQueue } from "./agent-notifications.js";
|
|
55
|
-
import { VerificationGate,
|
|
63
|
+
import { VerificationGate, isCheckOwnFile, extractAddedLines, isCodeFilePath, VERIFICATION_STATE_KIND, isVerificationCommand, } from "./verification-gate.js";
|
|
56
64
|
import { classifyVerificationCommand } from "./verification-evidence.js";
|
|
57
65
|
import { captureVerificationSnapshot } from "./verification-snapshot.js";
|
|
58
66
|
import { findUserSessionPrompt, getUserSessionPrompt } from "./session-preview.js";
|
|
59
67
|
import { normalizeMessageImages } from "./message-images.js";
|
|
68
|
+
import { loadStreamRules } from "./stream-rules.js";
|
|
60
69
|
import crypto from "node:crypto";
|
|
61
70
|
import fs from "node:fs/promises";
|
|
62
71
|
import os from "node:os";
|
|
63
72
|
import path from "node:path";
|
|
73
|
+
import { editTargetPaths } from "../tools/edit-targets.js";
|
|
64
74
|
/**
|
|
65
75
|
* A run whose tool calls fail more often than this is thrashing, not
|
|
66
76
|
* progressing — refuse to extend its turn budget.
|
|
67
77
|
*/
|
|
68
78
|
const TURN_EXTENSION_MAX_FAILURE_RATIO = 0.5;
|
|
69
|
-
/** Terminal subagent states — mirrors SubAgentManager's private isTerminal. */
|
|
70
|
-
function isTerminalSubAgentState(state) {
|
|
71
|
-
return (state === "completed" ||
|
|
72
|
-
state === "failed" ||
|
|
73
|
-
state === "interrupted" ||
|
|
74
|
-
state === "closed" ||
|
|
75
|
-
state === "reaped");
|
|
76
|
-
}
|
|
77
79
|
// ── Tool-result policy ─────────────────────────────────────
|
|
78
80
|
/** Resolve the per-result cap passed to the agent loop for the active transport. */
|
|
79
81
|
export function resolveSessionToolResultCharLimit(model, provider, accountId) {
|
|
@@ -130,6 +132,7 @@ export class AgentSession {
|
|
|
130
132
|
// transcript rows the live run showed.
|
|
131
133
|
appMarkers = [];
|
|
132
134
|
turnMetrics = [];
|
|
135
|
+
cacheDiagnostics = new CacheDiagnostics();
|
|
133
136
|
/** Internal-only (EZ_INTERNAL): live per-session cost/reliability recorder.
|
|
134
137
|
* Absent entirely in public builds — see core/internal-diagnostics.ts. */
|
|
135
138
|
diagnosticsRecorder;
|
|
@@ -141,20 +144,13 @@ export class AgentSession {
|
|
|
141
144
|
/** Forgets every file read; called whenever the conversation is replaced or
|
|
142
145
|
* rewound, so the model must re-read a file before changing it. */
|
|
143
146
|
clearReadTracker;
|
|
147
|
+
recordBashReads;
|
|
144
148
|
skills = [];
|
|
145
149
|
cacheKeyLogged = false;
|
|
146
150
|
// ── Self-correction hook state (mirrors the TUI's useAgentLoop refs) ──
|
|
147
151
|
// Reset at the start of every run; observed from the event stream; read by
|
|
148
|
-
// the loop-break (mid-loop)
|
|
149
|
-
hookStats = {
|
|
150
|
-
changedLines: 0,
|
|
151
|
-
toolCalls: 0,
|
|
152
|
-
toolFailures: 0,
|
|
153
|
-
turns: 0,
|
|
154
|
-
writeCalls: 0,
|
|
155
|
-
editCalls: 0,
|
|
156
|
-
bashCalls: 0,
|
|
157
|
-
};
|
|
152
|
+
// the loop-break (mid-loop) callback.
|
|
153
|
+
hookStats = { toolCalls: 0, toolFailures: 0, turns: 0 };
|
|
158
154
|
hookText = "";
|
|
159
155
|
hookConsecutiveFailures = 0;
|
|
160
156
|
hookRepeatedNoProgressCalls = 0;
|
|
@@ -164,20 +160,6 @@ export class AgentSession {
|
|
|
164
160
|
hookFileEditCounts = new Map();
|
|
165
161
|
hookToolCalls = new Map();
|
|
166
162
|
backgroundVerification = new Map();
|
|
167
|
-
idealReviewPhase = "idle";
|
|
168
|
-
/** Runtime-only suppression while Nolan owns verification in autopilot mode. */
|
|
169
|
-
idealReviewSuppressed = false;
|
|
170
|
-
/** Mirror of the last `hook_armed` value broadcast this run, so the event
|
|
171
|
-
* fires only on a real edge. */
|
|
172
|
-
idealReviewArmed = false;
|
|
173
|
-
/** Cached test-drift probe, keyed by the size of the edited-file set. Drift
|
|
174
|
-
* depends only on WHICH files were edited and that set only grows, so this
|
|
175
|
-
* keeps the arming check off the filesystem on most tool results — the probe
|
|
176
|
-
* is several sync existsSync calls per edited file. */
|
|
177
|
-
idealDriftProbe = null;
|
|
178
|
-
reviewCoverage;
|
|
179
|
-
/** Coverage follow-ups spent this run, capped by MAX_REVIEW_COVERAGE_INJECTIONS. */
|
|
180
|
-
reviewCoverageInjected = 0;
|
|
181
163
|
/** 0 = none; 1 = first nudge sent; 2 = final stop-and-report injected. */
|
|
182
164
|
loopBreakInjected = 0;
|
|
183
165
|
regroundingInjected = false;
|
|
@@ -187,8 +169,6 @@ export class AgentSession {
|
|
|
187
169
|
* injection at the next steering poll; judge failures fail open (no
|
|
188
170
|
* injection) and still consume budget + cooldown. */
|
|
189
171
|
semanticLoop = { checksUsed: 0, lastCheckTurn: 0, pending: false, verdict: null, injected: false };
|
|
190
|
-
/** Independent Ideal reviewer spawned once per run (score-gated). */
|
|
191
|
-
independentReviewStarted = false;
|
|
192
172
|
/**
|
|
193
173
|
* The environment as the cached system prompt currently describes it.
|
|
194
174
|
* Re-recorded on every prompt build, so a rebuild (e.g. `/add-dir`) needs no
|
|
@@ -199,11 +179,12 @@ export class AgentSession {
|
|
|
199
179
|
runStartedAt = 0;
|
|
200
180
|
/** Gate injections spent this run, capped by MAX_PROCESS_GATE_INJECTIONS. */
|
|
201
181
|
processGateInjected = 0;
|
|
202
|
-
/** Verification gate: code edited this run, nothing proved it since.
|
|
182
|
+
/** Verification gate: code edited this run, nothing proved it since. Always
|
|
183
|
+
* tracks evidence for run status; the `verificationGateEnabled` setting
|
|
184
|
+
* decides whether an unverified stop is also continued once. */
|
|
203
185
|
verificationGate = new VerificationGate();
|
|
204
|
-
/** Mirror of the last
|
|
205
|
-
|
|
206
|
-
verificationArmed = false;
|
|
186
|
+
/** Mirror of the last `hook_armed` value, so the event fires only on an edge. */
|
|
187
|
+
preFinalArmed = false;
|
|
207
188
|
compactionOccurred = false;
|
|
208
189
|
/**
|
|
209
190
|
* Re-grounding carry-over for post-turn compaction. `resetHookState` clears
|
|
@@ -216,6 +197,25 @@ export class AgentSession {
|
|
|
216
197
|
postTurnCompaction;
|
|
217
198
|
lastCompactionCompacted = false;
|
|
218
199
|
compactionRetryAfter = 0;
|
|
200
|
+
/**
|
|
201
|
+
* SoL-Pi plan-step compaction bookkeeping (see compaction/plan-step-policy.ts).
|
|
202
|
+
* Step progress comes from `[DONE:n]` markers in assistant text — the same
|
|
203
|
+
* contract the approved-plan UI tracks.
|
|
204
|
+
*/
|
|
205
|
+
planStepState = {
|
|
206
|
+
planPath: undefined,
|
|
207
|
+
scanIndex: 0,
|
|
208
|
+
doneSteps: new Set(),
|
|
209
|
+
requestsInCompletedSteps: 0,
|
|
210
|
+
requestsThisStep: 0,
|
|
211
|
+
requests: 0,
|
|
212
|
+
grownTokens: 0,
|
|
213
|
+
lastContextTokens: 0,
|
|
214
|
+
compactions: 0,
|
|
215
|
+
writeCostBalance: 0,
|
|
216
|
+
savingPerRequest: 0,
|
|
217
|
+
requestsSinceLastCompaction: 0,
|
|
218
|
+
};
|
|
219
219
|
/** A restored oversized checkpoint must be canonicalized before its first prompt is persisted. */
|
|
220
220
|
deferredCompactionPending = false;
|
|
221
221
|
/** Latest provider count, anchored to the assistant response it measured. */
|
|
@@ -231,8 +231,12 @@ export class AgentSession {
|
|
|
231
231
|
// different message (or past the end) by the time the cancel arrives.
|
|
232
232
|
userQueue = [];
|
|
233
233
|
queueSeq = 0;
|
|
234
|
+
/** Instant interrupt: the running loop's preempt listeners, fired on queueMessage. */
|
|
235
|
+
steeringListeners = new Set();
|
|
234
236
|
processManager;
|
|
235
237
|
lspManager;
|
|
238
|
+
testImpact;
|
|
239
|
+
debugManager;
|
|
236
240
|
subAgentManager;
|
|
237
241
|
/**
|
|
238
242
|
* Out-of-band push notifications (finished children, background-process
|
|
@@ -336,7 +340,6 @@ export class AgentSession {
|
|
|
336
340
|
this.provider = options.provider;
|
|
337
341
|
this.model = options.model;
|
|
338
342
|
this.cwd = options.cwd;
|
|
339
|
-
this.reviewCoverage = new ReviewCoverageTracker(this.cwd);
|
|
340
343
|
this.baseUrl = options.baseUrl;
|
|
341
344
|
this.maxTokens = this.resolveMaxTokens(options.model);
|
|
342
345
|
this.thinkingLevel = options.thinkingLevel;
|
|
@@ -402,7 +405,7 @@ export class AgentSession {
|
|
|
402
405
|
: this.opts.globalSubagents
|
|
403
406
|
? await discoverAgents({ globalAgentsDir: paths.agentsDir })
|
|
404
407
|
: [];
|
|
405
|
-
const { tools: builtInTools, processManager, rebuildReadTool, clearReadTracker, lspManager, subAgentManager, } = await createTools(this.cwd, {
|
|
408
|
+
const { tools: builtInTools, processManager, rebuildReadTool, clearReadTracker, recordBashReads, lspManager, testImpact, debugManager, subAgentManager, } = await createTools(this.cwd, {
|
|
406
409
|
agents,
|
|
407
410
|
skills: this.skills,
|
|
408
411
|
contextLimits: this.contextLimits,
|
|
@@ -433,17 +436,14 @@ export class AgentSession {
|
|
|
433
436
|
}),
|
|
434
437
|
getUseExternalGrep: () => this.settingsManager.get("grepUseRipgrep"),
|
|
435
438
|
authStorage: this.authStorage,
|
|
436
|
-
onFileRead: (filePath) => this.reviewCoverage.recordRead(filePath),
|
|
437
439
|
onFileMutated: (filePath) => {
|
|
438
440
|
const relative = path.relative(this.cwd, filePath) || path.basename(filePath);
|
|
439
441
|
this.hookFileEditCounts.set(relative, (this.hookFileEditCounts.get(relative) ?? 0) + 1);
|
|
440
|
-
this.reviewCoverage.recordChanged(filePath);
|
|
441
442
|
},
|
|
442
443
|
// Lazy — sessionId/model/provider can change after createTools() runs, so
|
|
443
444
|
// sub-agent spawns read the current parent state at execution time.
|
|
444
445
|
getProvider: () => this.provider,
|
|
445
446
|
getModel: () => this.model,
|
|
446
|
-
getThinkingLevel: () => this.thinkingLevel,
|
|
447
447
|
getBaseUrl: () => this.baseUrl,
|
|
448
448
|
getCacheKey: () => this.getPromptCacheKey(),
|
|
449
449
|
getMaxPerModel: () => this.settingsManager.get("subagentMaxPerModel"),
|
|
@@ -474,6 +474,11 @@ export class AgentSession {
|
|
|
474
474
|
// a hallucinated call can't mutate the repo — and buildSystemPrompt below is
|
|
475
475
|
// fed the same filtered names so the Tools section matches exactly.
|
|
476
476
|
this.tools = this.opts.allowedTools ? tools.filter((t) => this.isToolAllowed(t.name)) : tools;
|
|
477
|
+
// Both delegation tools carry the agent roster; when both survive the
|
|
478
|
+
// allow-list, `subagent` points at `spawn_agent`'s copy instead.
|
|
479
|
+
if (this.tools.some((t) => t.name === "spawn_agent")) {
|
|
480
|
+
this.tools = this.tools.map((t) => t.name === "subagent" ? { ...t, description: subAgentDescription(agents, true) } : t);
|
|
481
|
+
}
|
|
477
482
|
// Tier the built-ins: rarely reached schemas move into the tool_search
|
|
478
483
|
// catalog and cost one hint line each instead of a full parameter schema on
|
|
479
484
|
// every request. Allow-listed sessions keep the eager path — their fixed
|
|
@@ -489,12 +494,16 @@ export class AgentSession {
|
|
|
489
494
|
this.mcpCatalog ??= new DeferredToolCatalog(this.contextLimits);
|
|
490
495
|
this.mcpCatalog.add(deferred);
|
|
491
496
|
this.ensureToolSearchTool();
|
|
497
|
+
this.promoteWaitAgentAfterSpawn();
|
|
492
498
|
}
|
|
493
499
|
}
|
|
494
500
|
this.rebuildReadTool = rebuildReadTool;
|
|
495
501
|
this.clearReadTracker = clearReadTracker;
|
|
502
|
+
this.recordBashReads = recordBashReads;
|
|
496
503
|
this.processManager = processManager;
|
|
497
504
|
this.lspManager = lspManager;
|
|
505
|
+
this.testImpact = testImpact;
|
|
506
|
+
this.debugManager = debugManager;
|
|
498
507
|
this.subAgentManager = subAgentManager;
|
|
499
508
|
this.bindManagerCancellation(this.opts.signal);
|
|
500
509
|
// Connect MCP servers. Child sessions skip user-configured servers to avoid
|
|
@@ -779,6 +788,29 @@ export class AgentSession {
|
|
|
779
788
|
: { serverName, ok: false, error: outcome.error };
|
|
780
789
|
}, this.contextLimits));
|
|
781
790
|
}
|
|
791
|
+
/**
|
|
792
|
+
* `wait_agent` is deferred, yet nearly every `spawn_agent` is followed by it,
|
|
793
|
+
* so the model spent a whole turn on `tool_search` just to load it (bench 41:
|
|
794
|
+
* ~6 s per fan-out). Promote it as soon as a spawn succeeds instead: the tool
|
|
795
|
+
* list grows exactly as it would after that `tool_search`, one turn earlier,
|
|
796
|
+
* and sessions that never spawn keep the smaller prefix.
|
|
797
|
+
*/
|
|
798
|
+
promoteWaitAgentAfterSpawn() {
|
|
799
|
+
const index = this.tools.findIndex((t) => t.name === "spawn_agent");
|
|
800
|
+
const spawn = index >= 0 ? this.tools[index] : undefined;
|
|
801
|
+
if (!spawn)
|
|
802
|
+
return;
|
|
803
|
+
this.tools[index] = {
|
|
804
|
+
...spawn,
|
|
805
|
+
execute: async (args, context) => {
|
|
806
|
+
const result = await spawn.execute(args, context);
|
|
807
|
+
if (!this.tools.some((t) => t.name === "wait_agent")) {
|
|
808
|
+
this.tools.push(...(this.mcpCatalog?.promote(["wait_agent"]) ?? []));
|
|
809
|
+
}
|
|
810
|
+
return result;
|
|
811
|
+
},
|
|
812
|
+
};
|
|
813
|
+
}
|
|
782
814
|
/** Append tools, replacing any same-named entry (cached stub → live tool). */
|
|
783
815
|
replaceOrPushTools(tools) {
|
|
784
816
|
for (const tool of tools) {
|
|
@@ -917,6 +949,7 @@ export class AgentSession {
|
|
|
917
949
|
kind: "prompt",
|
|
918
950
|
visibility: "transcript",
|
|
919
951
|
}, options = {}) {
|
|
952
|
+
this.prewarmController?.abort();
|
|
920
953
|
await this.settlePostTurnCompaction();
|
|
921
954
|
await this.adoptDeferredCheckpointBeforePrompt();
|
|
922
955
|
const slash = await this.resolveSlashInput(content);
|
|
@@ -952,6 +985,7 @@ export class AgentSession {
|
|
|
952
985
|
* attachments are always a direct conversational turn.
|
|
953
986
|
*/
|
|
954
987
|
async promptWithAttachments(text, attachments) {
|
|
988
|
+
this.prewarmController?.abort();
|
|
955
989
|
await this.settlePostTurnCompaction();
|
|
956
990
|
await this.adoptDeferredCheckpointBeforePrompt();
|
|
957
991
|
const parts = this.buildAttachmentParts(text, attachments);
|
|
@@ -1050,15 +1084,7 @@ export class AgentSession {
|
|
|
1050
1084
|
resetHookState(originalRequest) {
|
|
1051
1085
|
this.opts.completionReview?.begin(originalRequest);
|
|
1052
1086
|
this.lspManager?.clearPendingDiagnostics();
|
|
1053
|
-
this.hookStats = {
|
|
1054
|
-
changedLines: 0,
|
|
1055
|
-
toolCalls: 0,
|
|
1056
|
-
toolFailures: 0,
|
|
1057
|
-
turns: 0,
|
|
1058
|
-
writeCalls: 0,
|
|
1059
|
-
editCalls: 0,
|
|
1060
|
-
bashCalls: 0,
|
|
1061
|
-
};
|
|
1087
|
+
this.hookStats = { toolCalls: 0, toolFailures: 0, turns: 0 };
|
|
1062
1088
|
this.hookText = "";
|
|
1063
1089
|
this.hookConsecutiveFailures = 0;
|
|
1064
1090
|
this.hookRepeatedNoProgressCalls = 0;
|
|
@@ -1067,13 +1093,6 @@ export class AgentSession {
|
|
|
1067
1093
|
this.hookCyclicPattern = null;
|
|
1068
1094
|
this.hookFileEditCounts.clear();
|
|
1069
1095
|
this.hookToolCalls.clear();
|
|
1070
|
-
this.reviewCoverage.reset();
|
|
1071
|
-
this.reviewCoverageInjected = 0;
|
|
1072
|
-
this.idealReviewPhase = "idle";
|
|
1073
|
-
// No event here: clients reset their own hold on run_start.
|
|
1074
|
-
this.idealReviewArmed = false;
|
|
1075
|
-
this.verificationArmed = false;
|
|
1076
|
-
this.idealDriftProbe = null;
|
|
1077
1096
|
this.loopBreakInjected = 0;
|
|
1078
1097
|
this.regroundingInjected = false;
|
|
1079
1098
|
this.hookRecentCalls = [];
|
|
@@ -1084,7 +1103,6 @@ export class AgentSession {
|
|
|
1084
1103
|
verdict: null,
|
|
1085
1104
|
injected: false,
|
|
1086
1105
|
};
|
|
1087
|
-
this.independentReviewStarted = false;
|
|
1088
1106
|
this.runStartedAt = Date.now();
|
|
1089
1107
|
this.processGateInjected = 0;
|
|
1090
1108
|
this.verificationGate.beginRun();
|
|
@@ -1105,8 +1123,8 @@ export class AgentSession {
|
|
|
1105
1123
|
}
|
|
1106
1124
|
/**
|
|
1107
1125
|
* Fold one agent event into the hook stat accumulators. Pure bookkeeping —
|
|
1108
|
-
* the same signals the TUI's useAgentLoop collects, so
|
|
1109
|
-
*
|
|
1126
|
+
* the same signals the TUI's useAgentLoop collects, so loop-break decisions
|
|
1127
|
+
* match across the CLI and the app.
|
|
1110
1128
|
*/
|
|
1111
1129
|
async trackHookEvent(event) {
|
|
1112
1130
|
if (this.opts.completionReview) {
|
|
@@ -1172,12 +1190,6 @@ export class AgentSession {
|
|
|
1172
1190
|
this.hookStats.toolCalls += 1;
|
|
1173
1191
|
if (event.isError)
|
|
1174
1192
|
this.hookStats.toolFailures += 1;
|
|
1175
|
-
if (name === "write")
|
|
1176
|
-
this.hookStats.writeCalls += 1;
|
|
1177
|
-
if (name === "edit")
|
|
1178
|
-
this.hookStats.editCalls += 1;
|
|
1179
|
-
if (name === "bash")
|
|
1180
|
-
this.hookStats.bashCalls += 1;
|
|
1181
1193
|
this.hookConsecutiveFailures = event.isError ? this.hookConsecutiveFailures + 1 : 0;
|
|
1182
1194
|
this.hookRepeatedNoProgressCalls = this.hookProgressTracker.record(name, args, event.result, event.isError);
|
|
1183
1195
|
this.hookCyclicPattern = this.hookCycleDetector.record(name, args, event.result, event.isError);
|
|
@@ -1194,27 +1206,23 @@ export class AgentSession {
|
|
|
1194
1206
|
if (this.hookRecentCalls.length > MAX_SEMANTIC_LOOP_CALLS) {
|
|
1195
1207
|
this.hookRecentCalls.splice(0, this.hookRecentCalls.length - MAX_SEMANTIC_LOOP_CALLS);
|
|
1196
1208
|
}
|
|
1197
|
-
if (name === "edit" && !event.isError) {
|
|
1198
|
-
const diff = event.details?.diff ?? event.result;
|
|
1199
|
-
const added = (diff.match(/^\+[^+]/gm) ?? []).length;
|
|
1200
|
-
const removed = (diff.match(/^-[^-]/gm) ?? []).length;
|
|
1201
|
-
this.hookStats.changedLines += added + removed;
|
|
1202
|
-
}
|
|
1203
1209
|
// Only host-observed successful mutations and trustworthy check results
|
|
1204
1210
|
// affect approval. The model's text is never evidence.
|
|
1205
1211
|
let verificationChanged = false;
|
|
1206
1212
|
if (!event.isError && args) {
|
|
1207
1213
|
if (name === "edit" || name === "write") {
|
|
1208
|
-
const filePath = String(args.file_path ?? "");
|
|
1209
1214
|
// Check-owning files (tsconfig.json, pytest.ini, vitest.config.ts …)
|
|
1210
1215
|
// are tracked even when they are not source code: editing one is how
|
|
1211
|
-
// a red suite is turned green without fixing anything.
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
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
|
+
}
|
|
1218
1226
|
}
|
|
1219
1227
|
}
|
|
1220
1228
|
}
|
|
@@ -1222,8 +1230,13 @@ export class AgentSession {
|
|
|
1222
1230
|
const command = typeof args.command === "string" ? args.command : "";
|
|
1223
1231
|
const classification = classifyVerificationCommand(command);
|
|
1224
1232
|
if (classification.accepted || classification.snapshotEligible) {
|
|
1225
|
-
|
|
1226
|
-
|
|
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];
|
|
1227
1240
|
// No parseable ID means the check cannot be tracked to a real exit
|
|
1228
1241
|
// code — no evidence either way. Recording a FAILURE here made
|
|
1229
1242
|
// every later green run of a different spelling look owed.
|
|
@@ -1294,10 +1307,35 @@ export class AgentSession {
|
|
|
1294
1307
|
// Tool results for this step are in the array and their side effects
|
|
1295
1308
|
// already hit the filesystem. Flushing here is what makes a crash lose
|
|
1296
1309
|
// at most the in-flight step instead of the entire turn.
|
|
1310
|
+
await this.creditBashReads();
|
|
1297
1311
|
await this.flushPendingMessages();
|
|
1298
1312
|
break;
|
|
1299
1313
|
}
|
|
1300
1314
|
}
|
|
1315
|
+
/**
|
|
1316
|
+
* Count full-file `cat` output from the step that just finished as reads, so
|
|
1317
|
+
* an edit after `cat` does not cost a second read. Uses the step's results as
|
|
1318
|
+
* stored in the transcript (after per-turn trimming): only bytes the model
|
|
1319
|
+
* actually received count.
|
|
1320
|
+
*/
|
|
1321
|
+
async creditBashReads() {
|
|
1322
|
+
if (!this.recordBashReads)
|
|
1323
|
+
return;
|
|
1324
|
+
const messages = this.activeLoopMessages ?? this.messages;
|
|
1325
|
+
const toolMessage = messages.at(-1);
|
|
1326
|
+
const assistant = messages.at(-2);
|
|
1327
|
+
if (toolMessage?.role !== "tool" || assistant?.role !== "assistant")
|
|
1328
|
+
return;
|
|
1329
|
+
if (typeof assistant.content === "string")
|
|
1330
|
+
return;
|
|
1331
|
+
const calls = assistant.content.filter((part) => part.type === "tool_call");
|
|
1332
|
+
try {
|
|
1333
|
+
await this.recordBashReads(calls, toolMessage.content);
|
|
1334
|
+
}
|
|
1335
|
+
catch (err) {
|
|
1336
|
+
log("WARN", "agent-session", "Crediting bash reads failed", { error: String(err) });
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1301
1339
|
/**
|
|
1302
1340
|
* Append every message added since the last flush to the session file.
|
|
1303
1341
|
*
|
|
@@ -1360,7 +1398,7 @@ export class AgentSession {
|
|
|
1360
1398
|
const diagnosticText = this.lspManager?.drainDiagnostics(this.getVerificationProblem() !== null, { deferUnverified: true });
|
|
1361
1399
|
if (diagnosticText)
|
|
1362
1400
|
this.eventBus.emit("diagnostics", { text: diagnosticText });
|
|
1363
|
-
this.
|
|
1401
|
+
this.refreshHookArming();
|
|
1364
1402
|
const notified = this.notifications.drain();
|
|
1365
1403
|
const notificationMessage = notified.length > 0 || diagnosticText
|
|
1366
1404
|
? {
|
|
@@ -1423,6 +1461,7 @@ export class AgentSession {
|
|
|
1423
1461
|
}
|
|
1424
1462
|
if (this.opts.selfCorrectionHooks === false)
|
|
1425
1463
|
return null;
|
|
1464
|
+
// Legacy key: the user-facing switch for loop-break and re-grounding nudges.
|
|
1426
1465
|
if (!this.settingsManager.get("idealReviewEnabled"))
|
|
1427
1466
|
return null;
|
|
1428
1467
|
// Deterministic stuck verdict, computed once and shared: the semantic
|
|
@@ -1622,83 +1661,6 @@ export class AgentSession {
|
|
|
1622
1661
|
.join("\n")
|
|
1623
1662
|
: "";
|
|
1624
1663
|
}
|
|
1625
|
-
/** Independent fresh-context review of the finished work (Codex Guardian
|
|
1626
|
-
* pattern). Spawns a READ-ONLY child on the ACTIVE model, waits bounded,
|
|
1627
|
-
* and returns findings for the acting agent to address — or nothing when
|
|
1628
|
-
* the review passes, is unavailable, or fails (in-thread review remains the
|
|
1629
|
-
* fallback; the feature degrades, never blocks).
|
|
1630
|
-
*
|
|
1631
|
-
* Runs inside the pre-stop poll, so the candidate final answer is already
|
|
1632
|
-
* held by arming and this wait cannot race a streamed answer. */
|
|
1633
|
-
async runIndependentReview(decision) {
|
|
1634
|
-
if (!this.subAgentManager)
|
|
1635
|
-
return [];
|
|
1636
|
-
if (this.independentReviewStarted)
|
|
1637
|
-
return [];
|
|
1638
|
-
// An allow-listed session (a subagent worker itself) must not spawn
|
|
1639
|
-
// harness-owned grandchildren the tool policy never granted.
|
|
1640
|
-
if (this.opts.allowedTools && !this.opts.allowedTools.includes("spawn_agent"))
|
|
1641
|
-
return [];
|
|
1642
|
-
if (decision.score < INDEPENDENT_REVIEW_SCORE_THRESHOLD)
|
|
1643
|
-
return [];
|
|
1644
|
-
this.independentReviewStarted = true;
|
|
1645
|
-
const taskName = `ideal-reviewer-${Math.random().toString(36).slice(2, 8)}`;
|
|
1646
|
-
let agentId;
|
|
1647
|
-
try {
|
|
1648
|
-
const task = buildReviewerTask({
|
|
1649
|
-
originalRequest: this.originalRequest,
|
|
1650
|
-
changedFiles: [...this.hookFileEditCounts.keys()],
|
|
1651
|
-
stats: this.hookStats,
|
|
1652
|
-
triggerReasons: decision.reasons,
|
|
1653
|
-
});
|
|
1654
|
-
// Active model forced at spawn time — never routed to a fast/review model.
|
|
1655
|
-
// The reviewer's own time limit ends it with a verdict on what it read;
|
|
1656
|
-
// the wait below is only a backstop against a hung child.
|
|
1657
|
-
const snapshot = await this.subAgentManager.spawn(taskName, task, undefined, {
|
|
1658
|
-
model: this.model,
|
|
1659
|
-
tools: REVIEWER_TOOLS,
|
|
1660
|
-
turnTimeoutMs: REVIEWER_TURN_TIMEOUT_MS,
|
|
1661
|
-
});
|
|
1662
|
-
agentId = snapshot.agent_id;
|
|
1663
|
-
const waited = await this.subAgentManager.wait([agentId], "all", REVIEWER_WAIT_MS);
|
|
1664
|
-
const agent = waited.agents[0];
|
|
1665
|
-
if (!agent || !isTerminalSubAgentState(agent.state)) {
|
|
1666
|
-
// Timeout: collect the straggler so the completion gate cannot fire on
|
|
1667
|
-
// it later, then fall back to the in-thread review.
|
|
1668
|
-
await this.subAgentManager.interrupt(agentId, true).catch(() => { });
|
|
1669
|
-
log("WARN", "ideal", "Independent reviewer timed out; falling back to in-thread review", {
|
|
1670
|
-
agentId,
|
|
1671
|
-
});
|
|
1672
|
-
return [];
|
|
1673
|
-
}
|
|
1674
|
-
const findings = parseReviewerFindings(agent.output ?? "");
|
|
1675
|
-
if (!findings) {
|
|
1676
|
-
log("WARN", "ideal", "Independent reviewer output unparseable; falling back", {
|
|
1677
|
-
agentId,
|
|
1678
|
-
state: agent.state,
|
|
1679
|
-
...(agent.error ? { error: agent.error } : {}),
|
|
1680
|
-
});
|
|
1681
|
-
return [];
|
|
1682
|
-
}
|
|
1683
|
-
if (findings.clean) {
|
|
1684
|
-
log("INFO", "ideal", "Independent reviewer verdict: clean", { agentId });
|
|
1685
|
-
return [];
|
|
1686
|
-
}
|
|
1687
|
-
log("INFO", "ideal", "Independent reviewer flagged findings", {
|
|
1688
|
-
agentId,
|
|
1689
|
-
count: String(findings.findings.length),
|
|
1690
|
-
});
|
|
1691
|
-
return [buildIndependentReviewMessage(findings.findings)];
|
|
1692
|
-
}
|
|
1693
|
-
catch (error) {
|
|
1694
|
-
if (agentId)
|
|
1695
|
-
await this.subAgentManager.interrupt(agentId, true).catch(() => { });
|
|
1696
|
-
log("WARN", "ideal", "Independent reviewer failed; falling back to in-thread review", {
|
|
1697
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1698
|
-
});
|
|
1699
|
-
return [];
|
|
1700
|
-
}
|
|
1701
|
-
}
|
|
1702
1664
|
/**
|
|
1703
1665
|
* Turn-budget extension gate. The loop consults this instead of stopping
|
|
1704
1666
|
* mid-task when it exhausts `maxTurns`. Grant ONLY on evidence of progress —
|
|
@@ -1732,98 +1694,45 @@ export class AgentSession {
|
|
|
1732
1694
|
});
|
|
1733
1695
|
return granted;
|
|
1734
1696
|
}
|
|
1735
|
-
/**
|
|
1736
|
-
*
|
|
1737
|
-
*
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
* The turn count is looked ahead by one on purpose. `hookStats.turns` only
|
|
1741
|
-
* advances at `turn_end`, so while the model is writing the draft the counter
|
|
1742
|
-
* still reads the PREVIOUS turn; the real gate sees one more. Without the
|
|
1743
|
-
* lookahead a run sitting on score 3 crosses to 4 on the draft's own
|
|
1744
|
-
* `turn_end` — after the text already streamed — which is precisely the
|
|
1745
|
-
* appear-then-vanish flash. Over-arming by one turn point costs only live
|
|
1746
|
-
* token streaming on a final answer that then shows whole; under-arming costs
|
|
1747
|
-
* the flash, so this errs toward arming.
|
|
1748
|
-
*/
|
|
1749
|
-
wouldInjectIdealReview() {
|
|
1750
|
-
if (this.opts.completionReview?.armed)
|
|
1751
|
-
return true;
|
|
1752
|
-
if (this.opts.selfCorrectionHooks === false || this.idealReviewSuppressed)
|
|
1753
|
-
return false;
|
|
1754
|
-
// Mid-review a stop still injects: the coverage follow-up while files are
|
|
1755
|
-
// unread, or its escalation once the budget is spent. Both make the model
|
|
1756
|
-
// answer again, so the candidate answer is a draft exactly as it is before
|
|
1757
|
-
// the review starts — without arming here it paints and the reviewed answer
|
|
1758
|
-
// lands under it as a duplicate.
|
|
1759
|
-
if (this.idealReviewPhase === "reviewing") {
|
|
1760
|
-
return this.reviewCoverage.evidence().missing.length > 0;
|
|
1761
|
-
}
|
|
1762
|
-
if (this.idealReviewPhase !== "idle")
|
|
1763
|
-
return false;
|
|
1764
|
-
if (!this.settingsManager.get("idealReviewEnabled"))
|
|
1697
|
+
/** Is the pre-stop verification gate active for this session? Off by the
|
|
1698
|
+
* `verificationGateEnabled` setting, by `selfCorrectionHooks: false`, and for
|
|
1699
|
+
* allow-listed sessions that cannot run commands at all. */
|
|
1700
|
+
verificationGateActive() {
|
|
1701
|
+
if (this.opts.selfCorrectionHooks === false)
|
|
1765
1702
|
return false;
|
|
1766
|
-
if (
|
|
1767
|
-
return true;
|
|
1768
|
-
}
|
|
1769
|
-
const files = this.hookFileEditCounts.size;
|
|
1770
|
-
if (files === 0)
|
|
1703
|
+
if (!this.settingsManager.get("verificationGateEnabled"))
|
|
1771
1704
|
return false;
|
|
1772
|
-
|
|
1773
|
-
this.idealDriftProbe = {
|
|
1774
|
-
files,
|
|
1775
|
-
drifted: detectTestDrift(this.hookFileEditCounts.keys(), this.cwd).length > 0,
|
|
1776
|
-
};
|
|
1777
|
-
}
|
|
1778
|
-
return this.idealDriftProbe.drifted;
|
|
1705
|
+
return !this.opts.allowedTools || this.opts.allowedTools.includes("bash");
|
|
1779
1706
|
}
|
|
1780
|
-
/** Would a stop right now inject
|
|
1781
|
-
*
|
|
1782
|
-
|
|
1707
|
+
/** Would a stop right now inject a pre-final follow-up? Queued LSP
|
|
1708
|
+
* diagnostics (real errors injected below), a mode-owned completion review,
|
|
1709
|
+
* or the verification gate can, so clients hold the candidate answer only
|
|
1710
|
+
* then. Same conditions as the pre-stop branch, so arming and injection
|
|
1711
|
+
* cannot disagree. */
|
|
1712
|
+
wouldInjectBeforeFinal() {
|
|
1713
|
+
if (this.opts.completionReview?.armed)
|
|
1714
|
+
return true;
|
|
1783
1715
|
if (this.lspManager?.hasQueuedDiagnostics())
|
|
1784
1716
|
return true;
|
|
1785
|
-
|
|
1786
|
-
return false;
|
|
1787
|
-
if (!this.settingsManager.get("verificationGateEnabled"))
|
|
1788
|
-
return false;
|
|
1789
|
-
if (this.opts.allowedTools && !this.opts.allowedTools.includes("bash"))
|
|
1790
|
-
return false;
|
|
1791
|
-
return this.verificationGate.willInject();
|
|
1717
|
+
return this.verificationGateActive() && this.verificationGate.willInject();
|
|
1792
1718
|
}
|
|
1793
1719
|
/** Broadcast pre-final hook arming on change. Both edges matter: armed=false
|
|
1794
|
-
* after the hook fires is what lets a client stream the
|
|
1795
|
-
*
|
|
1796
|
-
*
|
|
1797
|
-
* Callable before `initialize()`: the sidecar sets Nolan's review suppression
|
|
1798
|
-
* on a freshly constructed session, and every arming predicate below reads
|
|
1799
|
-
* settings that `initialize()` has not loaded yet. Nothing can be armed
|
|
1800
|
-
* before the session can run a turn, and the first `tool_result`/`turn_end`
|
|
1801
|
-
* recomputes both edges — so skipping is the correct answer, not a patch. */
|
|
1720
|
+
* after the hook fires is what lets a client stream the final answer live
|
|
1721
|
+
* again. Callable before `initialize()`, when no manager exists yet. */
|
|
1802
1722
|
refreshHookArming() {
|
|
1723
|
+
// Before `initialize()` settings are not loaded, so nothing can be armed.
|
|
1803
1724
|
if (!this.settingsManager)
|
|
1804
1725
|
return;
|
|
1805
|
-
this.
|
|
1806
|
-
this.
|
|
1807
|
-
}
|
|
1808
|
-
refreshVerificationArmed() {
|
|
1809
|
-
if (!this.settingsManager)
|
|
1726
|
+
const armed = this.wouldInjectBeforeFinal();
|
|
1727
|
+
if (armed === this.preFinalArmed)
|
|
1810
1728
|
return;
|
|
1811
|
-
|
|
1812
|
-
if (armed === this.verificationArmed)
|
|
1813
|
-
return;
|
|
1814
|
-
this.verificationArmed = armed;
|
|
1729
|
+
this.preFinalArmed = armed;
|
|
1815
1730
|
this.eventBus.emit("hook_armed", { kind: "verification", armed });
|
|
1816
1731
|
}
|
|
1817
|
-
refreshIdealReviewArmed() {
|
|
1818
|
-
const armed = this.wouldInjectIdealReview();
|
|
1819
|
-
if (armed === this.idealReviewArmed)
|
|
1820
|
-
return;
|
|
1821
|
-
this.idealReviewArmed = armed;
|
|
1822
|
-
this.eventBus.emit("hook_armed", { kind: "ideal", armed });
|
|
1823
|
-
}
|
|
1824
1732
|
/**
|
|
1825
|
-
* Pre-stop
|
|
1826
|
-
*
|
|
1733
|
+
* Pre-stop follow-ups: LSP errors, unread child agents and background
|
|
1734
|
+
* processes, the verification gate (when `verificationGateEnabled`), and a
|
|
1735
|
+
* mode-owned completion review.
|
|
1827
1736
|
*/
|
|
1828
1737
|
async getHookFollowUpMessages() {
|
|
1829
1738
|
// Exit notifications and task_output refer to the same host process record.
|
|
@@ -1836,14 +1745,15 @@ export class AgentSession {
|
|
|
1836
1745
|
if (backgroundChanged)
|
|
1837
1746
|
await this.persistVerificationState();
|
|
1838
1747
|
// Edits return immediately; only the completion boundary waits for remaining
|
|
1839
|
-
// checks. Queued timeouts stay explicitly unverified, never a false
|
|
1748
|
+
// checks. Queued timeouts stay explicitly unverified, never a false
|
|
1749
|
+
// all-clear; they join the verification demand below when the gate is on.
|
|
1840
1750
|
await this.lspManager?.flushDiagnostics(this.opts.signal);
|
|
1841
1751
|
if (this.opts.signal?.aborted)
|
|
1842
1752
|
return null;
|
|
1843
|
-
const diagnosticText = this.lspManager?.drainDiagnostics(this.getVerificationProblem() !== null);
|
|
1753
|
+
const diagnosticText = this.lspManager?.drainDiagnostics(this.verificationGateActive() && this.getVerificationProblem() !== null);
|
|
1844
1754
|
if (diagnosticText)
|
|
1845
1755
|
this.eventBus.emit("diagnostics", { text: diagnosticText });
|
|
1846
|
-
this.
|
|
1756
|
+
this.refreshHookArming();
|
|
1847
1757
|
const diagnosticMessages = diagnosticText
|
|
1848
1758
|
? [
|
|
1849
1759
|
{
|
|
@@ -1868,18 +1778,29 @@ export class AgentSession {
|
|
|
1868
1778
|
return [...diagnosticMessages, ...processFollowUp];
|
|
1869
1779
|
}
|
|
1870
1780
|
// Verification gate: code was edited but nothing verified since the last
|
|
1871
|
-
// edit.
|
|
1872
|
-
|
|
1873
|
-
if (this.opts.selfCorrectionHooks !== false &&
|
|
1874
|
-
this.settingsManager.get("verificationGateEnabled") &&
|
|
1875
|
-
(!this.opts.allowedTools || this.opts.allowedTools.includes("bash"))) {
|
|
1781
|
+
// edit. Off via the `verificationGateEnabled` setting.
|
|
1782
|
+
if (this.verificationGateActive()) {
|
|
1876
1783
|
const verificationReason = this.verificationGate.pendingReason();
|
|
1784
|
+
const pendingFiles = this.verificationGate.pendingFiles();
|
|
1877
1785
|
const verificationFollowUp = this.verificationGate.followUp();
|
|
1878
1786
|
if (verificationFollowUp) {
|
|
1787
|
+
// Name the tests that actually reach the unverified files, with the
|
|
1788
|
+
// command that runs exactly those, so the check is targeted rather
|
|
1789
|
+
// than guessed. Best-effort: no index or no runner leaves it unchanged.
|
|
1790
|
+
if (this.testImpact &&
|
|
1791
|
+
(verificationReason === "initial" || verificationReason === "recheck")) {
|
|
1792
|
+
const impactLine = await this.testImpact
|
|
1793
|
+
.impactFor(pendingFiles)
|
|
1794
|
+
.then(formatImpactForVerification)
|
|
1795
|
+
.catch(() => "");
|
|
1796
|
+
const first = verificationFollowUp[0];
|
|
1797
|
+
if (impactLine && first?.role === "user" && typeof first.content === "string") {
|
|
1798
|
+
verificationFollowUp[0] = { ...first, content: first.content + impactLine };
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1879
1801
|
log("INFO", "verification-gate", "Injecting verification follow-up", {});
|
|
1880
1802
|
// Announce, THEN disarm: clients release held text on disarm, so the
|
|
1881
|
-
// reverse order paints the draft and immediately deletes it
|
|
1882
|
-
// flash arming exists to prevent.
|
|
1803
|
+
// reverse order paints the draft and immediately deletes it.
|
|
1883
1804
|
this.eventBus.emit("hook", {
|
|
1884
1805
|
kind: "verification",
|
|
1885
1806
|
...(verificationReason === "tamper"
|
|
@@ -1892,7 +1813,7 @@ export class AgentSession {
|
|
|
1892
1813
|
return [...diagnosticMessages, ...verificationFollowUp];
|
|
1893
1814
|
}
|
|
1894
1815
|
}
|
|
1895
|
-
// Address real errors before review; unavailable checks share the
|
|
1816
|
+
// Address real errors before review; unavailable checks share the
|
|
1896
1817
|
// verification demand above instead of manufacturing a separate hook.
|
|
1897
1818
|
if (diagnosticMessages.length > 0)
|
|
1898
1819
|
return diagnosticMessages;
|
|
@@ -1912,131 +1833,24 @@ export class AgentSession {
|
|
|
1912
1833
|
}
|
|
1913
1834
|
this.refreshHookArming();
|
|
1914
1835
|
}
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
// model answer again, so the answer it interrupts is a draft and the
|
|
1929
|
-
// hook event is what tells clients to discard it. Injecting silently is
|
|
1930
|
-
// what let the pre-coverage answer paint above the reviewed one.
|
|
1931
|
-
this.eventBus.emit("hook", {
|
|
1932
|
-
kind: "ideal",
|
|
1933
|
-
coverageExpected: coverage.expected,
|
|
1934
|
-
coverageMissing: coverage.missing,
|
|
1935
|
-
});
|
|
1936
|
-
if (this.reviewCoverageInjected < MAX_REVIEW_COVERAGE_INJECTIONS) {
|
|
1937
|
-
this.reviewCoverageInjected += 1;
|
|
1938
|
-
// Stays armed (coverage is still outstanding) — this call is here so a
|
|
1939
|
-
// client that missed the earlier edge is armed before the next draft.
|
|
1940
|
-
this.refreshIdealReviewArmed();
|
|
1941
|
-
return [
|
|
1942
|
-
this.withReviewLspEvidence(buildReviewCoverageMessage(coverage.missing), lspEvidence),
|
|
1943
|
-
];
|
|
1944
|
-
}
|
|
1945
|
-
// Budget spent: close the gate so the run cannot spin on a file that
|
|
1946
|
-
// never becomes readable, and require the gap be reported to the user.
|
|
1947
|
-
this.idealReviewPhase = "complete";
|
|
1948
|
-
// The gate is shut, so this is the real disarm: the answer to the
|
|
1949
|
-
// escalation is final and streams live.
|
|
1950
|
-
this.refreshIdealReviewArmed();
|
|
1951
|
-
log("INFO", "ideal", "Ideal review coverage escalated after retry budget", {
|
|
1952
|
-
injected: String(this.reviewCoverageInjected),
|
|
1953
|
-
missing: coverage.missing,
|
|
1954
|
-
});
|
|
1955
|
-
return [buildReviewCoverageEscalationMessage(coverage.missing)];
|
|
1956
|
-
}
|
|
1957
|
-
this.idealReviewPhase = "complete";
|
|
1958
|
-
return null;
|
|
1836
|
+
return null;
|
|
1837
|
+
}
|
|
1838
|
+
/** Wraps the real run: cancels an in-flight cache prewarm and tracks run
|
|
1839
|
+
* activity / last real request time for {@link prewarm}. */
|
|
1840
|
+
async runLoop(options = {}) {
|
|
1841
|
+
this.prewarmController?.abort();
|
|
1842
|
+
this.runLoopDepth++;
|
|
1843
|
+
try {
|
|
1844
|
+
await this.runLoopInner(options);
|
|
1845
|
+
}
|
|
1846
|
+
finally {
|
|
1847
|
+
this.runLoopDepth--;
|
|
1848
|
+
this.lastRealRequestAt = Date.now();
|
|
1959
1849
|
}
|
|
1960
|
-
if (this.idealReviewPhase === "complete")
|
|
1961
|
-
return null;
|
|
1962
|
-
if (!this.settingsManager.get("idealReviewEnabled"))
|
|
1963
|
-
return null;
|
|
1964
|
-
const decision = evaluateIdealReview(this.hookStats);
|
|
1965
|
-
// Test drift fires the review even on a small change the score would skip:
|
|
1966
|
-
// a green-but-stale test is exactly what the volume gate sleeps through.
|
|
1967
|
-
const driftedFiles = detectTestDrift(this.hookFileEditCounts.keys(), this.cwd).slice(0, 5);
|
|
1968
|
-
if (!decision.shouldReview && driftedFiles.length === 0)
|
|
1969
|
-
return null;
|
|
1970
|
-
// Independent reviewer first (async, bounded): its findings ride in the
|
|
1971
|
-
// SAME follow-up batch as the in-thread review + coverage requirements, so
|
|
1972
|
-
// addressing everything still costs one extra turn.
|
|
1973
|
-
this.reviewCoverage.start(this.hookFileEditCounts.keys());
|
|
1974
|
-
this.idealReviewPhase = "reviewing";
|
|
1975
|
-
const coverage = this.reviewCoverage.evidence();
|
|
1976
|
-
const lspEvidence = this.reviewLspEvidence(coverage.expected);
|
|
1977
|
-
this.eventBus.emit("hook", {
|
|
1978
|
-
kind: "ideal",
|
|
1979
|
-
coverageExpected: coverage.expected,
|
|
1980
|
-
coverageMissing: coverage.missing,
|
|
1981
|
-
});
|
|
1982
|
-
// Recompute strictly AFTER the hook event: clients release held text on
|
|
1983
|
-
// disarm, so the reverse order would paint the draft and then delete it —
|
|
1984
|
-
// the exact flash arming exists to prevent. Arming normally PERSISTS here,
|
|
1985
|
-
// because review starts with every changed file uncovered and a stop while
|
|
1986
|
-
// coverage is outstanding injects again. Disarm lands later, on the read
|
|
1987
|
-
// that closes the last gap (or when the retry budget escalates).
|
|
1988
|
-
this.refreshIdealReviewArmed();
|
|
1989
|
-
// Announce the phase before the reviewer starts, not after its bounded wait.
|
|
1990
|
-
const independentMessages = await this.runIndependentReview(decision);
|
|
1991
|
-
log("INFO", "ideal", "Injecting ideal review before final response", {
|
|
1992
|
-
coverageExpected: coverage.expected,
|
|
1993
|
-
coverageMissing: coverage.missing,
|
|
1994
|
-
lspLowConfidence: lspEvidence.lowConfidence,
|
|
1995
|
-
lspMissing: lspEvidence.missing,
|
|
1996
|
-
});
|
|
1997
|
-
return [
|
|
1998
|
-
...independentMessages,
|
|
1999
|
-
this.withReviewLspEvidence(withReviewCoverageRequirements(buildIdealReviewMessage(decision.reasons, driftedFiles), coverage.missing), lspEvidence),
|
|
2000
|
-
];
|
|
2001
|
-
}
|
|
2002
|
-
reviewLspEvidence(files) {
|
|
2003
|
-
const lowConfidence = [];
|
|
2004
|
-
const missing = [];
|
|
2005
|
-
for (const filePath of files) {
|
|
2006
|
-
const outcome = this.lspManager?.getLatestOutcome(filePath);
|
|
2007
|
-
if (outcome?.kind === "low_confidence")
|
|
2008
|
-
lowConfidence.push(filePath);
|
|
2009
|
-
else if (outcome?.kind !== "clean" && outcome?.kind !== "diagnostics")
|
|
2010
|
-
missing.push(filePath);
|
|
2011
|
-
}
|
|
2012
|
-
return { lowConfidence, missing };
|
|
2013
|
-
}
|
|
2014
|
-
withReviewLspEvidence(message, evidence) {
|
|
2015
|
-
if (evidence.lowConfidence.length === 0 && evidence.missing.length === 0)
|
|
2016
|
-
return message;
|
|
2017
|
-
const notes = [
|
|
2018
|
-
...(evidence.lowConfidence.length > 0
|
|
2019
|
-
? [`Diagnostics are low confidence while indexing: ${evidence.lowConfidence.join(", ")}.`]
|
|
2020
|
-
: []),
|
|
2021
|
-
...(evidence.missing.length > 0
|
|
2022
|
-
? [`Diagnostics evidence is unavailable or missing: ${evidence.missing.join(", ")}.`]
|
|
2023
|
-
: []),
|
|
2024
|
-
"Do not describe those files as compiler-clean without other evidence.",
|
|
2025
|
-
];
|
|
2026
|
-
return {
|
|
2027
|
-
role: "user",
|
|
2028
|
-
provenance: message.provenance,
|
|
2029
|
-
content: `${String(message.content)}\n\n${notes.join(" ")}`,
|
|
2030
|
-
};
|
|
2031
1850
|
}
|
|
2032
1851
|
/** Auto-compact if needed, run agent loop with auth retry, and persist messages. */
|
|
2033
|
-
async
|
|
2034
|
-
|
|
2035
|
-
// during the previous turn gets its packs; the prompt is rebuilt only when
|
|
2036
|
-
// the set grows, keeping the cached prefix stable otherwise.
|
|
2037
|
-
if (this.refreshActiveLanguages())
|
|
2038
|
-
await this.rebuildSystemPromptInPlace();
|
|
2039
|
-
this.refreshSystemPromptTail();
|
|
1852
|
+
async runLoopInner(options = {}) {
|
|
1853
|
+
await this.prepareSystemPromptForRequest();
|
|
2040
1854
|
// One-shot cache-key marker per session so turn_end cacheRead numbers
|
|
2041
1855
|
// in the log can be traced back to a specific routing namespace —
|
|
2042
1856
|
// particularly useful when sub-agents inherit `parentKey:subagent`.
|
|
@@ -2135,11 +1949,14 @@ export class AgentSession {
|
|
|
2135
1949
|
// so the new value transparently yields a correctly-identified client.
|
|
2136
1950
|
let userAgent = this.provider === "anthropic" ? await getClaudeCliUserAgent() : undefined;
|
|
2137
1951
|
const loopMessages = await this.prepareDynamicContext();
|
|
1952
|
+
// Re-read every run so rule-file edits apply on the next turn; a few small files.
|
|
1953
|
+
const streamRules = await loadStreamRules(this.cwd);
|
|
2138
1954
|
const runAgentLoop = async (apiKey, accountId, projectId) => {
|
|
2139
1955
|
lastResolvedAccessToken = apiKey;
|
|
2140
1956
|
const modelInfo = getModel(this.model);
|
|
2141
1957
|
const effectiveBaseUrl = this.baseUrl ?? creds.baseUrl;
|
|
2142
1958
|
const generator = agentLoop(loopMessages, {
|
|
1959
|
+
...(streamRules.length > 0 ? { streamRules: { rules: streamRules } } : {}),
|
|
2143
1960
|
provider: this.provider,
|
|
2144
1961
|
model: this.model,
|
|
2145
1962
|
tools: options.disableTools ? [] : this.tools,
|
|
@@ -2187,6 +2004,35 @@ export class AgentSession {
|
|
|
2187
2004
|
// + pre-warm before the first turn. "baseline": current 5-min default.
|
|
2188
2005
|
cacheRetention: this.isSpeedOptimized() ? "long" : "short",
|
|
2189
2006
|
promptCacheKey: this.getPromptCacheKey(),
|
|
2007
|
+
responsesLite: resolveResponsesLite(this.codexShapeSetting("codexResponsesLite"), this.provider, this.model),
|
|
2008
|
+
strictTools: resolveStrictTools(this.codexShapeSetting("codexStrictTools"), this.provider),
|
|
2009
|
+
onContextPrepared: (context) => {
|
|
2010
|
+
const report = this.cacheDiagnostics.prepare(context, {
|
|
2011
|
+
provider: this.provider,
|
|
2012
|
+
model: this.model,
|
|
2013
|
+
at: Date.now(),
|
|
2014
|
+
cacheRetention: this.isSpeedOptimized() ? "long" : "short",
|
|
2015
|
+
route: { baseUrl: effectiveBaseUrl, accountId: this.lastAccountId ?? accountId },
|
|
2016
|
+
settings: {
|
|
2017
|
+
thinking: this.planModeRef.current || options.capThinking
|
|
2018
|
+
? clampThinkingForPlanMode(this.thinkingLevel)
|
|
2019
|
+
: this.thinkingLevel,
|
|
2020
|
+
webSearch: !options.disableTools,
|
|
2021
|
+
supportsImages: modelInfo?.supportsImages,
|
|
2022
|
+
promptCacheKey: this.getPromptCacheKey(),
|
|
2023
|
+
},
|
|
2024
|
+
});
|
|
2025
|
+
log("INFO", "cache", "Prepared context", {
|
|
2026
|
+
sessionId: this.sessionId || this.transportSessionId,
|
|
2027
|
+
data: JSON.stringify(report),
|
|
2028
|
+
});
|
|
2029
|
+
if (report.thinkingPrefixRiskBlocks > 0) {
|
|
2030
|
+
log("WARN", "cache", "Possible signed-thinking prefix mismatch; not server verified", {
|
|
2031
|
+
sessionId: this.sessionId || this.transportSessionId,
|
|
2032
|
+
blocks: String(report.thinkingPrefixRiskBlocks),
|
|
2033
|
+
});
|
|
2034
|
+
}
|
|
2035
|
+
},
|
|
2190
2036
|
supportsImages: modelInfo?.supportsImages,
|
|
2191
2037
|
supportsVideo: modelInfo?.supportsVideo,
|
|
2192
2038
|
userAgent,
|
|
@@ -2195,9 +2041,15 @@ export class AgentSession {
|
|
|
2195
2041
|
maxToolResultChars: resolveSessionToolResultCharLimit(this.model, this.provider, accountId),
|
|
2196
2042
|
// Aggregate per-turn budget across parallel tool results (fan-out guard).
|
|
2197
2043
|
maxTurnToolResultChars: resolveSessionTurnToolResultCharLimit(this.model, this.provider, accountId),
|
|
2044
|
+
// Warn when web/MCP output contains instruction-like text (see injection-detect.ts).
|
|
2045
|
+
transformToolResult: flagUntrustedToolResult,
|
|
2198
2046
|
// Self-correction hooks (same as the TUI): loop-break + re-grounding are
|
|
2199
2047
|
// polled mid-loop; the ideal review is polled when the agent would stop.
|
|
2200
2048
|
getSteeringMessages: () => this.getHookSteeringMessages(),
|
|
2049
|
+
onSteeringAvailable: (listener) => {
|
|
2050
|
+
this.steeringListeners.add(listener);
|
|
2051
|
+
return () => this.steeringListeners.delete(listener);
|
|
2052
|
+
},
|
|
2201
2053
|
getFollowUpMessages: () => this.getHookFollowUpMessages(),
|
|
2202
2054
|
onTurnBudgetExhausted: (ctx) => this.shouldExtendTurnBudget(ctx),
|
|
2203
2055
|
// Check authoritative provider usage before every model/tool step.
|
|
@@ -2223,6 +2075,7 @@ export class AgentSession {
|
|
|
2223
2075
|
// retained usage afterwards since it counted the pruned content.
|
|
2224
2076
|
const pruneResult = pruneStaleToolResults(messages);
|
|
2225
2077
|
if (pruneResult.pruned) {
|
|
2078
|
+
this.cacheDiagnostics.noteEdit("tool_prune", pruneResult.freedTokens);
|
|
2226
2079
|
this.providerContext = null;
|
|
2227
2080
|
log("INFO", "compaction", "Pruned stale tool outputs", {
|
|
2228
2081
|
prunedResults: String(pruneResult.prunedResults),
|
|
@@ -2263,6 +2116,9 @@ export class AgentSession {
|
|
|
2263
2116
|
usage,
|
|
2264
2117
|
pendingMessages,
|
|
2265
2118
|
});
|
|
2119
|
+
// An approved plan runs as ONE run, so step boundaries are only
|
|
2120
|
+
// visible here, between model steps — not after the run ends.
|
|
2121
|
+
const planStep = this.observePlanStepProgress(messages, contextWindow, activeTokens);
|
|
2266
2122
|
log("INFO", "compaction", "In-flight compaction decision", {
|
|
2267
2123
|
provider: this.provider,
|
|
2268
2124
|
model: this.model,
|
|
@@ -2270,8 +2126,10 @@ export class AgentSession {
|
|
|
2270
2126
|
contextWindow: String(contextWindow),
|
|
2271
2127
|
activeTokens: String(activeTokens),
|
|
2272
2128
|
triggerLimit: String(policy.targetTokens),
|
|
2129
|
+
...(planStep ? { planStep: `${planStep.compact} (${planStep.reason})` } : {}),
|
|
2273
2130
|
});
|
|
2274
|
-
if (!shouldCompact(messages, contextWindow, policy.threshold, activeTokens, policy.targetTokens)
|
|
2131
|
+
if (!shouldCompact(messages, contextWindow, policy.threshold, activeTokens, policy.targetTokens) &&
|
|
2132
|
+
!planStep?.compact)
|
|
2275
2133
|
return messages;
|
|
2276
2134
|
}
|
|
2277
2135
|
// compact() operates on this.messages, while an earlier transform may
|
|
@@ -2604,7 +2462,7 @@ export class AgentSession {
|
|
|
2604
2462
|
const canonicalPath = await this.sessionManager.resolveCanonicalSession(this.conversationId, this.cwd);
|
|
2605
2463
|
if (!canonicalPath || canonicalPath === this.sessionPath)
|
|
2606
2464
|
return;
|
|
2607
|
-
await this.adoptCompactionCheckpoint(await this.sessionManager.load(canonicalPath));
|
|
2465
|
+
await this.adoptCompactionCheckpoint(await this.sessionManager.load(canonicalPath, { canonical: true }));
|
|
2608
2466
|
}
|
|
2609
2467
|
async persistCompactionCheckpoint(sourceFingerprint, result) {
|
|
2610
2468
|
const parentSessionId = this.sessionId || undefined;
|
|
@@ -2653,6 +2511,99 @@ export class AgentSession {
|
|
|
2653
2511
|
if (this.postTurnCompaction)
|
|
2654
2512
|
await this.postTurnCompaction;
|
|
2655
2513
|
}
|
|
2514
|
+
/**
|
|
2515
|
+
* Advance plan-step bookkeeping over the messages added since the last
|
|
2516
|
+
* observation and, when a plan step was newly completed (`[DONE:n]`), run
|
|
2517
|
+
* the SoL-Pi cost rule. Called between model steps (in-flight) and once
|
|
2518
|
+
* after the run; `scanIndex` and `doneSteps` make each message and each
|
|
2519
|
+
* step count once across both paths. Returns undefined when no step
|
|
2520
|
+
* completed since the last observation.
|
|
2521
|
+
*/
|
|
2522
|
+
observePlanStepProgress(messages, contextWindow, activeTokens) {
|
|
2523
|
+
const st = this.planStepState;
|
|
2524
|
+
const planPath = this.approvedPlanPath;
|
|
2525
|
+
if (st.planPath !== planPath) {
|
|
2526
|
+
st.planPath = planPath;
|
|
2527
|
+
st.doneSteps = new Set();
|
|
2528
|
+
st.requestsInCompletedSteps = 0;
|
|
2529
|
+
st.requestsThisStep = 0;
|
|
2530
|
+
}
|
|
2531
|
+
if (st.scanIndex > messages.length)
|
|
2532
|
+
st.scanIndex = 0;
|
|
2533
|
+
let requests = 0;
|
|
2534
|
+
const newlyDone = [];
|
|
2535
|
+
for (let i = st.scanIndex; i < messages.length; i++) {
|
|
2536
|
+
const msg = messages[i];
|
|
2537
|
+
if (msg?.role !== "assistant")
|
|
2538
|
+
continue;
|
|
2539
|
+
requests++;
|
|
2540
|
+
const text = typeof msg.content === "string"
|
|
2541
|
+
? msg.content
|
|
2542
|
+
: msg.content.map((part) => (part.type === "text" ? part.text : "")).join("\n");
|
|
2543
|
+
for (const step of findCompletedMarkers(text)) {
|
|
2544
|
+
if (!st.doneSteps.has(step))
|
|
2545
|
+
newlyDone.push(step);
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
st.scanIndex = messages.length;
|
|
2549
|
+
const contextTokens = activeTokens ?? estimateConversationTokens(messages);
|
|
2550
|
+
if (st.lastContextTokens > 0)
|
|
2551
|
+
st.grownTokens += Math.max(0, contextTokens - st.lastContextTokens);
|
|
2552
|
+
st.lastContextTokens = contextTokens;
|
|
2553
|
+
st.requests += requests;
|
|
2554
|
+
st.requestsThisStep += requests;
|
|
2555
|
+
st.requestsSinceLastCompaction += requests;
|
|
2556
|
+
if (st.writeCostBalance > 0)
|
|
2557
|
+
st.writeCostBalance -= st.savingPerRequest * requests;
|
|
2558
|
+
if (!planPath || newlyDone.length === 0)
|
|
2559
|
+
return undefined;
|
|
2560
|
+
for (const step of newlyDone)
|
|
2561
|
+
st.doneSteps.add(step);
|
|
2562
|
+
st.requestsInCompletedSteps += st.requestsThisStep;
|
|
2563
|
+
st.requestsThisStep = 0;
|
|
2564
|
+
let planText;
|
|
2565
|
+
try {
|
|
2566
|
+
planText = readFileSync(planPath, "utf8");
|
|
2567
|
+
}
|
|
2568
|
+
catch {
|
|
2569
|
+
return undefined;
|
|
2570
|
+
}
|
|
2571
|
+
const totalSteps = extractPlanSteps(planText).length;
|
|
2572
|
+
const decision = decidePlanStepCompaction({
|
|
2573
|
+
contextTokens,
|
|
2574
|
+
keptTailTokens: PLAN_STEP_KEEP_TOKENS,
|
|
2575
|
+
contextWindow,
|
|
2576
|
+
// The model registry carries no cache price fields; use the SoL-Pi default.
|
|
2577
|
+
cacheWriteReadRatio: DEFAULT_CACHE_WRITE_READ_RATIO,
|
|
2578
|
+
stepsCompleted: st.doneSteps.size,
|
|
2579
|
+
stepsRemaining: Math.max(0, totalSteps - st.doneSteps.size),
|
|
2580
|
+
requestsInCompletedSteps: st.requestsInCompletedSteps,
|
|
2581
|
+
requests: st.requests,
|
|
2582
|
+
grownTokens: st.grownTokens,
|
|
2583
|
+
priorCompactions: st.compactions,
|
|
2584
|
+
writeCostBalance: st.writeCostBalance,
|
|
2585
|
+
requestsSinceLastCompaction: st.compactions > 0 ? st.requestsSinceLastCompaction : undefined,
|
|
2586
|
+
});
|
|
2587
|
+
log("INFO", "compaction", "Plan-step compaction decision", {
|
|
2588
|
+
compact: String(decision.compact),
|
|
2589
|
+
reason: decision.reason,
|
|
2590
|
+
});
|
|
2591
|
+
return decision;
|
|
2592
|
+
}
|
|
2593
|
+
/**
|
|
2594
|
+
* Plan-step bookkeeping after ANY successful compaction (as in the bench:
|
|
2595
|
+
* every compaction leaves a cache-write cost to be repaid by later savings).
|
|
2596
|
+
*/
|
|
2597
|
+
recordPlanStepCompaction(contextBefore) {
|
|
2598
|
+
const st = this.planStepState;
|
|
2599
|
+
const after = estimateConversationTokens(this.messages);
|
|
2600
|
+
st.scanIndex = this.messages.length;
|
|
2601
|
+
st.lastContextTokens = after;
|
|
2602
|
+
st.requestsSinceLastCompaction = 0;
|
|
2603
|
+
st.compactions++;
|
|
2604
|
+
st.writeCostBalance += after * (DEFAULT_CACHE_WRITE_READ_RATIO - 1);
|
|
2605
|
+
st.savingPerRequest = Math.max(0, contextBefore - after);
|
|
2606
|
+
}
|
|
2656
2607
|
/**
|
|
2657
2608
|
* Post-turn compaction: once the final response has been delivered, compact
|
|
2658
2609
|
* in the background while the user reads the answer, instead of making the
|
|
@@ -2661,7 +2612,9 @@ export class AgentSession {
|
|
|
2661
2612
|
* Codex `model_post_turn_compact_threshold_percent` guards: skip when user
|
|
2662
2613
|
* input is already queued (it would race the next turn), when the run was
|
|
2663
2614
|
* aborted, or during the failure cooldown — and never let a compaction
|
|
2664
|
-
* error surface in the completed turn.
|
|
2615
|
+
* error surface in the completed turn. Besides the size trigger, a newly
|
|
2616
|
+
* completed approved-plan step may compact when the cache-cost rule in
|
|
2617
|
+
* compaction/plan-step-policy.ts says the shrink pays for itself.
|
|
2665
2618
|
*/
|
|
2666
2619
|
maybeCompactPostTurn(creds) {
|
|
2667
2620
|
if (!this.settingsManager.get("autoCompact"))
|
|
@@ -2674,15 +2627,18 @@ export class AgentSession {
|
|
|
2674
2627
|
return;
|
|
2675
2628
|
if (Date.now() < this.compactionRetryAfter)
|
|
2676
2629
|
return;
|
|
2677
|
-
// One compaction per turn boundary: a pre-run or overflow-recovery
|
|
2678
|
-
// compaction already shrank this run's history — re-probing right after
|
|
2679
|
-
// the final response would only re-derive that decision.
|
|
2680
|
-
if (this.compactionOccurred)
|
|
2681
|
-
return;
|
|
2682
2630
|
const contextWindow = getContextWindow(this.model, {
|
|
2683
2631
|
provider: this.provider,
|
|
2684
2632
|
accountId: creds.accountId,
|
|
2685
2633
|
});
|
|
2634
|
+
// One compaction per turn boundary: a pre-run, in-flight or overflow
|
|
2635
|
+
// compaction already shrank this run's history — re-probing right after
|
|
2636
|
+
// the final response would only re-derive that decision. Still record the
|
|
2637
|
+
// final response's `[DONE:n]` steps so plan bookkeeping stays current.
|
|
2638
|
+
if (this.compactionOccurred) {
|
|
2639
|
+
this.observePlanStepProgress(this.messages, contextWindow, undefined);
|
|
2640
|
+
return;
|
|
2641
|
+
}
|
|
2686
2642
|
const policy = resolveCompactionPolicy({
|
|
2687
2643
|
provider: this.provider,
|
|
2688
2644
|
model: this.model,
|
|
@@ -2701,9 +2657,12 @@ export class AgentSession {
|
|
|
2701
2657
|
});
|
|
2702
2658
|
}
|
|
2703
2659
|
}
|
|
2704
|
-
|
|
2660
|
+
const planStep = this.observePlanStepProgress(this.messages, contextWindow, activeTokens);
|
|
2661
|
+
if (!shouldCompact(this.messages, contextWindow, policy.threshold, activeTokens, policy.targetTokens) &&
|
|
2662
|
+
!planStep?.compact)
|
|
2705
2663
|
return;
|
|
2706
2664
|
log("INFO", "compaction", "Post-turn compaction decision — compacting in background", {
|
|
2665
|
+
trigger: planStep?.compact ? `plan-step (${planStep.reason})` : "size",
|
|
2707
2666
|
provider: this.provider,
|
|
2708
2667
|
model: this.model,
|
|
2709
2668
|
transport: this.provider === "openai" && creds.accountId ? "codex_oauth" : "public_api",
|
|
@@ -2757,6 +2716,7 @@ export class AgentSession {
|
|
|
2757
2716
|
approvedPlanPath: this.approvedPlanPath,
|
|
2758
2717
|
});
|
|
2759
2718
|
const originalCount = this.messages.length;
|
|
2719
|
+
const contextTokensBefore = estimateConversationTokens(this.messages);
|
|
2760
2720
|
this.eventBus.emit("compaction_start", { messageCount: originalCount });
|
|
2761
2721
|
let contextSelection;
|
|
2762
2722
|
const runCompactor = async () => {
|
|
@@ -2794,7 +2754,7 @@ export class AgentSession {
|
|
|
2794
2754
|
let sourceFingerprint = computeSourceFingerprint(this.messages);
|
|
2795
2755
|
const canonicalPath = await this.sessionManager.resolveCanonicalSession(conversationId, this.cwd);
|
|
2796
2756
|
if (canonicalPath && canonicalPath !== this.sessionPath) {
|
|
2797
|
-
const newest = await this.sessionManager.load(canonicalPath);
|
|
2757
|
+
const newest = await this.sessionManager.load(canonicalPath, { canonical: true });
|
|
2798
2758
|
if (newest.header.sourceFingerprint === sourceFingerprint) {
|
|
2799
2759
|
await this.adoptCompactionCheckpoint(newest);
|
|
2800
2760
|
this.lastCompactionCompacted = true;
|
|
@@ -2868,6 +2828,10 @@ export class AgentSession {
|
|
|
2868
2828
|
}
|
|
2869
2829
|
});
|
|
2870
2830
|
}
|
|
2831
|
+
if (this.lastCompactionCompacted) {
|
|
2832
|
+
this.cacheDiagnostics.noteEdit("compaction");
|
|
2833
|
+
this.recordPlanStepCompaction(contextTokensBefore);
|
|
2834
|
+
}
|
|
2871
2835
|
this.eventBus.emit("compaction_end", {
|
|
2872
2836
|
compacted: this.lastCompactionCompacted,
|
|
2873
2837
|
originalCount,
|
|
@@ -2887,6 +2851,7 @@ export class AgentSession {
|
|
|
2887
2851
|
});
|
|
2888
2852
|
}
|
|
2889
2853
|
async newSession(preserveConversation = false) {
|
|
2854
|
+
this.cacheDiagnostics.reset();
|
|
2890
2855
|
// Approved-plan execution is a clean checkpoint of the same conversation;
|
|
2891
2856
|
// explicit new sessions reset the conversation identity.
|
|
2892
2857
|
if (!preserveConversation) {
|
|
@@ -2934,6 +2899,7 @@ export class AgentSession {
|
|
|
2934
2899
|
}
|
|
2935
2900
|
async loadSession(sessionPath) {
|
|
2936
2901
|
await this.loadExistingSession(sessionPath);
|
|
2902
|
+
this.cacheDiagnostics.reset();
|
|
2937
2903
|
if (this.sessionId)
|
|
2938
2904
|
await this.subAgentManager?.hydrate(this.sessionId);
|
|
2939
2905
|
this.eventBus.emit("session_start", { sessionId: this.sessionId });
|
|
@@ -2964,6 +2930,7 @@ export class AgentSession {
|
|
|
2964
2930
|
const branchMessages = this.sessionManager.getMessages(loaded.entries, this.currentLeafId);
|
|
2965
2931
|
const systemMsg = this.messages[0];
|
|
2966
2932
|
this.messages = [systemMsg, ...branchMessages];
|
|
2933
|
+
this.cacheDiagnostics.reset();
|
|
2967
2934
|
this.lastPersistedIndex = this.messages.length;
|
|
2968
2935
|
// Reads made in the dropped messages are no longer in the model's context.
|
|
2969
2936
|
this.clearReadTracker?.();
|
|
@@ -3031,23 +2998,33 @@ export class AgentSession {
|
|
|
3031
2998
|
: undefined;
|
|
3032
2999
|
return costUsd === undefined ? { used, size } : { used, size, costUsd };
|
|
3033
3000
|
}
|
|
3034
|
-
getPlanMode() {
|
|
3035
|
-
return this.planModeRef.current;
|
|
3036
|
-
}
|
|
3037
3001
|
/**
|
|
3038
|
-
*
|
|
3039
|
-
*
|
|
3040
|
-
*
|
|
3002
|
+
* Whether the provider's prompt cache has likely lapsed since the last
|
|
3003
|
+
* successful request, and how many tokens the next message would re-read at
|
|
3004
|
+
* full price. Null when the route has no known TTL or the chat is empty.
|
|
3041
3005
|
*/
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3006
|
+
getCacheExpiryStatus(now = Date.now()) {
|
|
3007
|
+
const status = assessCacheExpiry({
|
|
3008
|
+
current: {
|
|
3009
|
+
provider: this.provider,
|
|
3010
|
+
model: this.model,
|
|
3011
|
+
policy: resolveCacheTtl({
|
|
3012
|
+
provider: this.provider,
|
|
3013
|
+
model: this.model,
|
|
3014
|
+
cacheRetention: this.isSpeedOptimized() ? "long" : "short",
|
|
3015
|
+
baseUrl: this.baseUrl,
|
|
3016
|
+
accountId: this.lastAccountId,
|
|
3017
|
+
}),
|
|
3018
|
+
},
|
|
3019
|
+
lastTouch: this.cacheDiagnostics.lastCacheTouch(),
|
|
3020
|
+
now,
|
|
3021
|
+
prefixTokens: this.getContextUsage().used,
|
|
3022
|
+
hasHistory: this.messages.some((m) => m.role === "user"),
|
|
3023
|
+
});
|
|
3024
|
+
return status && { ...status, sessionId: this.sessionId || this.transportSessionId };
|
|
3025
|
+
}
|
|
3026
|
+
getPlanMode() {
|
|
3027
|
+
return this.planModeRef.current;
|
|
3051
3028
|
}
|
|
3052
3029
|
/** Queue a user message (optionally with attachments) to be injected mid-run
|
|
3053
3030
|
* as steering. Returns the new queue length. No-op semantics are the caller's
|
|
@@ -3055,6 +3032,9 @@ export class AgentSession {
|
|
|
3055
3032
|
queueMessage(text, attachments = []) {
|
|
3056
3033
|
this.queueSeq += 1;
|
|
3057
3034
|
this.userQueue.push({ id: `q${this.queueSeq}`, text, attachments });
|
|
3035
|
+
// Instant interrupt: preempt running tools so the steer lands right away.
|
|
3036
|
+
for (const listener of [...this.steeringListeners])
|
|
3037
|
+
listener();
|
|
3058
3038
|
return this.userQueue.length;
|
|
3059
3039
|
}
|
|
3060
3040
|
/** Pending queued messages (id + text), oldest first, for client display. */
|
|
@@ -3107,6 +3087,15 @@ export class AgentSession {
|
|
|
3107
3087
|
return `No background process with id "${id}"`;
|
|
3108
3088
|
return this.processManager.stop(id);
|
|
3109
3089
|
}
|
|
3090
|
+
/**
|
|
3091
|
+
* Force-stop every background process tree, synchronously. Background
|
|
3092
|
+
* commands run in their own process group, so the daemon's group kill on
|
|
3093
|
+
* quit never reaches them: this is the only thing that does. Callers on a
|
|
3094
|
+
* shutdown deadline run it before awaiting anything that can hang.
|
|
3095
|
+
*/
|
|
3096
|
+
stopBackgroundProcesses() {
|
|
3097
|
+
this.processManager?.shutdownAll();
|
|
3098
|
+
}
|
|
3110
3099
|
/** Replace a host-owned system prompt in place without resetting conversation history. */
|
|
3111
3100
|
setCustomSystemPrompt(systemPrompt, promptCacheKeyPrefix) {
|
|
3112
3101
|
this.customSystemPrompt = systemPrompt;
|
|
@@ -3253,6 +3242,18 @@ export class AgentSession {
|
|
|
3253
3242
|
* the standard prompt therefore needs a rebuild. Custom and sub-agent
|
|
3254
3243
|
* prompts never render packs, so detection is skipped for them.
|
|
3255
3244
|
*/
|
|
3245
|
+
/**
|
|
3246
|
+
* Bring the system prompt to the exact state the next provider request will
|
|
3247
|
+
* send. Shared by real runs and {@link prewarm} so both see one prefix.
|
|
3248
|
+
*/
|
|
3249
|
+
async prepareSystemPromptForRequest() {
|
|
3250
|
+
// Languages are re-detected at each task boundary so a project scaffolded
|
|
3251
|
+
// during the previous turn gets its packs; the prompt is rebuilt only when
|
|
3252
|
+
// the set grows, keeping the cached prefix stable otherwise.
|
|
3253
|
+
if (this.refreshActiveLanguages())
|
|
3254
|
+
await this.rebuildSystemPromptInPlace();
|
|
3255
|
+
this.refreshSystemPromptTail();
|
|
3256
|
+
}
|
|
3256
3257
|
refreshActiveLanguages() {
|
|
3257
3258
|
if (this.customSystemPrompt || this.agentPrompt !== undefined)
|
|
3258
3259
|
return false;
|
|
@@ -3351,6 +3352,17 @@ export class AgentSession {
|
|
|
3351
3352
|
}));
|
|
3352
3353
|
}
|
|
3353
3354
|
async persistTurnMetric(event) {
|
|
3355
|
+
if (event.stopReason === "error")
|
|
3356
|
+
this.cacheDiagnostics.discardAttempt();
|
|
3357
|
+
const cache = this.cacheDiagnostics.complete(event.usage, event.timing);
|
|
3358
|
+
if (cache) {
|
|
3359
|
+
log("INFO", "cache", "Context cache outcome", {
|
|
3360
|
+
sessionId: this.sessionId || this.transportSessionId,
|
|
3361
|
+
provider: this.provider,
|
|
3362
|
+
model: this.model,
|
|
3363
|
+
data: JSON.stringify(cache),
|
|
3364
|
+
});
|
|
3365
|
+
}
|
|
3354
3366
|
const payload = {
|
|
3355
3367
|
version: 1,
|
|
3356
3368
|
turn: event.turn,
|
|
@@ -3736,6 +3748,141 @@ export class AgentSession {
|
|
|
3736
3748
|
if (signal?.aborted)
|
|
3737
3749
|
this.managerAbortHandler();
|
|
3738
3750
|
}
|
|
3751
|
+
runLoopDepth = 0;
|
|
3752
|
+
lastRealRequestAt = 0;
|
|
3753
|
+
lastPrewarmAt = 0;
|
|
3754
|
+
prewarmController = null;
|
|
3755
|
+
/**
|
|
3756
|
+
* Best-effort Anthropic prompt-cache prewarm before the user's next turn
|
|
3757
|
+
* (desktop app calls this on the first keystroke after opening a chat or an
|
|
3758
|
+
* idle pause). Sends the exact request prefix the next real turn will use —
|
|
3759
|
+
* same system/tools/thinking/cache options — with `max_tokens: 1`, so the
|
|
3760
|
+
* first real reply is a cache read instead of a cold write.
|
|
3761
|
+
*/
|
|
3762
|
+
async prewarm(signal) {
|
|
3763
|
+
if (this.provider !== "anthropic")
|
|
3764
|
+
return { ok: false, reason: "provider" };
|
|
3765
|
+
if (this.settingsManager?.get("cachePrewarm") === false) {
|
|
3766
|
+
return { ok: false, reason: "disabled" };
|
|
3767
|
+
}
|
|
3768
|
+
if (this.runLoopDepth > 0)
|
|
3769
|
+
return { ok: false, reason: "run_active" };
|
|
3770
|
+
if (this.prewarmController)
|
|
3771
|
+
return { ok: false, reason: "in_flight" };
|
|
3772
|
+
const cacheRetention = this.isSpeedOptimized() ? "long" : "short";
|
|
3773
|
+
const ttlMs = (cacheRetention === "long" ? 60 : 5) * 60_000;
|
|
3774
|
+
const now = Date.now();
|
|
3775
|
+
if (now - Math.max(this.lastPrewarmAt, this.lastRealRequestAt) < ttlMs) {
|
|
3776
|
+
return { ok: false, reason: "cache_fresh" };
|
|
3777
|
+
}
|
|
3778
|
+
// Same preparation as the real run, BEFORE copying history: otherwise the
|
|
3779
|
+
// warmed system block differs from the next request (language packs are
|
|
3780
|
+
// detected at run start) and everything after it misses the cache.
|
|
3781
|
+
await this.prepareSystemPromptForRequest();
|
|
3782
|
+
// The await above yields: re-check that no run or other prewarm began.
|
|
3783
|
+
if (this.runLoopDepth > 0)
|
|
3784
|
+
return { ok: false, reason: "run_active" };
|
|
3785
|
+
if (this.prewarmController)
|
|
3786
|
+
return { ok: false, reason: "in_flight" };
|
|
3787
|
+
// End at the last user/tool message: the next real turn appends a new user
|
|
3788
|
+
// message after the trailing assistant reply, and its cache lookback hits
|
|
3789
|
+
// the entry written at this boundary. A request must end on a user turn.
|
|
3790
|
+
let end = this.messages.length;
|
|
3791
|
+
while (end > 0 && this.messages[end - 1]?.role === "assistant")
|
|
3792
|
+
end--;
|
|
3793
|
+
// The loop repairs tool pairing in place before every request; apply the
|
|
3794
|
+
// same repair to a copy so the warmed prefix is byte-identical to it.
|
|
3795
|
+
const messages = structuredClone(this.messages.slice(0, end));
|
|
3796
|
+
repairToolPairingAdjacent(messages);
|
|
3797
|
+
if (!messages.some((m) => m.role === "user" || m.role === "tool")) {
|
|
3798
|
+
return { ok: false, reason: "no_history" };
|
|
3799
|
+
}
|
|
3800
|
+
const tokens = estimateConversationTokens(messages);
|
|
3801
|
+
if (tokens < 4_000)
|
|
3802
|
+
return { ok: false, reason: "too_small" };
|
|
3803
|
+
const controller = new AbortController();
|
|
3804
|
+
const onAbort = () => controller.abort();
|
|
3805
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
3806
|
+
this.prewarmController = controller;
|
|
3807
|
+
const started = Date.now();
|
|
3808
|
+
try {
|
|
3809
|
+
const creds = await this.authStorage.resolveCredentials(this.provider, {
|
|
3810
|
+
storageKeys: this.currentAuthStorageKeys(),
|
|
3811
|
+
});
|
|
3812
|
+
if (controller.signal.aborted || this.runLoopDepth > 0) {
|
|
3813
|
+
return { ok: false, reason: "aborted" };
|
|
3814
|
+
}
|
|
3815
|
+
const modelInfo = getModel(this.model);
|
|
3816
|
+
const result = stream({
|
|
3817
|
+
provider: this.provider,
|
|
3818
|
+
model: this.model,
|
|
3819
|
+
messages,
|
|
3820
|
+
tools: this.tools,
|
|
3821
|
+
webSearch: true,
|
|
3822
|
+
maxTokens: this.maxTokens,
|
|
3823
|
+
thinking: this.planModeRef.current
|
|
3824
|
+
? clampThinkingForPlanMode(this.thinkingLevel)
|
|
3825
|
+
: this.thinkingLevel,
|
|
3826
|
+
apiKey: creds.accessToken,
|
|
3827
|
+
baseUrl: this.baseUrl ?? creds.baseUrl,
|
|
3828
|
+
accountId: creds.accountId,
|
|
3829
|
+
transportSessionId: this.sessionId || this.transportSessionId,
|
|
3830
|
+
cacheRetention,
|
|
3831
|
+
promptCacheKey: this.getPromptCacheKey(),
|
|
3832
|
+
supportsImages: modelInfo?.supportsImages,
|
|
3833
|
+
supportsVideo: modelInfo?.supportsVideo,
|
|
3834
|
+
userAgent: await getClaudeCliUserAgent(),
|
|
3835
|
+
prewarm: true,
|
|
3836
|
+
signal: controller.signal,
|
|
3837
|
+
});
|
|
3838
|
+
const response = await result.response;
|
|
3839
|
+
const usage = response.usage;
|
|
3840
|
+
if (usage.inputTokens === 0 && usage.outputTokens === 0) {
|
|
3841
|
+
// @prestyj/ai sent nothing: budget thinking can't stay identical at max_tokens 1.
|
|
3842
|
+
log("INFO", "prewarm", "Cache prewarm skipped: budget thinking", {
|
|
3843
|
+
model: this.model,
|
|
3844
|
+
});
|
|
3845
|
+
return { ok: false, reason: "thinking_budget_incompatible" };
|
|
3846
|
+
}
|
|
3847
|
+
this.lastPrewarmAt = Date.now();
|
|
3848
|
+
const warmedPolicy = resolveCacheTtl({
|
|
3849
|
+
provider: this.provider,
|
|
3850
|
+
model: this.model,
|
|
3851
|
+
cacheRetention,
|
|
3852
|
+
baseUrl: this.baseUrl ?? creds.baseUrl,
|
|
3853
|
+
accountId: creds.accountId,
|
|
3854
|
+
});
|
|
3855
|
+
if (warmedPolicy) {
|
|
3856
|
+
this.cacheDiagnostics.noteCacheTouch({
|
|
3857
|
+
at: started,
|
|
3858
|
+
provider: this.provider,
|
|
3859
|
+
model: this.model,
|
|
3860
|
+
policy: warmedPolicy,
|
|
3861
|
+
});
|
|
3862
|
+
}
|
|
3863
|
+
log("INFO", "prewarm", "Cache prewarm complete", {
|
|
3864
|
+
tokens: String(tokens),
|
|
3865
|
+
cacheRead: String(usage.cacheRead ?? 0),
|
|
3866
|
+
cacheWrite: String(usage.cacheWrite ?? 0),
|
|
3867
|
+
ms: String(Date.now() - started),
|
|
3868
|
+
});
|
|
3869
|
+
return { ok: true, reason: "warmed", usage };
|
|
3870
|
+
}
|
|
3871
|
+
catch (error) {
|
|
3872
|
+
if (isAbortError(error) || controller.signal.aborted)
|
|
3873
|
+
return { ok: false, reason: "aborted" };
|
|
3874
|
+
log("WARN", "prewarm", `Cache prewarm failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3875
|
+
return { ok: false, reason: "error" };
|
|
3876
|
+
}
|
|
3877
|
+
finally {
|
|
3878
|
+
signal?.removeEventListener("abort", onAbort);
|
|
3879
|
+
if (this.prewarmController === controller)
|
|
3880
|
+
this.prewarmController = null;
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
codexShapeSetting(key) {
|
|
3884
|
+
return this.settingsManager?.get(key) ?? "auto";
|
|
3885
|
+
}
|
|
3739
3886
|
/** True when speedProfile is "optimized" (1-h cache TTL + pre-warm), or the
|
|
3740
3887
|
* session was constructed with `forceLongCacheRetention` (Nolan sessions). */
|
|
3741
3888
|
isSpeedOptimized() {
|
|
@@ -3764,16 +3911,22 @@ export class AgentSession {
|
|
|
3764
3911
|
return this.getPromptCacheKey();
|
|
3765
3912
|
}
|
|
3766
3913
|
async dispose() {
|
|
3914
|
+
// First and synchronous: nothing below may delay this, or a hung teardown
|
|
3915
|
+
// step leaves background commands running after the app has quit.
|
|
3916
|
+
this.stopBackgroundProcesses();
|
|
3767
3917
|
// Quiesce any in-flight post-turn compaction BEFORE tearing down state:
|
|
3768
3918
|
// the background compact() snapshots and replaces `this.messages`, so
|
|
3769
3919
|
// letting it run past this point would checkpoint a near-empty history
|
|
3770
3920
|
// and leak a junk session file after teardown.
|
|
3771
3921
|
if (this.postTurnCompaction)
|
|
3772
3922
|
await this.postTurnCompaction;
|
|
3923
|
+
this.cacheDiagnostics.reset();
|
|
3773
3924
|
this.diagnosticsRecorder?.finalize();
|
|
3774
3925
|
this.managerAbortSignal?.removeEventListener("abort", this.managerAbortHandler);
|
|
3775
|
-
|
|
3926
|
+
// Again, in case a turn racing teardown started one while we awaited.
|
|
3927
|
+
this.stopBackgroundProcesses();
|
|
3776
3928
|
this.lspManager?.shutdownAll();
|
|
3929
|
+
this.debugManager?.shutdown();
|
|
3777
3930
|
await Promise.all([this.subAgentManager?.shutdownAll(), this.mcpManager?.dispose()]);
|
|
3778
3931
|
await this.extensionLoader.deactivateAll();
|
|
3779
3932
|
this.setSessionPath("");
|
|
@@ -3809,8 +3962,10 @@ export class AgentSession {
|
|
|
3809
3962
|
// A stale physical checkpoint is only an address, not the conversation tip.
|
|
3810
3963
|
// Resolve every resume—not just over-threshold/deferred compaction resumes—
|
|
3811
3964
|
// before reading history so the next prompt cannot continue an old branch.
|
|
3812
|
-
const
|
|
3813
|
-
const loaded =
|
|
3965
|
+
const resolvedPath = await this.sessionManager.resolveCanonicalSession(sessionPath, this.cwd);
|
|
3966
|
+
const loaded = resolvedPath
|
|
3967
|
+
? await this.sessionManager.load(resolvedPath, { canonical: true })
|
|
3968
|
+
: await this.sessionManager.load(sessionPath);
|
|
3814
3969
|
// Use the leaf from the header to walk the correct branch
|
|
3815
3970
|
const loadedMessages = this.sessionManager.getMessages(loaded.entries, loaded.header.leafId);
|
|
3816
3971
|
const savedCompletionReview = [...loaded.entries]
|