@prestyj/cli 5.9.0 → 5.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -5
- package/assets/skills/evidence-led-ui/SKILL.md +1 -1
- package/dist/app-sidecar-auth-broadcast.test.d.ts +2 -0
- package/dist/app-sidecar-auth-broadcast.test.d.ts.map +1 -0
- package/dist/app-sidecar-auth-broadcast.test.js +282 -0
- package/dist/app-sidecar-auth-broadcast.test.js.map +1 -0
- package/dist/app-sidecar-sessions.d.ts +5 -2
- package/dist/app-sidecar-sessions.d.ts.map +1 -1
- package/dist/app-sidecar-sessions.js +34 -8
- package/dist/app-sidecar-sessions.js.map +1 -1
- package/dist/app-sidecar-sessions.test.js +95 -0
- package/dist/app-sidecar-sessions.test.js.map +1 -1
- package/dist/app-sidecar.js +871 -142
- package/dist/app-sidecar.js.map +1 -1
- package/dist/chat-agents/general.test.js +5 -3
- package/dist/chat-agents/general.test.js.map +1 -1
- package/dist/chat-agents/memory.d.ts.map +1 -1
- package/dist/chat-agents/memory.js +14 -5
- package/dist/chat-agents/memory.js.map +1 -1
- package/dist/chat-agents/memory.test.js +11 -1
- package/dist/chat-agents/memory.test.js.map +1 -1
- package/dist/chat-agents/shared.js +1 -1
- package/dist/chat-agents/shared.js.map +1 -1
- package/dist/chat-agents/specialists.test.js +3 -2
- package/dist/chat-agents/specialists.test.js.map +1 -1
- package/dist/cli/command-routing.d.ts +1 -1
- package/dist/cli/command-routing.d.ts.map +1 -1
- package/dist/cli/command-routing.js +1 -0
- package/dist/cli/command-routing.js.map +1 -1
- package/dist/cli/command-routing.test.js +1 -0
- package/dist/cli/command-routing.test.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +183 -51
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +16 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +51 -2
- package/dist/config.js.map +1 -1
- package/dist/config.test.js +71 -2
- package/dist/config.test.js.map +1 -1
- package/dist/core/agent-mcp-passthrough.test.d.ts +2 -0
- package/dist/core/agent-mcp-passthrough.test.d.ts.map +1 -0
- package/dist/core/agent-mcp-passthrough.test.js +61 -0
- package/dist/core/agent-mcp-passthrough.test.js.map +1 -0
- package/dist/core/agent-notifications.d.ts +62 -0
- package/dist/core/agent-notifications.d.ts.map +1 -0
- package/dist/core/agent-notifications.js +92 -0
- package/dist/core/agent-notifications.js.map +1 -0
- package/dist/core/agent-notifications.test.d.ts +2 -0
- package/dist/core/agent-notifications.test.d.ts.map +1 -0
- package/dist/core/agent-notifications.test.js +97 -0
- package/dist/core/agent-notifications.test.js.map +1 -0
- package/dist/core/agent-session-add-dir.test.d.ts +2 -0
- package/dist/core/agent-session-add-dir.test.d.ts.map +1 -0
- package/dist/core/agent-session-add-dir.test.js +110 -0
- package/dist/core/agent-session-add-dir.test.js.map +1 -0
- package/dist/core/agent-session-checkpoint-flush.test.d.ts +2 -0
- package/dist/core/agent-session-checkpoint-flush.test.d.ts.map +1 -0
- package/dist/core/agent-session-checkpoint-flush.test.js +201 -0
- package/dist/core/agent-session-checkpoint-flush.test.js.map +1 -0
- package/dist/core/agent-session-compaction.test.js +327 -13
- package/dist/core/agent-session-compaction.test.js.map +1 -1
- package/dist/core/agent-session-crash-durability.test.d.ts +2 -0
- package/dist/core/agent-session-crash-durability.test.d.ts.map +1 -0
- package/dist/core/agent-session-crash-durability.test.js +102 -0
- package/dist/core/agent-session-crash-durability.test.js.map +1 -0
- package/dist/core/agent-session-marker-anchors.test.d.ts +2 -0
- package/dist/core/agent-session-marker-anchors.test.d.ts.map +1 -0
- package/dist/core/agent-session-marker-anchors.test.js +326 -0
- package/dist/core/agent-session-marker-anchors.test.js.map +1 -0
- package/dist/core/agent-session-memory-tail.test.js +4 -7
- package/dist/core/agent-session-memory-tail.test.js.map +1 -1
- package/dist/core/agent-session-model-switch.test.d.ts +2 -0
- package/dist/core/agent-session-model-switch.test.d.ts.map +1 -0
- package/dist/core/agent-session-model-switch.test.js +161 -0
- package/dist/core/agent-session-model-switch.test.js.map +1 -0
- package/dist/core/agent-session-notifications.test.d.ts +2 -0
- package/dist/core/agent-session-notifications.test.d.ts.map +1 -0
- package/dist/core/agent-session-notifications.test.js +114 -0
- package/dist/core/agent-session-notifications.test.js.map +1 -0
- package/dist/core/agent-session-process-gate.test.d.ts +2 -0
- package/dist/core/agent-session-process-gate.test.d.ts.map +1 -0
- package/dist/core/agent-session-process-gate.test.js +92 -0
- package/dist/core/agent-session-process-gate.test.js.map +1 -0
- package/dist/core/agent-session-queue.test.js +69 -7
- package/dist/core/agent-session-queue.test.js.map +1 -1
- package/dist/core/agent-session-review-coverage.test.js +80 -23
- package/dist/core/agent-session-review-coverage.test.js.map +1 -1
- package/dist/core/agent-session-slash-restore.test.d.ts +2 -0
- package/dist/core/agent-session-slash-restore.test.d.ts.map +1 -0
- package/dist/core/agent-session-slash-restore.test.js +121 -0
- package/dist/core/agent-session-slash-restore.test.js.map +1 -0
- package/dist/core/agent-session-subagent-policy.test.js +7 -3
- package/dist/core/agent-session-subagent-policy.test.js.map +1 -1
- package/dist/core/agent-session.d.ts +256 -10
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +1071 -219
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/agent-tools-allowlist.test.js +5 -3
- package/dist/core/agent-tools-allowlist.test.js.map +1 -1
- package/dist/core/agents.d.ts +11 -0
- package/dist/core/agents.d.ts.map +1 -1
- package/dist/core/agents.js +21 -0
- package/dist/core/agents.js.map +1 -1
- package/dist/core/agents.test.d.ts +2 -0
- package/dist/core/agents.test.d.ts.map +1 -0
- package/dist/core/agents.test.js +67 -0
- package/dist/core/agents.test.js.map +1 -0
- package/dist/core/auth-providers.js +1 -1
- package/dist/core/auth-providers.js.map +1 -1
- package/dist/core/compaction/active-context.test.js +21 -0
- package/dist/core/compaction/active-context.test.js.map +1 -1
- package/dist/core/compaction/compactor.d.ts +108 -8
- package/dist/core/compaction/compactor.d.ts.map +1 -1
- package/dist/core/compaction/compactor.js +423 -119
- package/dist/core/compaction/compactor.js.map +1 -1
- package/dist/core/compaction/compactor.test.js +361 -9
- package/dist/core/compaction/compactor.test.js.map +1 -1
- package/dist/core/compaction/policy.d.ts +17 -0
- package/dist/core/compaction/policy.d.ts.map +1 -0
- package/dist/core/compaction/policy.js +22 -0
- package/dist/core/compaction/policy.js.map +1 -0
- package/dist/core/compaction/policy.test.d.ts +2 -0
- package/dist/core/compaction/policy.test.d.ts.map +1 -0
- package/dist/core/compaction/policy.test.js +35 -0
- package/dist/core/compaction/policy.test.js.map +1 -0
- package/dist/core/compaction/query-aware-selector.d.ts +33 -0
- package/dist/core/compaction/query-aware-selector.d.ts.map +1 -0
- package/dist/core/compaction/query-aware-selector.js +202 -0
- package/dist/core/compaction/query-aware-selector.js.map +1 -0
- package/dist/core/compaction/query-aware-selector.test.d.ts +2 -0
- package/dist/core/compaction/query-aware-selector.test.d.ts.map +1 -0
- package/dist/core/compaction/query-aware-selector.test.js +96 -0
- package/dist/core/compaction/query-aware-selector.test.js.map +1 -0
- package/dist/core/compaction/token-estimator.test.js +1 -1
- package/dist/core/compaction/token-estimator.test.js.map +1 -1
- package/dist/core/compaction/tool-result-pruner.d.ts +24 -11
- package/dist/core/compaction/tool-result-pruner.d.ts.map +1 -1
- package/dist/core/compaction/tool-result-pruner.js +98 -26
- package/dist/core/compaction/tool-result-pruner.js.map +1 -1
- package/dist/core/compaction/tool-result-pruner.test.js +178 -6
- package/dist/core/compaction/tool-result-pruner.test.js.map +1 -1
- package/dist/core/encode-cwd.d.ts +17 -0
- package/dist/core/encode-cwd.d.ts.map +1 -1
- package/dist/core/encode-cwd.js +20 -7
- package/dist/core/encode-cwd.js.map +1 -1
- package/dist/core/encode-cwd.test.js +20 -1
- package/dist/core/encode-cwd.test.js.map +1 -1
- package/dist/core/event-bus.d.ts +23 -0
- package/dist/core/event-bus.d.ts.map +1 -1
- package/dist/core/event-bus.js +10 -0
- package/dist/core/event-bus.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +28 -8
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/plugin-bundles.d.ts +23 -0
- package/dist/core/extensions/plugin-bundles.d.ts.map +1 -0
- package/dist/core/extensions/plugin-bundles.js +228 -0
- package/dist/core/extensions/plugin-bundles.js.map +1 -0
- package/dist/core/extensions/plugin-bundles.test.d.ts +2 -0
- package/dist/core/extensions/plugin-bundles.test.d.ts.map +1 -0
- package/dist/core/extensions/plugin-bundles.test.js +110 -0
- package/dist/core/extensions/plugin-bundles.test.js.map +1 -0
- package/dist/core/foreign-session-import.d.ts +120 -0
- package/dist/core/foreign-session-import.d.ts.map +1 -0
- package/dist/core/foreign-session-import.js +598 -0
- package/dist/core/foreign-session-import.js.map +1 -0
- package/dist/core/foreign-session-import.test.d.ts +2 -0
- package/dist/core/foreign-session-import.test.d.ts.map +1 -0
- package/dist/core/foreign-session-import.test.js +220 -0
- package/dist/core/foreign-session-import.test.js.map +1 -0
- package/dist/core/foreign-transcript-blocks.d.ts +51 -0
- package/dist/core/foreign-transcript-blocks.d.ts.map +1 -0
- package/dist/core/foreign-transcript-blocks.js +151 -0
- package/dist/core/foreign-transcript-blocks.js.map +1 -0
- package/dist/core/ideal-review.d.ts +20 -1
- package/dist/core/ideal-review.d.ts.map +1 -1
- package/dist/core/ideal-review.js +43 -3
- package/dist/core/ideal-review.js.map +1 -1
- package/dist/core/ideal-review.test.js +106 -5
- package/dist/core/ideal-review.test.js.map +1 -1
- package/dist/core/local-endpoint-store.d.ts +50 -0
- package/dist/core/local-endpoint-store.d.ts.map +1 -0
- package/dist/core/local-endpoint-store.js +201 -0
- package/dist/core/local-endpoint-store.js.map +1 -0
- package/dist/core/local-endpoint-store.test.d.ts +2 -0
- package/dist/core/local-endpoint-store.test.d.ts.map +1 -0
- package/dist/core/local-endpoint-store.test.js +131 -0
- package/dist/core/local-endpoint-store.test.js.map +1 -0
- package/dist/core/loop-breaker.d.ts.map +1 -1
- package/dist/core/loop-breaker.js +1 -0
- package/dist/core/loop-breaker.js.map +1 -1
- package/dist/core/lsp/client.d.ts +65 -0
- package/dist/core/lsp/client.d.ts.map +1 -1
- package/dist/core/lsp/client.js +197 -14
- package/dist/core/lsp/client.js.map +1 -1
- package/dist/core/lsp/jsonrpc.d.ts +38 -1
- package/dist/core/lsp/jsonrpc.d.ts.map +1 -1
- package/dist/core/lsp/jsonrpc.js +13 -1
- package/dist/core/lsp/jsonrpc.js.map +1 -1
- package/dist/core/lsp/manager.d.ts +47 -9
- package/dist/core/lsp/manager.d.ts.map +1 -1
- package/dist/core/lsp/manager.js +107 -59
- package/dist/core/lsp/manager.js.map +1 -1
- package/dist/core/lsp/manager.test.js +50 -1
- package/dist/core/lsp/manager.test.js.map +1 -1
- package/dist/core/lsp/pool.d.ts +113 -0
- package/dist/core/lsp/pool.d.ts.map +1 -0
- package/dist/core/lsp/pool.js +255 -0
- package/dist/core/lsp/pool.js.map +1 -0
- package/dist/core/lsp/pool.test.d.ts +2 -0
- package/dist/core/lsp/pool.test.d.ts.map +1 -0
- package/dist/core/lsp/pool.test.js +286 -0
- package/dist/core/lsp/pool.test.js.map +1 -0
- package/dist/core/lsp/servers.d.ts.map +1 -1
- package/dist/core/lsp/servers.js +43 -3
- package/dist/core/lsp/servers.js.map +1 -1
- package/dist/core/lsp/servers.test.d.ts +2 -0
- package/dist/core/lsp/servers.test.d.ts.map +1 -0
- package/dist/core/lsp/servers.test.js +67 -0
- package/dist/core/lsp/servers.test.js.map +1 -0
- package/dist/core/lsp/test-support.d.ts +18 -0
- package/dist/core/lsp/test-support.d.ts.map +1 -0
- package/dist/core/lsp/test-support.js +27 -0
- package/dist/core/lsp/test-support.js.map +1 -0
- package/dist/core/lsp/uri.test.d.ts +2 -0
- package/dist/core/lsp/uri.test.d.ts.map +1 -0
- package/dist/core/lsp/uri.test.js +56 -0
- package/dist/core/lsp/uri.test.js.map +1 -0
- package/dist/core/lsp/windows.test.d.ts +2 -0
- package/dist/core/lsp/windows.test.d.ts.map +1 -0
- package/dist/core/lsp/windows.test.js +239 -0
- package/dist/core/lsp/windows.test.js.map +1 -0
- package/dist/core/mcp/catalog-cache.d.ts +63 -0
- package/dist/core/mcp/catalog-cache.d.ts.map +1 -0
- package/dist/core/mcp/catalog-cache.js +163 -0
- package/dist/core/mcp/catalog-cache.js.map +1 -0
- package/dist/core/mcp/catalog-cache.test.d.ts +2 -0
- package/dist/core/mcp/catalog-cache.test.d.ts.map +1 -0
- package/dist/core/mcp/catalog-cache.test.js +99 -0
- package/dist/core/mcp/catalog-cache.test.js.map +1 -0
- package/dist/core/mcp/client-session-recovery.test.d.ts +2 -0
- package/dist/core/mcp/client-session-recovery.test.d.ts.map +1 -0
- package/dist/core/mcp/client-session-recovery.test.js +231 -0
- package/dist/core/mcp/client-session-recovery.test.js.map +1 -0
- package/dist/core/mcp/client.d.ts +158 -0
- package/dist/core/mcp/client.d.ts.map +1 -1
- package/dist/core/mcp/client.js +476 -35
- package/dist/core/mcp/client.js.map +1 -1
- package/dist/core/mcp/elicitation-bridge.d.ts +41 -0
- package/dist/core/mcp/elicitation-bridge.d.ts.map +1 -0
- package/dist/core/mcp/elicitation-bridge.js +52 -0
- package/dist/core/mcp/elicitation-bridge.js.map +1 -0
- package/dist/core/mcp/elicitation.test.d.ts +2 -0
- package/dist/core/mcp/elicitation.test.d.ts.map +1 -0
- package/dist/core/mcp/elicitation.test.js +158 -0
- package/dist/core/mcp/elicitation.test.js.map +1 -0
- package/dist/core/mcp/index.d.ts +5 -1
- package/dist/core/mcp/index.d.ts.map +1 -1
- package/dist/core/mcp/index.js +2 -0
- package/dist/core/mcp/index.js.map +1 -1
- package/dist/core/mcp/legacy-negotiation.test.d.ts +2 -0
- package/dist/core/mcp/legacy-negotiation.test.d.ts.map +1 -0
- package/dist/core/mcp/legacy-negotiation.test.js +80 -0
- package/dist/core/mcp/legacy-negotiation.test.js.map +1 -0
- package/dist/core/mcp/oauth-provider.d.ts +1 -2
- package/dist/core/mcp/oauth-provider.d.ts.map +1 -1
- package/dist/core/mcp/oauth-provider.js +6 -2
- package/dist/core/mcp/oauth-provider.js.map +1 -1
- package/dist/core/mcp/oauth-store.d.ts +1 -1
- package/dist/core/mcp/oauth-store.d.ts.map +1 -1
- package/dist/core/mcp/oauth-store.js.map +1 -1
- package/dist/core/mcp/resolve-stdio.d.ts +35 -0
- package/dist/core/mcp/resolve-stdio.d.ts.map +1 -1
- package/dist/core/mcp/resolve-stdio.js +80 -1
- package/dist/core/mcp/resolve-stdio.js.map +1 -1
- package/dist/core/mcp/resolve-stdio.test.js +99 -4
- package/dist/core/mcp/resolve-stdio.test.js.map +1 -1
- package/dist/core/mcp/session-elicitation-wiring.test.d.ts +2 -0
- package/dist/core/mcp/session-elicitation-wiring.test.d.ts.map +1 -0
- package/dist/core/mcp/session-elicitation-wiring.test.js +138 -0
- package/dist/core/mcp/session-elicitation-wiring.test.js.map +1 -0
- package/dist/core/mcp/shared-pool.d.ts +158 -0
- package/dist/core/mcp/shared-pool.d.ts.map +1 -0
- package/dist/core/mcp/shared-pool.js +235 -0
- package/dist/core/mcp/shared-pool.js.map +1 -0
- package/dist/core/mcp/shared-pool.test.d.ts +2 -0
- package/dist/core/mcp/shared-pool.test.d.ts.map +1 -0
- package/dist/core/mcp/shared-pool.test.js +425 -0
- package/dist/core/mcp/shared-pool.test.js.map +1 -0
- package/dist/core/mcp/types.d.ts +12 -0
- package/dist/core/mcp/types.d.ts.map +1 -1
- package/dist/core/mcp/when-connected.test.d.ts +2 -0
- package/dist/core/mcp/when-connected.test.d.ts.map +1 -0
- package/dist/core/mcp/when-connected.test.js +46 -0
- package/dist/core/mcp/when-connected.test.js.map +1 -0
- package/dist/core/network-guard.d.ts +47 -0
- package/dist/core/network-guard.d.ts.map +1 -0
- package/dist/core/network-guard.js +208 -0
- package/dist/core/network-guard.js.map +1 -0
- package/dist/core/network-guard.test.d.ts +2 -0
- package/dist/core/network-guard.test.d.ts.map +1 -0
- package/dist/core/network-guard.test.js +72 -0
- package/dist/core/network-guard.test.js.map +1 -0
- package/dist/core/nolan-context.d.ts.map +1 -1
- package/dist/core/nolan-context.js +8 -0
- package/dist/core/nolan-context.js.map +1 -1
- package/dist/core/nolan-context.test.js +40 -0
- package/dist/core/nolan-context.test.js.map +1 -1
- package/dist/core/nolan-prompt.js +3 -0
- package/dist/core/nolan-prompt.js.map +1 -1
- package/dist/core/nolan-prompt.test.js +5 -0
- package/dist/core/nolan-prompt.test.js.map +1 -1
- package/dist/core/persistent-shell.d.ts +4 -1
- package/dist/core/persistent-shell.d.ts.map +1 -1
- package/dist/core/persistent-shell.js +21 -4
- package/dist/core/persistent-shell.js.map +1 -1
- package/dist/core/process-gate.d.ts +45 -0
- package/dist/core/process-gate.d.ts.map +1 -0
- package/dist/core/process-gate.js +79 -0
- package/dist/core/process-gate.js.map +1 -0
- package/dist/core/process-gate.test.d.ts +2 -0
- package/dist/core/process-gate.test.d.ts.map +1 -0
- package/dist/core/process-gate.test.js +87 -0
- package/dist/core/process-gate.test.js.map +1 -0
- package/dist/core/process-manager-dev-server-repro.test.js +34 -12
- package/dist/core/process-manager-dev-server-repro.test.js.map +1 -1
- package/dist/core/process-manager-notifications.test.d.ts +2 -0
- package/dist/core/process-manager-notifications.test.d.ts.map +1 -0
- package/dist/core/process-manager-notifications.test.js +217 -0
- package/dist/core/process-manager-notifications.test.js.map +1 -0
- package/dist/core/process-manager.d.ts +51 -2
- package/dist/core/process-manager.d.ts.map +1 -1
- package/dist/core/process-manager.js +212 -15
- package/dist/core/process-manager.js.map +1 -1
- package/dist/core/project-discovery.d.ts +38 -6
- package/dist/core/project-discovery.d.ts.map +1 -1
- package/dist/core/project-discovery.js +327 -40
- package/dist/core/project-discovery.js.map +1 -1
- package/dist/core/project-discovery.test.js +226 -1
- package/dist/core/project-discovery.test.js.map +1 -1
- package/dist/core/prompt-commands.d.ts.map +1 -1
- package/dist/core/prompt-commands.js +89 -156
- package/dist/core/prompt-commands.js.map +1 -1
- package/dist/core/prompt-commands.test.js +4 -10
- package/dist/core/prompt-commands.test.js.map +1 -1
- package/dist/core/radio.test.js +4 -1
- package/dist/core/radio.test.js.map +1 -1
- package/dist/core/regrounding.d.ts.map +1 -1
- package/dist/core/regrounding.js +1 -0
- package/dist/core/regrounding.js.map +1 -1
- package/dist/core/run-claim.d.ts +37 -0
- package/dist/core/run-claim.d.ts.map +1 -0
- package/dist/core/run-claim.js +46 -0
- package/dist/core/run-claim.js.map +1 -0
- package/dist/core/run-claim.test.d.ts +2 -0
- package/dist/core/run-claim.test.d.ts.map +1 -0
- package/dist/core/run-claim.test.js +107 -0
- package/dist/core/run-claim.test.js.map +1 -0
- package/dist/core/run-journal.test.d.ts +2 -0
- package/dist/core/run-journal.test.d.ts.map +1 -0
- package/dist/core/run-journal.test.js +195 -0
- package/dist/core/run-journal.test.js.map +1 -0
- package/dist/core/run-lifecycle.d.ts +22 -3
- package/dist/core/run-lifecycle.d.ts.map +1 -1
- package/dist/core/run-lifecycle.js +12 -3
- package/dist/core/run-lifecycle.js.map +1 -1
- package/dist/core/runtime-mode.d.ts +2 -0
- package/dist/core/runtime-mode.d.ts.map +1 -1
- package/dist/core/runtime-mode.js +4 -0
- package/dist/core/runtime-mode.js.map +1 -1
- package/dist/core/sandbox-domains.d.ts +14 -0
- package/dist/core/sandbox-domains.d.ts.map +1 -0
- package/dist/core/sandbox-domains.js +76 -0
- package/dist/core/sandbox-domains.js.map +1 -0
- package/dist/core/sandbox-feedback.d.ts +22 -0
- package/dist/core/sandbox-feedback.d.ts.map +1 -0
- package/dist/core/sandbox-feedback.js +69 -0
- package/dist/core/sandbox-feedback.js.map +1 -0
- package/dist/core/sandbox-feedback.test.d.ts +2 -0
- package/dist/core/sandbox-feedback.test.d.ts.map +1 -0
- package/dist/core/sandbox-feedback.test.js +32 -0
- package/dist/core/sandbox-feedback.test.js.map +1 -0
- package/dist/core/sandbox.d.ts +91 -0
- package/dist/core/sandbox.d.ts.map +1 -0
- package/dist/core/sandbox.js +254 -0
- package/dist/core/sandbox.js.map +1 -0
- package/dist/core/sandbox.test.d.ts +2 -0
- package/dist/core/sandbox.test.d.ts.map +1 -0
- package/dist/core/sandbox.test.js +157 -0
- package/dist/core/sandbox.test.js.map +1 -0
- package/dist/core/session-compaction.d.ts +6 -0
- package/dist/core/session-compaction.d.ts.map +1 -1
- package/dist/core/session-compaction.js +11 -1
- package/dist/core/session-compaction.js.map +1 -1
- package/dist/core/session-compaction.test.js +18 -1
- package/dist/core/session-compaction.test.js.map +1 -1
- package/dist/core/session-export.d.ts +98 -0
- package/dist/core/session-export.d.ts.map +1 -0
- package/dist/core/session-export.js +364 -0
- package/dist/core/session-export.js.map +1 -0
- package/dist/core/session-export.test.d.ts +2 -0
- package/dist/core/session-export.test.d.ts.map +1 -0
- package/dist/core/session-export.test.js +353 -0
- package/dist/core/session-export.test.js.map +1 -0
- package/dist/core/session-history.d.ts +95 -6
- package/dist/core/session-history.d.ts.map +1 -1
- package/dist/core/session-history.js +261 -17
- package/dist/core/session-history.js.map +1 -1
- package/dist/core/session-history.test.js +173 -2
- package/dist/core/session-history.test.js.map +1 -1
- package/dist/core/session-manager.d.ts +223 -6
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +515 -26
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-manager.test.js +147 -3
- package/dist/core/session-manager.test.js.map +1 -1
- package/dist/core/session-notification-visibility.test.d.ts +2 -0
- package/dist/core/session-notification-visibility.test.d.ts.map +1 -0
- package/dist/core/session-notification-visibility.test.js +113 -0
- package/dist/core/session-notification-visibility.test.js.map +1 -0
- package/dist/core/session-preview.d.ts +5 -4
- package/dist/core/session-preview.d.ts.map +1 -1
- package/dist/core/session-preview.js +12 -5
- package/dist/core/session-preview.js.map +1 -1
- package/dist/core/session-restore-display.test.js +25 -0
- package/dist/core/session-restore-display.test.js.map +1 -1
- package/dist/core/session-storage.d.ts.map +1 -1
- package/dist/core/session-storage.js +64 -4
- package/dist/core/session-storage.js.map +1 -1
- package/dist/core/session-storage.test.js +10 -2
- package/dist/core/session-storage.test.js.map +1 -1
- package/dist/core/settings-manager.d.ts +13 -2
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +36 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/setup-history.d.ts +0 -6
- package/dist/core/setup-history.d.ts.map +1 -1
- package/dist/core/setup-history.js +2 -18
- package/dist/core/setup-history.js.map +1 -1
- package/dist/core/setup-history.test.js +11 -26
- package/dist/core/setup-history.test.js.map +1 -1
- package/dist/core/skills-routing.test.js +3 -1
- package/dist/core/skills-routing.test.js.map +1 -1
- package/dist/core/slash-commands.d.ts +18 -0
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +42 -10
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/slash-commands.test.d.ts +2 -0
- package/dist/core/slash-commands.test.d.ts.map +1 -0
- package/dist/core/slash-commands.test.js +59 -0
- package/dist/core/slash-commands.test.js.map +1 -0
- package/dist/core/speed-benchmark.test.js +9 -3
- package/dist/core/speed-benchmark.test.js.map +1 -1
- package/dist/core/steering.d.ts +12 -0
- package/dist/core/steering.d.ts.map +1 -1
- package/dist/core/steering.js +16 -0
- package/dist/core/steering.js.map +1 -1
- package/dist/core/subagent-manager.d.ts +14 -1
- package/dist/core/subagent-manager.d.ts.map +1 -1
- package/dist/core/subagent-manager.js +48 -1
- package/dist/core/subagent-manager.js.map +1 -1
- package/dist/core/subagent-manager.test.js +43 -7
- package/dist/core/subagent-manager.test.js.map +1 -1
- package/dist/core/subagent-store.test.js +14 -2
- package/dist/core/subagent-store.test.js.map +1 -1
- package/dist/core/verification-evidence.d.ts +17 -0
- package/dist/core/verification-evidence.d.ts.map +1 -0
- package/dist/core/verification-evidence.js +283 -0
- package/dist/core/verification-evidence.js.map +1 -0
- package/dist/core/verification-evidence.test.d.ts +2 -0
- package/dist/core/verification-evidence.test.d.ts.map +1 -0
- package/dist/core/verification-evidence.test.js +103 -0
- package/dist/core/verification-evidence.test.js.map +1 -0
- package/dist/core/workspace-guard.d.ts +2 -0
- package/dist/core/workspace-guard.d.ts.map +1 -1
- package/dist/core/workspace-guard.js +4 -1
- package/dist/core/workspace-guard.js.map +1 -1
- package/dist/core/workspace-guard.test.js +21 -0
- package/dist/core/workspace-guard.test.js.map +1 -1
- package/dist/interactive.d.ts.map +1 -1
- package/dist/interactive.js +17 -3
- package/dist/interactive.js.map +1 -1
- package/dist/modes/acp-mode.d.ts +122 -0
- package/dist/modes/acp-mode.d.ts.map +1 -0
- package/dist/modes/acp-mode.js +1366 -0
- package/dist/modes/acp-mode.js.map +1 -0
- package/dist/modes/acp-mode.test.d.ts +2 -0
- package/dist/modes/acp-mode.test.d.ts.map +1 -0
- package/dist/modes/acp-mode.test.js +1065 -0
- package/dist/modes/acp-mode.test.js.map +1 -0
- package/dist/modes/index.d.ts +1 -0
- package/dist/modes/index.d.ts.map +1 -1
- package/dist/modes/index.js +1 -0
- package/dist/modes/index.js.map +1 -1
- package/dist/modes/json-mode.d.ts +35 -0
- package/dist/modes/json-mode.d.ts.map +1 -1
- package/dist/modes/json-mode.js +38 -0
- package/dist/modes/json-mode.js.map +1 -1
- package/dist/modes/json-mode.test.d.ts +2 -0
- package/dist/modes/json-mode.test.d.ts.map +1 -0
- package/dist/modes/json-mode.test.js +84 -0
- package/dist/modes/json-mode.test.js.map +1 -0
- package/dist/modes/subagent-worker-mode.d.ts +5 -0
- package/dist/modes/subagent-worker-mode.d.ts.map +1 -1
- package/dist/modes/subagent-worker-mode.js +91 -11
- package/dist/modes/subagent-worker-mode.js.map +1 -1
- package/dist/modes/subagent-worker-mode.test.d.ts +2 -0
- package/dist/modes/subagent-worker-mode.test.d.ts.map +1 -0
- package/dist/modes/subagent-worker-mode.test.js +52 -0
- package/dist/modes/subagent-worker-mode.test.js.map +1 -0
- package/dist/session.d.ts +29 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +41 -0
- package/dist/session.js.map +1 -1
- package/dist/system-prompt.d.ts +11 -1
- package/dist/system-prompt.d.ts.map +1 -1
- package/dist/system-prompt.js +39 -18
- package/dist/system-prompt.js.map +1 -1
- package/dist/system-prompt.test.js +33 -17
- package/dist/system-prompt.test.js.map +1 -1
- package/dist/test-support/fake-home.d.ts +20 -0
- package/dist/test-support/fake-home.d.ts.map +1 -0
- package/dist/test-support/fake-home.js +37 -0
- package/dist/test-support/fake-home.js.map +1 -0
- package/dist/tools/bash.d.ts +3 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +70 -14
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/bash.test.js +122 -7
- package/dist/tools/bash.test.js.map +1 -1
- package/dist/tools/edit-robustness.test.js +3 -1
- package/dist/tools/edit-robustness.test.js.map +1 -1
- package/dist/tools/find.d.ts.map +1 -1
- package/dist/tools/find.js +4 -1
- package/dist/tools/find.js.map +1 -1
- package/dist/tools/goals.d.ts +2 -2
- package/dist/tools/grep.d.ts +3 -1
- package/dist/tools/grep.d.ts.map +1 -1
- package/dist/tools/grep.js +137 -12
- package/dist/tools/grep.js.map +1 -1
- package/dist/tools/grep.test.js +53 -0
- package/dist/tools/grep.test.js.map +1 -1
- package/dist/tools/index.d.ts +19 -2
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +22 -19
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/index.test.d.ts +2 -0
- package/dist/tools/index.test.d.ts.map +1 -0
- package/dist/tools/index.test.js +55 -0
- package/dist/tools/index.test.js.map +1 -0
- package/dist/tools/overflow.test.js +4 -7
- package/dist/tools/overflow.test.js.map +1 -1
- package/dist/tools/path-utils.d.ts +15 -0
- package/dist/tools/path-utils.d.ts.map +1 -1
- package/dist/tools/path-utils.js +17 -0
- package/dist/tools/path-utils.js.map +1 -1
- package/dist/tools/path-utils.test.js +7 -2
- package/dist/tools/path-utils.test.js.map +1 -1
- package/dist/tools/prompt-hints.js +1 -1
- package/dist/tools/prompt-hints.js.map +1 -1
- package/dist/tools/read-only-bash.d.ts +3 -0
- package/dist/tools/read-only-bash.d.ts.map +1 -1
- package/dist/tools/read-only-bash.js +7 -9
- package/dist/tools/read-only-bash.js.map +1 -1
- package/dist/tools/search-code.d.ts.map +1 -1
- package/dist/tools/search-code.js +6 -4
- package/dist/tools/search-code.js.map +1 -1
- package/dist/tools/subagent-control.d.ts.map +1 -1
- package/dist/tools/subagent-control.js +6 -2
- package/dist/tools/subagent-control.js.map +1 -1
- package/dist/tools/subagent-shared.d.ts +6 -0
- package/dist/tools/subagent-shared.d.ts.map +1 -1
- package/dist/tools/subagent-shared.js +6 -0
- package/dist/tools/subagent-shared.js.map +1 -1
- package/dist/tools/subagent.d.ts +1 -1
- package/dist/tools/subagent.d.ts.map +1 -1
- package/dist/tools/subagent.js +51 -8
- package/dist/tools/subagent.js.map +1 -1
- package/dist/tools/subagent.test.js +65 -3
- package/dist/tools/subagent.test.js.map +1 -1
- package/dist/tools/task-output.d.ts.map +1 -1
- package/dist/tools/task-output.js +3 -1
- package/dist/tools/task-output.js.map +1 -1
- package/dist/tools/tool-search.d.ts +12 -1
- package/dist/tools/tool-search.d.ts.map +1 -1
- package/dist/tools/tool-search.js +22 -3
- package/dist/tools/tool-search.js.map +1 -1
- package/dist/tools/tool-search.test.d.ts +2 -0
- package/dist/tools/tool-search.test.d.ts.map +1 -0
- package/dist/tools/tool-search.test.js +69 -0
- package/dist/tools/tool-search.test.js.map +1 -0
- package/dist/tools/web-fetch.d.ts +3 -2
- package/dist/tools/web-fetch.d.ts.map +1 -1
- package/dist/tools/web-fetch.js +22 -8
- package/dist/tools/web-fetch.js.map +1 -1
- package/dist/tools/web-fetch.test.js +34 -0
- package/dist/tools/web-fetch.test.js.map +1 -1
- package/dist/tools/web-search.d.ts +2 -1
- package/dist/tools/web-search.d.ts.map +1 -1
- package/dist/tools/web-search.js +21 -5
- package/dist/tools/web-search.js.map +1 -1
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/App.d.ts.map +1 -1
- package/dist/ui/App.js +32 -81
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/app-items.d.ts +1 -12
- package/dist/ui/app-items.d.ts.map +1 -1
- package/dist/ui/app-items.js.map +1 -1
- package/dist/ui/components/Footer.d.ts +1 -1
- package/dist/ui/components/Footer.js +2 -2
- package/dist/ui/components/Footer.js.map +1 -1
- package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
- package/dist/ui/hooks/useAgentLoop.js +18 -4
- package/dist/ui/hooks/useAgentLoop.js.map +1 -1
- package/dist/ui/hooks/usePixelFixFlow.d.ts +1 -2
- package/dist/ui/hooks/usePixelFixFlow.d.ts.map +1 -1
- package/dist/ui/hooks/usePixelFixFlow.js +2 -5
- package/dist/ui/hooks/usePixelFixFlow.js.map +1 -1
- package/dist/ui/login.js +1 -1
- package/dist/ui/login.js.map +1 -1
- package/dist/ui/terminal-history-status-renderers.d.ts +1 -2
- package/dist/ui/terminal-history-status-renderers.d.ts.map +1 -1
- package/dist/ui/terminal-history-status-renderers.js +2 -15
- package/dist/ui/terminal-history-status-renderers.js.map +1 -1
- package/dist/ui/terminal-history.d.ts.map +1 -1
- package/dist/ui/terminal-history.js +2 -4
- package/dist/ui/terminal-history.js.map +1 -1
- package/dist/ui/thinking-level-cycle.test.js +8 -8
- package/dist/ui/thinking-level-cycle.test.js.map +1 -1
- package/dist/ui/transcript/MiscRows.d.ts +1 -4
- package/dist/ui/transcript/MiscRows.d.ts.map +1 -1
- package/dist/ui/transcript/MiscRows.js +2 -7
- package/dist/ui/transcript/MiscRows.js.map +1 -1
- package/dist/ui/transcript/TranscriptRenderer.d.ts.map +1 -1
- package/dist/ui/transcript/TranscriptRenderer.js +1 -3
- package/dist/ui/transcript/TranscriptRenderer.js.map +1 -1
- package/dist/ui/transcript/presentation.d.ts +1 -9
- package/dist/ui/transcript/presentation.d.ts.map +1 -1
- package/dist/ui/transcript/presentation.js +0 -9
- package/dist/ui/transcript/presentation.js.map +1 -1
- package/dist/ui/transcript/spacing.d.ts +2 -2
- package/dist/ui/transcript/spacing.d.ts.map +1 -1
- package/dist/ui/transcript/spacing.js +0 -1
- package/dist/ui/transcript/spacing.js.map +1 -1
- package/dist/ui/transcript/spacing.test.js +1 -3
- package/dist/ui/transcript/spacing.test.js.map +1 -1
- package/dist/ui/tui-history-parity.test.js +2 -9
- package/dist/ui/tui-history-parity.test.js.map +1 -1
- package/dist/utils/github.d.ts +20 -0
- package/dist/utils/github.d.ts.map +1 -0
- package/dist/utils/github.js +65 -0
- package/dist/utils/github.js.map +1 -0
- package/dist/utils/github.test.d.ts +2 -0
- package/dist/utils/github.test.d.ts.map +1 -0
- package/dist/utils/github.test.js +23 -0
- package/dist/utils/github.test.js.map +1 -0
- package/dist/utils/image.d.ts +26 -3
- package/dist/utils/image.d.ts.map +1 -1
- package/dist/utils/image.js +83 -14
- package/dist/utils/image.js.map +1 -1
- package/dist/utils/image.test.d.ts +2 -0
- package/dist/utils/image.test.d.ts.map +1 -0
- package/dist/utils/image.test.js +139 -0
- package/dist/utils/image.test.js.map +1 -0
- package/dist/utils/process.d.ts +26 -3
- package/dist/utils/process.d.ts.map +1 -1
- package/dist/utils/process.js +68 -10
- package/dist/utils/process.js.map +1 -1
- package/dist/utils/process.test.d.ts +2 -0
- package/dist/utils/process.test.d.ts.map +1 -0
- package/dist/utils/process.test.js +42 -0
- package/dist/utils/process.test.js.map +1 -0
- package/package.json +6 -5
|
@@ -11,33 +11,47 @@ import { getClaudeCliUserAgent } from "./claude-code-version.js";
|
|
|
11
11
|
import { kimiCodingHeaders, isKimiCodingEndpoint } from "./oauth/kimi.js";
|
|
12
12
|
import { SessionManager, NOLAN_TURN_CUSTOM_KIND, AUTOPILOT_MARKER_CUSTOM_KIND, APP_MARKER_CUSTOM_KIND, } from "./session-manager.js";
|
|
13
13
|
import { ExtensionLoader } from "./extensions/loader.js";
|
|
14
|
-
import { shouldCompact, compact } from "./compaction/compactor.js";
|
|
14
|
+
import { shouldCompact, compact, } from "./compaction/compactor.js";
|
|
15
|
+
import { getHistoryMessageVisibility, remapAnchorForCompaction, stripRecordedPosition, } from "./session-history.js";
|
|
16
|
+
import { sourceFingerprint as computeSourceFingerprint } from "./session-compaction.js";
|
|
15
17
|
import { getAuthStorageKeys, getContextWindow, getModel, getToolResultCharLimit, MODELS, } from "./model-registry.js";
|
|
16
18
|
import { discoverSkills } from "./skills.js";
|
|
17
19
|
import { ensureAppDirs } from "../config.js";
|
|
18
20
|
import { buildSystemPrompt } from "../system-prompt.js";
|
|
19
21
|
import { createTools, createWebSearchTool, } from "../tools/index.js";
|
|
22
|
+
import { buildProcessCompletionFollowUp } from "./process-gate.js";
|
|
20
23
|
import { buildSubAgentCompletionFollowUp } from "./subagent-manager.js";
|
|
21
24
|
import { applyAsyncSubagentPolicy } from "./subagent-policy.js";
|
|
25
|
+
import { z } from "zod";
|
|
22
26
|
import { MCPClientManager, getAllMcpServers } from "./mcp/index.js";
|
|
23
27
|
import { DeferredToolCatalog } from "./mcp/deferred-catalog.js";
|
|
28
|
+
import { McpCatalogCache } from "./mcp/catalog-cache.js";
|
|
29
|
+
import { describeDropped, importForeignSession, } from "./foreign-session-import.js";
|
|
24
30
|
import { createToolSearchTool } from "../tools/tool-search.js";
|
|
25
31
|
import { log } from "./logger.js";
|
|
26
32
|
import { setEstimatorModel, calibrateEstimatorFromUsage } from "./compaction/token-estimator.js";
|
|
27
33
|
import { calculateActiveContextTokens } from "./compaction/active-context.js";
|
|
34
|
+
import { resolveCompactionPolicy } from "./compaction/policy.js";
|
|
28
35
|
import { pruneStaleToolResults } from "./compaction/tool-result-pruner.js";
|
|
29
36
|
import { discoverAgents } from "./agents.js";
|
|
30
37
|
import { enhancePrompt } from "../utils/prompt-enhancer.js";
|
|
31
38
|
import { detectProjectStack } from "./language-detector.js";
|
|
32
|
-
import { evaluateIdealReview, buildIdealReviewMessage, buildReviewCoverageMessage, withReviewCoverageRequirements, detectTestDrift, ReviewCoverageTracker, } from "./ideal-review.js";
|
|
39
|
+
import { evaluateIdealReview, buildIdealReviewMessage, buildReviewCoverageEscalationMessage, buildReviewCoverageMessage, MAX_REVIEW_COVERAGE_INJECTIONS, withReviewCoverageRequirements, detectTestDrift, ReviewCoverageTracker, } from "./ideal-review.js";
|
|
33
40
|
import { evaluateLoopBreak, buildLoopBreakMessage, CycleDetector, ToolCallProgressTracker, detectTextRepetition, } from "./loop-breaker.js";
|
|
34
41
|
import { buildRegroundingMessage } from "./regrounding.js";
|
|
35
|
-
import { wrapSteeringText, STEERING_PREFIX } from "./steering.js";
|
|
42
|
+
import { wrapSteeringText, buildNotificationSteeringText, STEERING_PREFIX } from "./steering.js";
|
|
43
|
+
import { AgentNotificationQueue } from "./agent-notifications.js";
|
|
36
44
|
import { findUserSessionPrompt, getUserSessionPrompt } from "./session-preview.js";
|
|
37
45
|
import { normalizeMessageImages } from "./message-images.js";
|
|
38
46
|
import crypto from "node:crypto";
|
|
39
47
|
import fs from "node:fs/promises";
|
|
48
|
+
import os from "node:os";
|
|
40
49
|
import path from "node:path";
|
|
50
|
+
/**
|
|
51
|
+
* A run whose tool calls fail more often than this is thrashing, not
|
|
52
|
+
* progressing — refuse to extend its turn budget.
|
|
53
|
+
*/
|
|
54
|
+
const TURN_EXTENSION_MAX_FAILURE_RATIO = 0.5;
|
|
41
55
|
// ── Tool-result policy ─────────────────────────────────────
|
|
42
56
|
/** Resolve the per-result cap passed to the agent loop for the active transport. */
|
|
43
57
|
export function resolveSessionToolResultCharLimit(model, provider, accountId) {
|
|
@@ -56,6 +70,17 @@ export function resolveSessionTurnToolResultCharLimit(model, provider, accountId
|
|
|
56
70
|
const contextChars = getContextWindow(model, { provider, accountId }) * 3.5;
|
|
57
71
|
return Math.max(100_000, Math.min(Math.floor(contextChars * 0.15), 240_000));
|
|
58
72
|
}
|
|
73
|
+
/** Marker the compactor prepends to the summary message it injects. */
|
|
74
|
+
/**
|
|
75
|
+
* True when an assistant message ends a turn with tool calls still awaiting
|
|
76
|
+
* their results. Inserting a user message there would orphan the tool_use
|
|
77
|
+
* blocks and the provider rejects the next request.
|
|
78
|
+
*/
|
|
79
|
+
function hasUnresolvedToolCalls(message) {
|
|
80
|
+
if (typeof message.content === "string" || !Array.isArray(message.content))
|
|
81
|
+
return false;
|
|
82
|
+
return message.content.some((part) => part.type === "tool_call");
|
|
83
|
+
}
|
|
59
84
|
// ── Agent Session ──────────────────────────────────────────
|
|
60
85
|
export class AgentSession {
|
|
61
86
|
eventBus = new EventBus();
|
|
@@ -114,12 +139,20 @@ export class AgentSession {
|
|
|
114
139
|
/** Runtime-only suppression while Nolan owns verification in autopilot mode. */
|
|
115
140
|
idealReviewSuppressed = false;
|
|
116
141
|
reviewCoverage;
|
|
142
|
+
/** Coverage follow-ups spent this run, capped by MAX_REVIEW_COVERAGE_INJECTIONS. */
|
|
143
|
+
reviewCoverageInjected = 0;
|
|
117
144
|
/** 0 = none; 1 = first nudge sent; 2 = final stop-and-report injected. */
|
|
118
145
|
loopBreakInjected = 0;
|
|
119
146
|
regroundingInjected = false;
|
|
147
|
+
/** Wall-clock start of the current run; scopes the background-process gate. */
|
|
148
|
+
runStartedAt = 0;
|
|
149
|
+
/** Gate injections spent this run, capped by MAX_PROCESS_GATE_INJECTIONS. */
|
|
150
|
+
processGateInjected = 0;
|
|
120
151
|
compactionOccurred = false;
|
|
121
152
|
lastCompactionCompacted = false;
|
|
122
153
|
compactionRetryAfter = 0;
|
|
154
|
+
/** A restored oversized checkpoint must be canonicalized before its first prompt is persisted. */
|
|
155
|
+
deferredCompactionPending = false;
|
|
123
156
|
/** Latest provider count, anchored to the assistant response it measured. */
|
|
124
157
|
providerContext = null;
|
|
125
158
|
originalRequest = "";
|
|
@@ -127,10 +160,21 @@ export class AgentSession {
|
|
|
127
160
|
// mid-loop steering boundary (user steering wins over the hooks), mirroring
|
|
128
161
|
// the TUI's getSteeringMessages. Each entry carries its own attachments so a
|
|
129
162
|
// user can queue media (images/video/files) mid-run, not just plain text.
|
|
163
|
+
// Each entry carries a stable id so a client can cancel one specific pending
|
|
164
|
+
// message by identity. Index-based removal would race: the queue drains at
|
|
165
|
+
// every turn boundary, so an index captured by the UI can point at a
|
|
166
|
+
// different message (or past the end) by the time the cancel arrives.
|
|
130
167
|
userQueue = [];
|
|
168
|
+
queueSeq = 0;
|
|
131
169
|
processManager;
|
|
132
170
|
lspManager;
|
|
133
171
|
subAgentManager;
|
|
172
|
+
/**
|
|
173
|
+
* Out-of-band push notifications (finished children, background-process
|
|
174
|
+
* progress). Producers enqueue; `getHookSteeringMessages` drains into the
|
|
175
|
+
* live turn so the agent never has to spend a turn asking.
|
|
176
|
+
*/
|
|
177
|
+
notifications = new AgentNotificationQueue();
|
|
134
178
|
managerAbortSignal;
|
|
135
179
|
managerAbortHandler = () => {
|
|
136
180
|
void this.subAgentManager?.interruptAll();
|
|
@@ -138,6 +182,11 @@ export class AgentSession {
|
|
|
138
182
|
mcpManager;
|
|
139
183
|
/** Deferred MCP tools awaiting discovery via tool_search (bench A win). */
|
|
140
184
|
mcpCatalog;
|
|
185
|
+
/** Live (connected) MCP tools by name — the reconcile target for cached stubs. */
|
|
186
|
+
liveMcpTools = new Map();
|
|
187
|
+
/** Server name for each cached-only tool, so a stub knows what to wait on. */
|
|
188
|
+
cachedMcpToolServers = new Map();
|
|
189
|
+
mcpCatalogCache = new McpCatalogCache();
|
|
141
190
|
provider;
|
|
142
191
|
model;
|
|
143
192
|
cwd;
|
|
@@ -158,7 +207,10 @@ export class AgentSession {
|
|
|
158
207
|
* set, the system prompt carries the `[DONE:n]` progress contract so the
|
|
159
208
|
* model emits step-completion markers the UI's plan-progress widget reads. */
|
|
160
209
|
approvedPlanPath;
|
|
210
|
+
/** Extra workspace roots added with `/add-dir` (resolved, de-duplicated). */
|
|
211
|
+
additionalRoots = [];
|
|
161
212
|
sessionId = "";
|
|
213
|
+
checkpointGeneration = 0;
|
|
162
214
|
/** Stable identity shared by compaction and approved-plan checkpoint files. */
|
|
163
215
|
conversationId = "";
|
|
164
216
|
/** Original user-authored prompt, retained when internal messages replace history. */
|
|
@@ -168,6 +220,31 @@ export class AgentSession {
|
|
|
168
220
|
transportSessionId = crypto.randomUUID();
|
|
169
221
|
sessionPath = "";
|
|
170
222
|
lastPersistedIndex = 0;
|
|
223
|
+
/**
|
|
224
|
+
* The array `agentLoop` is currently mutating, while a run is in flight.
|
|
225
|
+
*
|
|
226
|
+
* Normally identical to `this.messages`, but a mid-loop compaction rebinds
|
|
227
|
+
* `this.messages` to the compacted result while the loop keeps appending to
|
|
228
|
+
* its own array. Step-boundary flushes must follow the loop's array or they
|
|
229
|
+
* would silently persist nothing for the rest of the run.
|
|
230
|
+
*/
|
|
231
|
+
activeLoopMessages = null;
|
|
232
|
+
/**
|
|
233
|
+
* Number of non-system messages guaranteed to be in the session file — the
|
|
234
|
+
* anchor base for transcript markers (Nolan turns, autopilot verdicts, app
|
|
235
|
+
* markers). `this.messages` can run ahead of the file: the agent loop
|
|
236
|
+
* appends assistant/tool/steering messages in place but they are only
|
|
237
|
+
* persisted when the run SUCCEEDS, so after a failed run the in-memory list
|
|
238
|
+
* carries an unpersisted tail. Markers anchored against that tail point past
|
|
239
|
+
* their real position on resume — the row (notably an error) renders lower
|
|
240
|
+
* in the transcript than it happened, bunching at the bottom, or gets
|
|
241
|
+
* dropped as out-of-range. Anchoring to the persisted prefix keeps resume
|
|
242
|
+
* placement 1:1 with where the row appeared live.
|
|
243
|
+
*/
|
|
244
|
+
persistedTranscriptCount() {
|
|
245
|
+
return this.messages.slice(0, this.lastPersistedIndex).filter((m) => m.role !== "system")
|
|
246
|
+
.length;
|
|
247
|
+
}
|
|
171
248
|
/** Current leaf entry ID in the session DAG — used to chain parentIds for branching. */
|
|
172
249
|
currentLeafId = null;
|
|
173
250
|
opts;
|
|
@@ -243,6 +320,23 @@ export class AgentSession {
|
|
|
243
320
|
lspDiagnostics: this.settingsManager.get("lspDiagnostics"),
|
|
244
321
|
getWriteGuardSettings: () => ({
|
|
245
322
|
allowOutsideWorkspaceWrites: this.settingsManager.get("allowOutsideWorkspaceWrites"),
|
|
323
|
+
additionalRoots: this.additionalRoots,
|
|
324
|
+
}),
|
|
325
|
+
getNetworkPolicy: () => ({
|
|
326
|
+
mode: this.settingsManager.get("networkMode"),
|
|
327
|
+
allow: this.settingsManager.get("networkAllow"),
|
|
328
|
+
}),
|
|
329
|
+
getSandboxPolicy: () => ({
|
|
330
|
+
mode: this.settingsManager.get("sandboxMode"),
|
|
331
|
+
// networkAllow always applies. Choosing "allowlist" is the user taking
|
|
332
|
+
// over network policy, so the built-in developer defaults drop out and
|
|
333
|
+
// only their hosts remain reachable.
|
|
334
|
+
allowedDomains: this.settingsManager.get("networkAllow"),
|
|
335
|
+
strictDomains: this.settingsManager.get("networkMode") === "allowlist",
|
|
336
|
+
// Same source of truth as getWriteGuardSettings, so bash and the write
|
|
337
|
+
// tool agree on which roots are writable.
|
|
338
|
+
additionalRoots: this.additionalRoots,
|
|
339
|
+
allowOutsideWorkspaceWrites: this.settingsManager.get("allowOutsideWorkspaceWrites"),
|
|
246
340
|
}),
|
|
247
341
|
authStorage: this.authStorage,
|
|
248
342
|
onFileRead: (filePath) => this.reviewCoverage.recordRead(filePath),
|
|
@@ -259,8 +353,12 @@ export class AgentSession {
|
|
|
259
353
|
getBaseUrl: () => this.baseUrl,
|
|
260
354
|
getCacheKey: () => this.getPromptCacheKey(),
|
|
261
355
|
getMaxPerModel: () => this.settingsManager.get("subagentMaxPerModel"),
|
|
262
|
-
|
|
356
|
+
// A persistent child is already the single allowed fan-out level. Blocking
|
|
357
|
+
// `subagent` here created a timeout sandwich: the nested call could consume
|
|
358
|
+
// the child's entire 10-minute turn budget, discarding all nested results.
|
|
359
|
+
disableSubagents: this.opts.subagentWorker,
|
|
263
360
|
onSubAgentState: (snapshot) => this.eventBus.emit("subagent_state", snapshot),
|
|
361
|
+
notifications: this.notifications,
|
|
264
362
|
// Plan mode: only wired when the host supplies callbacks. The ref is
|
|
265
363
|
// shared so bash/edit/write enforce read-only restrictions live.
|
|
266
364
|
...(this.opts.onEnterPlan || this.opts.onExitPlan
|
|
@@ -282,23 +380,14 @@ export class AgentSession {
|
|
|
282
380
|
this.lspManager = lspManager;
|
|
283
381
|
this.subAgentManager = subAgentManager;
|
|
284
382
|
this.bindManagerCancellation(this.opts.signal);
|
|
285
|
-
// Connect MCP servers
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
//
|
|
294
|
-
// The connect attempt itself can block for up to the per-server connect
|
|
295
|
-
// timeout (~30s) — a slow stdio server such as a first-run
|
|
296
|
-
// `npx -y @playwright/mcp` download stalls here. When the host gates its own
|
|
297
|
-
// readiness on initialize() (the ezcoder-app sidecar can't emit its
|
|
298
|
-
// listening handshake until this resolves), `backgroundMcpConnect` moves the
|
|
299
|
-
// connect off the critical path so the session becomes usable immediately
|
|
300
|
-
// and tools are appended whenever the servers come up.
|
|
301
|
-
this.mcpManager = new MCPClientManager();
|
|
383
|
+
// Connect MCP servers. Child sessions skip user-configured servers to avoid
|
|
384
|
+
// cold-spawning every server per short-lived worker. Background connection
|
|
385
|
+
// keeps slow first-run stdio servers off the host readiness path.
|
|
386
|
+
this.mcpManager = new MCPClientManager({
|
|
387
|
+
catalogCache: this.mcpCatalogCache,
|
|
388
|
+
modernProtocol: this.settingsManager.get("mcpModernProtocol"),
|
|
389
|
+
onElicit: this.opts.onMcpElicit,
|
|
390
|
+
});
|
|
302
391
|
if (this.opts.backgroundMcpConnect) {
|
|
303
392
|
void this.connectMcpServers();
|
|
304
393
|
}
|
|
@@ -306,10 +395,9 @@ export class AgentSession {
|
|
|
306
395
|
await this.connectMcpServers();
|
|
307
396
|
}
|
|
308
397
|
const basePrompt = this.customSystemPrompt ??
|
|
309
|
-
(await buildSystemPrompt(this.cwd, this.skills, false, undefined, this.tools.map((tool) => tool.name), undefined, this.provider));
|
|
398
|
+
(await buildSystemPrompt(this.cwd, this.skills, false, undefined, this.tools.map((tool) => tool.name), undefined, this.provider, this.promptEnvironment()));
|
|
310
399
|
this.baseSystemPrompt = basePrompt;
|
|
311
400
|
this.messages = [{ role: "system", content: this.withSystemPromptTail(basePrompt) }];
|
|
312
|
-
this.syncUltraOrchestrationPrompt();
|
|
313
401
|
// Load or create session. Transient sessions (subagent spawns) never
|
|
314
402
|
// touch the session store — sessionPath stays empty and persistMessage
|
|
315
403
|
// is a no-op so their transcripts can't pollute `ezcoder continue`.
|
|
@@ -462,6 +550,11 @@ export class AgentSession {
|
|
|
462
550
|
if (this.opts.allowedTools && mcpWhitelist) {
|
|
463
551
|
servers = servers.filter((s) => mcpWhitelist.includes(s.name));
|
|
464
552
|
}
|
|
553
|
+
// Seed the catalog from the on-disk cache BEFORE connecting. With
|
|
554
|
+
// `backgroundMcpConnect` the first turns would otherwise run against an
|
|
555
|
+
// empty catalog and tool_search would answer "the catalog is empty" for
|
|
556
|
+
// capabilities that genuinely exist — a wrong answer, not a slow one.
|
|
557
|
+
await this.seedMcpCatalogFromCache(servers);
|
|
465
558
|
const connected = await this.mcpManager.connectAll(servers);
|
|
466
559
|
// Defense-in-depth: even from a whitelisted server, only push tools that
|
|
467
560
|
// pass the allow-list (no-op when there's no allow-list).
|
|
@@ -497,23 +590,146 @@ export class AgentSession {
|
|
|
497
590
|
addMcpTools(mcpTools) {
|
|
498
591
|
if (mcpTools.length === 0)
|
|
499
592
|
return;
|
|
593
|
+
for (const tool of mcpTools) {
|
|
594
|
+
this.liveMcpTools.set(tool.name, tool);
|
|
595
|
+
this.cachedMcpToolServers.delete(tool.name);
|
|
596
|
+
}
|
|
500
597
|
const defer = !this.opts.allowedTools && this.settingsManager.get("deferredMcpTools");
|
|
501
598
|
if (!defer) {
|
|
502
|
-
this.
|
|
599
|
+
this.replaceOrPushTools(mcpTools);
|
|
503
600
|
return;
|
|
504
601
|
}
|
|
505
602
|
this.mcpCatalog ??= new DeferredToolCatalog();
|
|
603
|
+
// `add` is name-keyed, so live definitions replace cached stubs in place.
|
|
506
604
|
this.mcpCatalog.add(mcpTools);
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
605
|
+
// A stub the model already promoted lives in `this.tools`; swap it for the
|
|
606
|
+
// live tool so later calls dispatch directly instead of through the stub.
|
|
607
|
+
this.replaceLivePromotedTools(mcpTools);
|
|
608
|
+
this.ensureToolSearchTool();
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Register `tool_search` once. Promotion of a cached-only entry waits for its
|
|
612
|
+
* server so the model is told immediately when that capability turns out to
|
|
613
|
+
* be unreachable, instead of promoting a tool that fails on first call.
|
|
614
|
+
*/
|
|
615
|
+
ensureToolSearchTool() {
|
|
616
|
+
if (!this.mcpCatalog)
|
|
617
|
+
return;
|
|
618
|
+
if (this.tools.some((t) => t.name === "tool_search"))
|
|
619
|
+
return;
|
|
620
|
+
this.tools.push(createToolSearchTool(this.mcpCatalog, (promoted) => {
|
|
621
|
+
this.tools.push(...promoted);
|
|
622
|
+
}, async (toolName) => {
|
|
623
|
+
if (this.liveMcpTools.has(toolName))
|
|
624
|
+
return undefined;
|
|
625
|
+
const serverName = this.cachedMcpToolServers.get(toolName);
|
|
626
|
+
if (!serverName)
|
|
627
|
+
return undefined;
|
|
628
|
+
const outcome = (await this.mcpManager?.whenConnected(serverName)) ?? {
|
|
629
|
+
ok: false,
|
|
630
|
+
error: "MCP is disabled for this session",
|
|
631
|
+
};
|
|
632
|
+
return outcome.ok
|
|
633
|
+
? { serverName, ok: true }
|
|
634
|
+
: { serverName, ok: false, error: outcome.error };
|
|
635
|
+
}));
|
|
636
|
+
}
|
|
637
|
+
/** Append tools, replacing any same-named entry (cached stub → live tool). */
|
|
638
|
+
replaceOrPushTools(tools) {
|
|
639
|
+
for (const tool of tools) {
|
|
640
|
+
const index = this.tools.findIndex((t) => t.name === tool.name);
|
|
641
|
+
if (index >= 0)
|
|
642
|
+
this.tools[index] = tool;
|
|
643
|
+
else
|
|
644
|
+
this.tools.push(tool);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
/** Swap already-promoted cached stubs for their live equivalents, in place. */
|
|
648
|
+
replaceLivePromotedTools(tools) {
|
|
649
|
+
for (const tool of tools) {
|
|
650
|
+
const index = this.tools.findIndex((t) => t.name === tool.name);
|
|
651
|
+
if (index >= 0)
|
|
652
|
+
this.tools[index] = tool;
|
|
511
653
|
}
|
|
512
654
|
}
|
|
513
655
|
/**
|
|
514
|
-
*
|
|
656
|
+
* Publish cached tool definitions into the deferred catalog so `tool_search`
|
|
657
|
+
* answers correctly on turn 1. A cached stub carries the real name, one-line
|
|
658
|
+
* description and input schema; calling it waits for the live connection and
|
|
659
|
+
* then dispatches against the real client, or returns a clear error when that
|
|
660
|
+
* server ultimately failed. Live tools replace stubs on connect.
|
|
515
661
|
*/
|
|
516
|
-
async
|
|
662
|
+
async seedMcpCatalogFromCache(servers) {
|
|
663
|
+
if (!this.opts.backgroundMcpConnect)
|
|
664
|
+
return;
|
|
665
|
+
if (this.opts.allowedTools || !this.settingsManager.get("deferredMcpTools"))
|
|
666
|
+
return;
|
|
667
|
+
let entries;
|
|
668
|
+
try {
|
|
669
|
+
entries = await this.mcpCatalogCache.entriesFor(servers);
|
|
670
|
+
}
|
|
671
|
+
catch {
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
const stubs = [];
|
|
675
|
+
for (const [serverName, entry] of entries) {
|
|
676
|
+
for (const cached of entry.tools) {
|
|
677
|
+
if (this.liveMcpTools.has(cached.name))
|
|
678
|
+
continue;
|
|
679
|
+
this.cachedMcpToolServers.set(cached.name, serverName);
|
|
680
|
+
stubs.push(this.buildCachedMcpTool(serverName, cached));
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
if (stubs.length === 0)
|
|
684
|
+
return;
|
|
685
|
+
log("INFO", "mcp", "Seeded deferred tool catalog from cache", {
|
|
686
|
+
tools: String(stubs.length),
|
|
687
|
+
servers: String(entries.size),
|
|
688
|
+
});
|
|
689
|
+
this.addCachedMcpTools(stubs);
|
|
690
|
+
}
|
|
691
|
+
/** Catalog-only registration for cached stubs — never marks them live. */
|
|
692
|
+
addCachedMcpTools(stubs) {
|
|
693
|
+
this.mcpCatalog ??= new DeferredToolCatalog();
|
|
694
|
+
this.mcpCatalog.add(stubs);
|
|
695
|
+
this.ensureToolSearchTool();
|
|
696
|
+
}
|
|
697
|
+
buildCachedMcpTool(serverName, cached) {
|
|
698
|
+
return {
|
|
699
|
+
name: cached.name,
|
|
700
|
+
description: cached.description,
|
|
701
|
+
parameters: z.record(z.string(), z.unknown()),
|
|
702
|
+
rawInputSchema: cached.rawInputSchema,
|
|
703
|
+
execute: async (args, context) => {
|
|
704
|
+
const live = this.liveMcpTools.get(cached.name);
|
|
705
|
+
if (live)
|
|
706
|
+
return live.execute(args, context);
|
|
707
|
+
const outcome = (await this.mcpManager?.whenConnected(serverName)) ?? {
|
|
708
|
+
ok: false,
|
|
709
|
+
error: "MCP is disabled for this session",
|
|
710
|
+
};
|
|
711
|
+
if (!outcome.ok) {
|
|
712
|
+
return (`MCP tool ${cached.name} is unavailable: server "${serverName}" did not connect ` +
|
|
713
|
+
`(${outcome.error}). This tool was offered from a cached catalog. ` +
|
|
714
|
+
`Use a different approach or ask the user to check their MCP configuration.`);
|
|
715
|
+
}
|
|
716
|
+
const connected = this.liveMcpTools.get(cached.name);
|
|
717
|
+
if (!connected) {
|
|
718
|
+
return (`MCP tool ${cached.name} no longer exists: server "${serverName}" connected but ` +
|
|
719
|
+
`does not expose it. The cached catalog entry was stale.`);
|
|
720
|
+
}
|
|
721
|
+
return connected.execute(args, context);
|
|
722
|
+
},
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Resolve a `/name [args]` input to the prompt template it expands into, or
|
|
727
|
+
* null when it isn't a prompt-template command for THIS session (an ordinary
|
|
728
|
+
* message, a registry/action command, or any slash input on a non-coder
|
|
729
|
+
* agent). Shared by {@link prompt} and {@link willExpandPromptTemplate} so
|
|
730
|
+
* callers can't drift from the expansion that actually happens.
|
|
731
|
+
*/
|
|
732
|
+
async resolveSlashInput(content) {
|
|
517
733
|
const parsedInput = this.slashCommands.parse(content);
|
|
518
734
|
const coderCommands = this.opts.coderSlashCommands !== false;
|
|
519
735
|
// Non-coder agents only intercept commands registered in their own registry.
|
|
@@ -521,27 +737,53 @@ export class AgentSession {
|
|
|
521
737
|
const parsed = parsedInput && (coderCommands || this.slashCommands.get(parsedInput.name))
|
|
522
738
|
? parsedInput
|
|
523
739
|
: null;
|
|
524
|
-
if (parsed)
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
740
|
+
if (!parsed)
|
|
741
|
+
return null;
|
|
742
|
+
// EZ Coder alone can resolve its prompt-template and project commands.
|
|
743
|
+
const builtinPromptCmd = coderCommands ? getPromptCommand(parsed.name) : undefined;
|
|
744
|
+
const customCmds = coderCommands ? await loadCustomCommands(this.cwd) : [];
|
|
745
|
+
const customPromptCmd = !builtinPromptCmd
|
|
746
|
+
? customCmds.find((c) => c.name === parsed.name)
|
|
747
|
+
: undefined;
|
|
748
|
+
const promptText = builtinPromptCmd?.prompt ?? customPromptCmd?.prompt;
|
|
749
|
+
if (!promptText)
|
|
750
|
+
return { kind: "command" };
|
|
751
|
+
return {
|
|
752
|
+
kind: "template",
|
|
753
|
+
fullPrompt: parsed.args
|
|
754
|
+
? `${promptText}\n\n## User Instructions\n\n${parsed.args}`
|
|
755
|
+
: promptText,
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Whether {@link prompt} would expand this input into a template body and
|
|
760
|
+
* persist it as a user message. Hosts use it to record the typed `/name` for
|
|
761
|
+
* transcript restore — gating on anything looser risks tagging an unrelated
|
|
762
|
+
* message when the command turns out NOT to expand.
|
|
763
|
+
*/
|
|
764
|
+
async willExpandPromptTemplate(content) {
|
|
765
|
+
return (await this.resolveSlashInput(content))?.kind === "template";
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Process user input. Handles slash commands or runs agent loop.
|
|
769
|
+
*/
|
|
770
|
+
async prompt(content, provenance = {
|
|
771
|
+
source: "human",
|
|
772
|
+
kind: "prompt",
|
|
773
|
+
visibility: "transcript",
|
|
774
|
+
}, options = {}) {
|
|
775
|
+
await this.adoptDeferredCheckpointBeforePrompt();
|
|
776
|
+
const slash = await this.resolveSlashInput(content);
|
|
777
|
+
if (slash?.kind === "template") {
|
|
778
|
+
// Prompt templates remain human-originated: the user invoked the command.
|
|
779
|
+
const userMessage = { role: "user", content: slash.fullPrompt, provenance };
|
|
780
|
+
this.messages.push(userMessage);
|
|
781
|
+
await this.persistMessage(userMessage);
|
|
782
|
+
this.lastPersistedIndex = this.messages.length;
|
|
783
|
+
await this.runLoop(options);
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
if (slash?.kind === "command") {
|
|
545
787
|
const cmdContext = this.createSlashCommandContext();
|
|
546
788
|
const result = await this.slashCommands.execute(content, cmdContext);
|
|
547
789
|
if (result) {
|
|
@@ -550,11 +792,11 @@ export class AgentSession {
|
|
|
550
792
|
return;
|
|
551
793
|
}
|
|
552
794
|
// Push user message
|
|
553
|
-
const userMessage = { role: "user", content };
|
|
795
|
+
const userMessage = { role: "user", content, provenance };
|
|
554
796
|
this.messages.push(userMessage);
|
|
555
797
|
await this.persistMessage(userMessage);
|
|
556
798
|
this.lastPersistedIndex = this.messages.length;
|
|
557
|
-
await this.runLoop();
|
|
799
|
+
await this.runLoop(options);
|
|
558
800
|
}
|
|
559
801
|
/**
|
|
560
802
|
* Prompt with multimodal attachments (images / videos) alongside optional
|
|
@@ -564,10 +806,15 @@ export class AgentSession {
|
|
|
564
806
|
* attachments are always a direct conversational turn.
|
|
565
807
|
*/
|
|
566
808
|
async promptWithAttachments(text, attachments) {
|
|
809
|
+
await this.adoptDeferredCheckpointBeforePrompt();
|
|
567
810
|
const parts = this.buildAttachmentParts(text, attachments);
|
|
568
811
|
if (parts.length === 0)
|
|
569
812
|
return;
|
|
570
|
-
const userMessage = {
|
|
813
|
+
const userMessage = {
|
|
814
|
+
role: "user",
|
|
815
|
+
content: parts,
|
|
816
|
+
provenance: { source: "human", kind: "prompt", visibility: "transcript" },
|
|
817
|
+
};
|
|
571
818
|
this.messages.push(userMessage);
|
|
572
819
|
await this.persistMessage(userMessage);
|
|
573
820
|
this.lastPersistedIndex = this.messages.length;
|
|
@@ -656,9 +903,12 @@ export class AgentSession {
|
|
|
656
903
|
this.hookFileEditCounts.clear();
|
|
657
904
|
this.hookToolCalls.clear();
|
|
658
905
|
this.reviewCoverage.reset();
|
|
906
|
+
this.reviewCoverageInjected = 0;
|
|
659
907
|
this.idealReviewPhase = "idle";
|
|
660
908
|
this.loopBreakInjected = 0;
|
|
661
909
|
this.regroundingInjected = false;
|
|
910
|
+
this.runStartedAt = Date.now();
|
|
911
|
+
this.processGateInjected = 0;
|
|
662
912
|
this.compactionOccurred = false;
|
|
663
913
|
this.originalRequest = originalRequest;
|
|
664
914
|
}
|
|
@@ -709,28 +959,94 @@ export class AgentSession {
|
|
|
709
959
|
}
|
|
710
960
|
}
|
|
711
961
|
await this.persistTurnMetric(event);
|
|
962
|
+
// The assistant message for this turn is now in the array.
|
|
963
|
+
await this.flushPendingMessages();
|
|
964
|
+
break;
|
|
965
|
+
case "checkpoint":
|
|
966
|
+
// Tool results for this step are in the array and their side effects
|
|
967
|
+
// already hit the filesystem. Flushing here is what makes a crash lose
|
|
968
|
+
// at most the in-flight step instead of the entire turn.
|
|
969
|
+
await this.flushPendingMessages();
|
|
712
970
|
break;
|
|
713
971
|
}
|
|
714
972
|
}
|
|
715
973
|
/**
|
|
716
|
-
*
|
|
717
|
-
*
|
|
718
|
-
*
|
|
719
|
-
*
|
|
974
|
+
* Append every message added since the last flush to the session file.
|
|
975
|
+
*
|
|
976
|
+
* Safe to call mid-run: `agentLoop` mutates its message array in place, so
|
|
977
|
+
* the slice from `lastPersistedIndex` is always exactly the new tail.
|
|
978
|
+
* Transient sessions (subagent spawns) have no session file and
|
|
979
|
+
* `persistMessage` no-ops for them.
|
|
980
|
+
*
|
|
981
|
+
* Persistence failures must never take down a live run — the post-loop flush
|
|
982
|
+
* retries the same range.
|
|
983
|
+
*/
|
|
984
|
+
async flushPendingMessages() {
|
|
985
|
+
const messages = this.activeLoopMessages ?? this.messages;
|
|
986
|
+
if (this.lastPersistedIndex >= messages.length)
|
|
987
|
+
return;
|
|
988
|
+
const target = messages.length;
|
|
989
|
+
try {
|
|
990
|
+
for (let i = this.lastPersistedIndex; i < target; i++) {
|
|
991
|
+
await this.persistMessage(messages[i]);
|
|
992
|
+
this.lastPersistedIndex = i + 1;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
catch (err) {
|
|
996
|
+
log("WARN", "session", "Failed to flush messages at a step boundary", {
|
|
997
|
+
error: err instanceof Error ? err.message : String(err),
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Mid-loop steering hook: delivers pushed notifications and queued user
|
|
1003
|
+
* steering, fires the loop-breaker when the agent looks stuck, then
|
|
1004
|
+
* post-compaction re-grounding. At most one loop-break/re-grounding per run.
|
|
1005
|
+
* Mirrors the TUI's getSteeringMessages ordering.
|
|
720
1006
|
*/
|
|
721
1007
|
getHookSteeringMessages() {
|
|
1008
|
+
// Push notifications: a child that finished or a background build that
|
|
1009
|
+
// exited is new *fact*, not a correction, and it is cheap (bounded to 1 KiB
|
|
1010
|
+
// per drain). Drained above the loop-break checks so the agent can act on
|
|
1011
|
+
// it in the very next turn instead of discovering it at the pre-stop
|
|
1012
|
+
// completion gate — but it never displaces user steering, which rides out
|
|
1013
|
+
// in the same batch when both are pending.
|
|
1014
|
+
const notified = this.notifications.drain();
|
|
1015
|
+
const notificationMessage = notified.length > 0
|
|
1016
|
+
? {
|
|
1017
|
+
role: "user",
|
|
1018
|
+
content: buildNotificationSteeringText(notified.map((entry) => entry.text)),
|
|
1019
|
+
provenance: { source: "runtime", kind: "notification", visibility: "hidden" },
|
|
1020
|
+
}
|
|
1021
|
+
: null;
|
|
1022
|
+
if (notificationMessage) {
|
|
1023
|
+
log("INFO", "notifications", "Injecting pushed notifications", {
|
|
1024
|
+
count: String(notified.length),
|
|
1025
|
+
chars: String(notificationMessage.content.length),
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
722
1028
|
// User steering wins: drain any messages queued during this run first so the
|
|
723
1029
|
// agent sees them mid-loop instead of after it stops.
|
|
724
1030
|
if (this.userQueue.length > 0) {
|
|
725
1031
|
const queued = this.userQueue.splice(0);
|
|
1032
|
+
// The agent has now consumed these. Announce the new depth immediately so
|
|
1033
|
+
// clients can drop the "queued" affordance at the turn boundary rather
|
|
1034
|
+
// than holding it until the whole run ends — the message is already in
|
|
1035
|
+
// the loop, and showing it as still-pending for minutes is a lie.
|
|
1036
|
+
this.eventBus.emit("queue_drained", { count: this.userQueue.length });
|
|
726
1037
|
// Frame each queued item as concurrent steering — without this wrapper
|
|
727
1038
|
// the model treats a mid-run message as a fresh request that supersedes
|
|
728
1039
|
// the original task and silently drops it. ONE message per queued item
|
|
729
1040
|
// (not merged): each persists as its own user message, so a resumed
|
|
730
1041
|
// session shows the same number of bubbles the live run did.
|
|
731
|
-
|
|
1042
|
+
const steeringMessages = queued.map((m) => {
|
|
1043
|
+
const provenance = {
|
|
1044
|
+
source: "human",
|
|
1045
|
+
kind: "steering",
|
|
1046
|
+
visibility: "transcript",
|
|
1047
|
+
};
|
|
732
1048
|
if (m.attachments.length === 0) {
|
|
733
|
-
return { role: "user", content: wrapSteeringText(m.text) };
|
|
1049
|
+
return { role: "user", content: wrapSteeringText(m.text), provenance };
|
|
734
1050
|
}
|
|
735
1051
|
// Queued attachments ride the same native-block path as a non-queued
|
|
736
1052
|
// attachment prompt, prefixed with the steering framing.
|
|
@@ -738,9 +1054,12 @@ export class AgentSession {
|
|
|
738
1054
|
{ type: "text", text: STEERING_PREFIX },
|
|
739
1055
|
...this.buildAttachmentParts(m.text, m.attachments),
|
|
740
1056
|
];
|
|
741
|
-
return { role: "user", content: parts };
|
|
1057
|
+
return { role: "user", content: parts, provenance };
|
|
742
1058
|
});
|
|
1059
|
+
return notificationMessage ? [...steeringMessages, notificationMessage] : steeringMessages;
|
|
743
1060
|
}
|
|
1061
|
+
if (notificationMessage)
|
|
1062
|
+
return [notificationMessage];
|
|
744
1063
|
if (this.opts.selfCorrectionHooks === false)
|
|
745
1064
|
return null;
|
|
746
1065
|
if (!this.settingsManager.get("idealReviewEnabled"))
|
|
@@ -782,6 +1101,39 @@ export class AgentSession {
|
|
|
782
1101
|
}
|
|
783
1102
|
return null;
|
|
784
1103
|
}
|
|
1104
|
+
/**
|
|
1105
|
+
* Turn-budget extension gate. The loop consults this instead of stopping
|
|
1106
|
+
* mid-task when it exhausts `maxTurns`. Grant ONLY on evidence of progress —
|
|
1107
|
+
* handing more turns to a spinning agent just buys it more tokens to spin
|
|
1108
|
+
* with — so reuse the same stuck signals that drive the loop-breaker.
|
|
1109
|
+
*/
|
|
1110
|
+
shouldExtendTurnBudget(ctx) {
|
|
1111
|
+
const refusals = [];
|
|
1112
|
+
const stuck = evaluateLoopBreak({
|
|
1113
|
+
consecutiveFailures: this.hookConsecutiveFailures,
|
|
1114
|
+
repeatedNoProgressCalls: this.hookRepeatedNoProgressCalls,
|
|
1115
|
+
textRepetitionDetected: detectTextRepetition(this.hookText),
|
|
1116
|
+
...(this.hookCyclicPattern ? { cyclicPattern: this.hookCyclicPattern } : {}),
|
|
1117
|
+
});
|
|
1118
|
+
if (stuck.shouldBreak)
|
|
1119
|
+
refusals.push(...stuck.reasons);
|
|
1120
|
+
// Stage 2 means the loop-breaker already detected spinning twice and told
|
|
1121
|
+
// the agent to stop and report. Do not overrule that with more turns.
|
|
1122
|
+
if (this.loopBreakInjected >= 2)
|
|
1123
|
+
refusals.push("loop-breaker already escalated");
|
|
1124
|
+
const { toolCalls, toolFailures } = this.hookStats;
|
|
1125
|
+
if (toolCalls > 0 && toolFailures / toolCalls > TURN_EXTENSION_MAX_FAILURE_RATIO) {
|
|
1126
|
+
refusals.push(`${toolFailures}/${toolCalls} tool calls failed`);
|
|
1127
|
+
}
|
|
1128
|
+
const granted = refusals.length === 0;
|
|
1129
|
+
log("INFO", "turn-budget", granted ? "Extending turn budget" : "Refusing turn-budget extension", {
|
|
1130
|
+
turn: String(ctx.turn),
|
|
1131
|
+
maxTurns: String(ctx.maxTurns),
|
|
1132
|
+
extension: String(ctx.extension),
|
|
1133
|
+
...(granted ? {} : { reasons: refusals.join(", ") }),
|
|
1134
|
+
});
|
|
1135
|
+
return granted;
|
|
1136
|
+
}
|
|
785
1137
|
/**
|
|
786
1138
|
* Pre-stop Ideal review phase machine. Once review starts, completion is
|
|
787
1139
|
* blocked until harness-owned post-injection reads cover every changed file.
|
|
@@ -790,6 +1142,17 @@ export class AgentSession {
|
|
|
790
1142
|
const childCompletionFollowUp = buildSubAgentCompletionFollowUp(this.subAgentManager);
|
|
791
1143
|
if (childCompletionFollowUp)
|
|
792
1144
|
return childCompletionFollowUp;
|
|
1145
|
+
// Background processes started this run and never read block completion:
|
|
1146
|
+
// their progress/exit checkpoints only land on the steering path, which an
|
|
1147
|
+
// agent about to stop never reaches.
|
|
1148
|
+
const processFollowUp = buildProcessCompletionFollowUp(this.processManager?.list() ?? [], this.runStartedAt, this.processGateInjected);
|
|
1149
|
+
if (processFollowUp) {
|
|
1150
|
+
this.processGateInjected += 1;
|
|
1151
|
+
log("INFO", "process-gate", "Injecting background-process completion gate", {
|
|
1152
|
+
injected: String(this.processGateInjected),
|
|
1153
|
+
});
|
|
1154
|
+
return processFollowUp;
|
|
1155
|
+
}
|
|
793
1156
|
if (this.opts.selfCorrectionHooks === false || this.idealReviewSuppressed)
|
|
794
1157
|
return null;
|
|
795
1158
|
if (this.idealReviewPhase === "reviewing") {
|
|
@@ -802,9 +1165,20 @@ export class AgentSession {
|
|
|
802
1165
|
lspMissing: lspEvidence.missing,
|
|
803
1166
|
});
|
|
804
1167
|
if (coverage.missing.length > 0) {
|
|
805
|
-
|
|
806
|
-
this.
|
|
807
|
-
|
|
1168
|
+
if (this.reviewCoverageInjected < MAX_REVIEW_COVERAGE_INJECTIONS) {
|
|
1169
|
+
this.reviewCoverageInjected += 1;
|
|
1170
|
+
return [
|
|
1171
|
+
this.withReviewLspEvidence(buildReviewCoverageMessage(coverage.missing), lspEvidence),
|
|
1172
|
+
];
|
|
1173
|
+
}
|
|
1174
|
+
// Budget spent: close the gate so the run cannot spin on a file that
|
|
1175
|
+
// never becomes readable, and require the gap be reported to the user.
|
|
1176
|
+
this.idealReviewPhase = "complete";
|
|
1177
|
+
log("INFO", "ideal", "Ideal review coverage escalated after retry budget", {
|
|
1178
|
+
injected: String(this.reviewCoverageInjected),
|
|
1179
|
+
missing: coverage.missing,
|
|
1180
|
+
});
|
|
1181
|
+
return [buildReviewCoverageEscalationMessage(coverage.missing)];
|
|
808
1182
|
}
|
|
809
1183
|
this.idealReviewPhase = "complete";
|
|
810
1184
|
return null;
|
|
@@ -862,10 +1236,14 @@ export class AgentSession {
|
|
|
862
1236
|
: []),
|
|
863
1237
|
"Do not describe those files as compiler-clean without other evidence.",
|
|
864
1238
|
];
|
|
865
|
-
return {
|
|
1239
|
+
return {
|
|
1240
|
+
role: "user",
|
|
1241
|
+
provenance: message.provenance,
|
|
1242
|
+
content: `${String(message.content)}\n\n${notes.join(" ")}`,
|
|
1243
|
+
};
|
|
866
1244
|
}
|
|
867
1245
|
/** Auto-compact if needed, run agent loop with auth retry, and persist messages. */
|
|
868
|
-
async runLoop() {
|
|
1246
|
+
async runLoop(options = {}) {
|
|
869
1247
|
this.refreshSystemPromptTail();
|
|
870
1248
|
// One-shot cache-key marker per session so turn_end cacheRead numbers
|
|
871
1249
|
// in the log can be traced back to a specific routing namespace —
|
|
@@ -902,7 +1280,14 @@ export class AgentSession {
|
|
|
902
1280
|
provider: this.provider,
|
|
903
1281
|
accountId: creds.accountId,
|
|
904
1282
|
});
|
|
905
|
-
const
|
|
1283
|
+
const policy = resolveCompactionPolicy({
|
|
1284
|
+
provider: this.provider,
|
|
1285
|
+
model: this.model,
|
|
1286
|
+
contextWindow,
|
|
1287
|
+
threshold: this.settingsManager.get("compactThreshold"),
|
|
1288
|
+
accountId: creds.accountId,
|
|
1289
|
+
approvedPlanPath: this.approvedPlanPath,
|
|
1290
|
+
});
|
|
906
1291
|
let activeTokens;
|
|
907
1292
|
if (this.providerContext) {
|
|
908
1293
|
const anchorIndex = this.messages.lastIndexOf(this.providerContext.anchor);
|
|
@@ -916,9 +1301,17 @@ export class AgentSession {
|
|
|
916
1301
|
this.providerContext = null;
|
|
917
1302
|
}
|
|
918
1303
|
}
|
|
919
|
-
|
|
1304
|
+
log("INFO", "compaction", "Pre-run compaction decision", {
|
|
1305
|
+
provider: this.provider,
|
|
1306
|
+
model: this.model,
|
|
1307
|
+
transport: this.provider === "openai" && creds.accountId ? "codex_oauth" : "public_api",
|
|
1308
|
+
contextWindow: String(contextWindow),
|
|
1309
|
+
activeTokens: activeTokens === undefined ? "estimated" : String(activeTokens),
|
|
1310
|
+
triggerLimit: String(policy.targetTokens),
|
|
1311
|
+
});
|
|
1312
|
+
if (shouldCompact(this.messages, contextWindow, policy.threshold, activeTokens)) {
|
|
920
1313
|
try {
|
|
921
|
-
await this.compact(creds);
|
|
1314
|
+
await this.compact(creds, "automatic");
|
|
922
1315
|
if (this.lastCompactionCompacted) {
|
|
923
1316
|
// Re-grounding hook keys off this — the context was just summarized.
|
|
924
1317
|
this.compactionOccurred = true;
|
|
@@ -944,10 +1337,11 @@ export class AgentSession {
|
|
|
944
1337
|
const generator = agentLoop(loopMessages, {
|
|
945
1338
|
provider: this.provider,
|
|
946
1339
|
model: this.model,
|
|
947
|
-
tools: this.tools,
|
|
948
|
-
webSearch:
|
|
1340
|
+
tools: options.disableTools ? [] : this.tools,
|
|
1341
|
+
webSearch: !options.disableTools,
|
|
949
1342
|
maxTokens: this.maxTokens,
|
|
950
1343
|
maxTurns: this.opts.maxTurns,
|
|
1344
|
+
maxTurnExtensions: this.opts.maxTurnExtensions,
|
|
951
1345
|
thinking: this.thinkingLevel,
|
|
952
1346
|
apiKey,
|
|
953
1347
|
baseUrl: effectiveBaseUrl,
|
|
@@ -976,6 +1370,7 @@ export class AgentSession {
|
|
|
976
1370
|
// polled mid-loop; the ideal review is polled when the agent would stop.
|
|
977
1371
|
getSteeringMessages: () => this.getHookSteeringMessages(),
|
|
978
1372
|
getFollowUpMessages: () => this.getHookFollowUpMessages(),
|
|
1373
|
+
onTurnBudgetExhausted: (ctx) => this.shouldExtendTurnBudget(ctx),
|
|
979
1374
|
// Check authoritative provider usage before every model/tool step.
|
|
980
1375
|
// Forced overflow recovery bypasses settings and cooldown; proactive
|
|
981
1376
|
// checks honor both and estimate only messages unseen by the provider.
|
|
@@ -1002,6 +1397,7 @@ export class AgentSession {
|
|
|
1002
1397
|
this.providerContext = null;
|
|
1003
1398
|
log("INFO", "compaction", "Pruned stale tool outputs", {
|
|
1004
1399
|
prunedResults: String(pruneResult.prunedResults),
|
|
1400
|
+
compactedToolCalls: String(pruneResult.compactedToolCalls),
|
|
1005
1401
|
freedTokens: String(pruneResult.freedTokens),
|
|
1006
1402
|
});
|
|
1007
1403
|
}
|
|
@@ -1026,12 +1422,27 @@ export class AgentSession {
|
|
|
1026
1422
|
provider: this.provider,
|
|
1027
1423
|
accountId,
|
|
1028
1424
|
});
|
|
1029
|
-
const
|
|
1425
|
+
const policy = resolveCompactionPolicy({
|
|
1426
|
+
provider: this.provider,
|
|
1427
|
+
model: this.model,
|
|
1428
|
+
contextWindow,
|
|
1429
|
+
threshold: this.settingsManager.get("compactThreshold"),
|
|
1430
|
+
accountId,
|
|
1431
|
+
approvedPlanPath: this.approvedPlanPath,
|
|
1432
|
+
});
|
|
1030
1433
|
const activeTokens = calculateActiveContextTokens(messages, {
|
|
1031
1434
|
usage,
|
|
1032
1435
|
pendingMessages,
|
|
1033
1436
|
});
|
|
1034
|
-
|
|
1437
|
+
log("INFO", "compaction", "In-flight compaction decision", {
|
|
1438
|
+
provider: this.provider,
|
|
1439
|
+
model: this.model,
|
|
1440
|
+
transport: this.provider === "openai" && accountId ? "codex_oauth" : "public_api",
|
|
1441
|
+
contextWindow: String(contextWindow),
|
|
1442
|
+
activeTokens: String(activeTokens),
|
|
1443
|
+
triggerLimit: String(policy.targetTokens),
|
|
1444
|
+
});
|
|
1445
|
+
if (!shouldCompact(messages, contextWindow, policy.threshold, activeTokens))
|
|
1035
1446
|
return messages;
|
|
1036
1447
|
}
|
|
1037
1448
|
// compact() operates on this.messages, while an earlier transform may
|
|
@@ -1044,7 +1455,7 @@ export class AgentSession {
|
|
|
1044
1455
|
accountId,
|
|
1045
1456
|
projectId,
|
|
1046
1457
|
baseUrl: effectiveBaseUrl,
|
|
1047
|
-
});
|
|
1458
|
+
}, force ? "forced" : "automatic");
|
|
1048
1459
|
}
|
|
1049
1460
|
catch (error) {
|
|
1050
1461
|
this.messages = messages;
|
|
@@ -1064,9 +1475,15 @@ export class AgentSession {
|
|
|
1064
1475
|
return this.messages;
|
|
1065
1476
|
},
|
|
1066
1477
|
});
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1478
|
+
this.activeLoopMessages = loopMessages;
|
|
1479
|
+
try {
|
|
1480
|
+
for await (const event of generator) {
|
|
1481
|
+
await this.trackHookEvent(event);
|
|
1482
|
+
this.eventBus.forwardAgentEvent(event);
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
finally {
|
|
1486
|
+
this.activeLoopMessages = null;
|
|
1070
1487
|
}
|
|
1071
1488
|
};
|
|
1072
1489
|
const clearInvalidStaticApiKey = async (error) => {
|
|
@@ -1145,7 +1562,9 @@ export class AgentSession {
|
|
|
1145
1562
|
}
|
|
1146
1563
|
}
|
|
1147
1564
|
this.messages = loopMessages;
|
|
1148
|
-
//
|
|
1565
|
+
// Backstop. Step-boundary flushes (checkpoint / turn_end) normally leave
|
|
1566
|
+
// nothing here, but this still catches messages appended after the last
|
|
1567
|
+
// checkpoint — an aborted final step, or a flush that failed mid-run.
|
|
1149
1568
|
for (let i = this.lastPersistedIndex; i < this.messages.length; i++) {
|
|
1150
1569
|
await this.persistMessage(this.messages[i]);
|
|
1151
1570
|
}
|
|
@@ -1153,6 +1572,11 @@ export class AgentSession {
|
|
|
1153
1572
|
}
|
|
1154
1573
|
async switchModel(provider, model) {
|
|
1155
1574
|
const prevProvider = this.provider;
|
|
1575
|
+
const prevModel = this.model;
|
|
1576
|
+
// Diff gate: a "switch" to the model already in use is not state change.
|
|
1577
|
+
// Recording it anyway would write a redundant marker and a redundant
|
|
1578
|
+
// history note on every no-op re-selection from the UI.
|
|
1579
|
+
const changed = model !== prevModel || (Boolean(provider) && provider !== prevProvider);
|
|
1156
1580
|
if (provider)
|
|
1157
1581
|
this.provider = provider;
|
|
1158
1582
|
this.model = model;
|
|
@@ -1161,7 +1585,6 @@ export class AgentSession {
|
|
|
1161
1585
|
// the live selection after an in-session model switch.
|
|
1162
1586
|
this.opts.provider = this.provider;
|
|
1163
1587
|
this.opts.model = this.model;
|
|
1164
|
-
this.syncUltraOrchestrationPrompt();
|
|
1165
1588
|
setEstimatorModel(model);
|
|
1166
1589
|
// maxTokens must follow the active model — it was frozen at the boot
|
|
1167
1590
|
// model's `maxOutputTokens` in the constructor, so without this a session
|
|
@@ -1182,6 +1605,27 @@ export class AgentSession {
|
|
|
1182
1605
|
const newReadTool = this.rebuildReadTool(model);
|
|
1183
1606
|
this.tools = this.tools.map((t) => (t.name === "read" ? newReadTool : t));
|
|
1184
1607
|
}
|
|
1608
|
+
// Model-dependent guidance lives in the uncached tail, so this rewrites
|
|
1609
|
+
// only the bytes after the cache marker — the cached prefix survives the
|
|
1610
|
+
// switch intact and the next turn still reads from cache.
|
|
1611
|
+
this.refreshSystemPromptTail();
|
|
1612
|
+
if (changed) {
|
|
1613
|
+
// Durable, replayable record of which model produced which segment. Rides
|
|
1614
|
+
// the existing app-marker path (parentId null, anchored by message count)
|
|
1615
|
+
// so a resumed session reconstructs the switch without the LLM seeing an
|
|
1616
|
+
// extra transcript row.
|
|
1617
|
+
await this.persistAppMarker("model_switch", {
|
|
1618
|
+
from: prevModel,
|
|
1619
|
+
to: this.model,
|
|
1620
|
+
provider: this.provider,
|
|
1621
|
+
fromProvider: prevProvider,
|
|
1622
|
+
}).catch((error) => {
|
|
1623
|
+
log("WARN", "session", "Failed to persist model-switch marker", {
|
|
1624
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1625
|
+
});
|
|
1626
|
+
});
|
|
1627
|
+
this.appendModelSwitchNote(prevModel, prevProvider);
|
|
1628
|
+
}
|
|
1185
1629
|
// Update provider-specific tools when provider changes
|
|
1186
1630
|
if (provider && provider !== prevProvider) {
|
|
1187
1631
|
// Add/remove client-side web_search tool based on provider.
|
|
@@ -1193,7 +1637,10 @@ export class AgentSession {
|
|
|
1193
1637
|
}
|
|
1194
1638
|
else if (this.provider !== "anthropic" && !hasWebSearch) {
|
|
1195
1639
|
// Switching FROM anthropic — add client-side web_search
|
|
1196
|
-
this.tools.push(createWebSearchTool()
|
|
1640
|
+
this.tools.push(createWebSearchTool(() => ({
|
|
1641
|
+
mode: this.settingsManager.get("networkMode"),
|
|
1642
|
+
allow: this.settingsManager.get("networkAllow"),
|
|
1643
|
+
})));
|
|
1197
1644
|
}
|
|
1198
1645
|
// Reconnect MCP servers ONLY when GLM is involved on either side — GLM
|
|
1199
1646
|
// is the only provider with a different server set (Z.AI tools), so a
|
|
@@ -1225,6 +1672,8 @@ export class AgentSession {
|
|
|
1225
1672
|
// re-adding. Some tools may already have been promoted out of the catalog.
|
|
1226
1673
|
this.tools = this.tools.filter((t) => !t.name.startsWith("mcp__"));
|
|
1227
1674
|
this.mcpCatalog?.removeWhere((name) => name.startsWith("mcp__"));
|
|
1675
|
+
this.liveMcpTools.clear();
|
|
1676
|
+
this.cachedMcpToolServers.clear();
|
|
1228
1677
|
this.addMcpTools(mcpTools);
|
|
1229
1678
|
}
|
|
1230
1679
|
catch (err) {
|
|
@@ -1233,7 +1682,93 @@ export class AgentSession {
|
|
|
1233
1682
|
}
|
|
1234
1683
|
}
|
|
1235
1684
|
}
|
|
1236
|
-
|
|
1685
|
+
/**
|
|
1686
|
+
* Record the switch as its own trailing message at the point it happened,
|
|
1687
|
+
* rather than by rewriting the system prompt. Two reasons: the system prompt
|
|
1688
|
+
* is the cached prefix and rewriting it costs a full cache write, and a
|
|
1689
|
+
* resumed transcript can only attribute output to the right model if the
|
|
1690
|
+
* switch sits in message order.
|
|
1691
|
+
*
|
|
1692
|
+
* Skipped before the conversation starts (nothing to disambiguate) and while
|
|
1693
|
+
* an assistant turn has unresolved tool calls, where inserting a user message
|
|
1694
|
+
* would break tool_use/tool_result pairing.
|
|
1695
|
+
*/
|
|
1696
|
+
appendModelSwitchNote(prevModel, prevProvider) {
|
|
1697
|
+
const last = this.messages[this.messages.length - 1];
|
|
1698
|
+
if (!last || last.role === "system")
|
|
1699
|
+
return;
|
|
1700
|
+
if (last.role === "assistant" && hasUnresolvedToolCalls(last))
|
|
1701
|
+
return;
|
|
1702
|
+
const from = prevProvider === this.provider ? prevModel : `${prevProvider}/${prevModel}`;
|
|
1703
|
+
const to = prevProvider === this.provider ? this.model : `${this.provider}/${this.model}`;
|
|
1704
|
+
this.messages.push({
|
|
1705
|
+
role: "user",
|
|
1706
|
+
content: `[Model switched from ${from} to ${to}. Everything above was produced under the previous model; continue the current task from here.]`,
|
|
1707
|
+
provenance: { source: "runtime", kind: "model_switch", visibility: "hidden" },
|
|
1708
|
+
});
|
|
1709
|
+
}
|
|
1710
|
+
async adoptCompactionCheckpoint(loaded) {
|
|
1711
|
+
const systemMessage = this.messages[0];
|
|
1712
|
+
const loadedMessages = this.sessionManager.getMessages(loaded.entries, loaded.header.leafId);
|
|
1713
|
+
this.messages = [systemMessage, ...loadedMessages];
|
|
1714
|
+
this.sessionId = loaded.header.id;
|
|
1715
|
+
this.conversationId = loaded.header.conversationId ?? loaded.header.id;
|
|
1716
|
+
this.checkpointGeneration = loaded.header.generation ?? 0;
|
|
1717
|
+
this.currentLeafId = loaded.header.leafId;
|
|
1718
|
+
this.setSessionPath(loaded.path);
|
|
1719
|
+
this.nolanTurns = this.sessionManager.getNolanTurns(loaded.entries, loaded.header.leafId);
|
|
1720
|
+
this.autopilotMarkers = this.sessionManager.getAutopilotMarkers(loaded.entries, loaded.header.leafId);
|
|
1721
|
+
this.appMarkers = this.sessionManager.getAppMarkers(loaded.entries, loaded.header.leafId);
|
|
1722
|
+
this.turnMetrics = this.sessionManager.getTurnMetrics(loaded.entries);
|
|
1723
|
+
this.lastPersistedIndex = this.messages.length;
|
|
1724
|
+
this.providerContext = null;
|
|
1725
|
+
await this.subAgentManager?.rebindParentSession(this.sessionId);
|
|
1726
|
+
}
|
|
1727
|
+
/** Canonicalize a deferred restore before a new prompt can fork stale history. */
|
|
1728
|
+
async adoptDeferredCheckpointBeforePrompt() {
|
|
1729
|
+
if (!this.deferredCompactionPending || !this.conversationId)
|
|
1730
|
+
return;
|
|
1731
|
+
this.deferredCompactionPending = false;
|
|
1732
|
+
const canonicalPath = await this.sessionManager.resolveCanonicalSession(this.conversationId, this.cwd);
|
|
1733
|
+
if (!canonicalPath || canonicalPath === this.sessionPath)
|
|
1734
|
+
return;
|
|
1735
|
+
await this.adoptCompactionCheckpoint(await this.sessionManager.load(canonicalPath));
|
|
1736
|
+
}
|
|
1737
|
+
async persistCompactionCheckpoint(sourceFingerprint, result) {
|
|
1738
|
+
const parentSessionId = this.sessionId || undefined;
|
|
1739
|
+
const session = await this.sessionManager.create(this.cwd, this.provider, this.model, {
|
|
1740
|
+
conversationId: this.conversationId || undefined,
|
|
1741
|
+
generation: this.checkpointGeneration + 1,
|
|
1742
|
+
parentSessionId,
|
|
1743
|
+
sourceFingerprint,
|
|
1744
|
+
retainedMessageCount: result.retainedCount === 0
|
|
1745
|
+
? 0
|
|
1746
|
+
: this.messages
|
|
1747
|
+
.slice(-result.retainedCount)
|
|
1748
|
+
.filter((message) => getHistoryMessageVisibility(message) !== "hidden").length,
|
|
1749
|
+
preview: this.sessionPreview || undefined,
|
|
1750
|
+
});
|
|
1751
|
+
this.sessionId = session.id;
|
|
1752
|
+
this.checkpointGeneration = session.header.generation ?? 0;
|
|
1753
|
+
this.conversationId = session.header.conversationId ?? session.id;
|
|
1754
|
+
this.currentLeafId = null;
|
|
1755
|
+
this.setSessionPath(session.path);
|
|
1756
|
+
await this.subAgentManager?.rebindParentSession(this.sessionId);
|
|
1757
|
+
for (const message of this.messages) {
|
|
1758
|
+
if (message.role !== "system")
|
|
1759
|
+
await this.persistMessage(message);
|
|
1760
|
+
}
|
|
1761
|
+
this.lastPersistedIndex = this.messages.length;
|
|
1762
|
+
await this.rePersistTurnMetrics();
|
|
1763
|
+
await this.rePersistNolanTurns();
|
|
1764
|
+
await this.rePersistAutopilotMarkers();
|
|
1765
|
+
await this.rePersistAppMarkers();
|
|
1766
|
+
await this.persistAppMarker("compaction", {
|
|
1767
|
+
originalCount: result.originalCount,
|
|
1768
|
+
newCount: result.newCount,
|
|
1769
|
+
});
|
|
1770
|
+
}
|
|
1771
|
+
async compact(existingCredentials, mode = "manual") {
|
|
1237
1772
|
this.lastCompactionCompacted = false;
|
|
1238
1773
|
const creds = existingCredentials ??
|
|
1239
1774
|
(await this.authStorage.resolveCredentials(this.provider, {
|
|
@@ -1243,71 +1778,141 @@ export class AgentSession {
|
|
|
1243
1778
|
provider: this.provider,
|
|
1244
1779
|
accountId: creds.accountId,
|
|
1245
1780
|
});
|
|
1246
|
-
|
|
1247
|
-
const result = await compact(this.messages, {
|
|
1781
|
+
const policy = resolveCompactionPolicy({
|
|
1248
1782
|
provider: this.provider,
|
|
1249
1783
|
model: this.model,
|
|
1250
|
-
apiKey: creds.accessToken,
|
|
1251
|
-
accountId: creds.accountId,
|
|
1252
|
-
projectId: creds.projectId,
|
|
1253
|
-
baseUrl: this.baseUrl ?? creds.baseUrl,
|
|
1254
1784
|
contextWindow,
|
|
1255
|
-
|
|
1785
|
+
threshold: this.settingsManager.get("compactThreshold"),
|
|
1786
|
+
accountId: creds.accountId,
|
|
1787
|
+
approvedPlanPath: this.approvedPlanPath,
|
|
1256
1788
|
});
|
|
1257
|
-
|
|
1258
|
-
this.
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1789
|
+
const originalCount = this.messages.length;
|
|
1790
|
+
this.eventBus.emit("compaction_start", { messageCount: originalCount });
|
|
1791
|
+
let contextSelection;
|
|
1792
|
+
const runCompactor = async () => {
|
|
1793
|
+
const output = await compact(this.messages, {
|
|
1794
|
+
provider: this.provider,
|
|
1795
|
+
model: this.model,
|
|
1796
|
+
apiKey: creds.accessToken,
|
|
1797
|
+
accountId: creds.accountId,
|
|
1798
|
+
projectId: creds.projectId,
|
|
1799
|
+
baseUrl: this.baseUrl ?? creds.baseUrl,
|
|
1800
|
+
contextWindow,
|
|
1801
|
+
targetTokens: policy.targetTokens,
|
|
1802
|
+
signal: this.opts.signal,
|
|
1803
|
+
approvedPlanPath: this.approvedPlanPath,
|
|
1264
1804
|
});
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1805
|
+
contextSelection = output.result.contextSelection;
|
|
1806
|
+
return output;
|
|
1807
|
+
};
|
|
1808
|
+
let finalCount = originalCount;
|
|
1809
|
+
if (this.opts.transient || !this.conversationId) {
|
|
1810
|
+
const result = await runCompactor();
|
|
1811
|
+
finalCount = result.result.newCount;
|
|
1812
|
+
this.messages = result.messages;
|
|
1813
|
+
this.lastCompactionCompacted = result.result.compacted;
|
|
1814
|
+
if (result.result.compacted) {
|
|
1815
|
+
this.providerContext = null;
|
|
1816
|
+
this.remapMarkerAnchors(result.result.anchorRemap);
|
|
1817
|
+
this.lastPersistedIndex = this.messages.length;
|
|
1818
|
+
}
|
|
1276
1819
|
}
|
|
1277
1820
|
else {
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1821
|
+
const conversationId = this.conversationId;
|
|
1822
|
+
await this.sessionManager.withCompactionLease(conversationId, this.opts.signal, async () => {
|
|
1823
|
+
let sourceFingerprint = computeSourceFingerprint(this.messages);
|
|
1824
|
+
const canonicalPath = await this.sessionManager.resolveCanonicalSession(conversationId, this.cwd);
|
|
1825
|
+
if (canonicalPath && canonicalPath !== this.sessionPath) {
|
|
1826
|
+
const newest = await this.sessionManager.load(canonicalPath);
|
|
1827
|
+
if (newest.header.sourceFingerprint === sourceFingerprint) {
|
|
1828
|
+
await this.adoptCompactionCheckpoint(newest);
|
|
1829
|
+
this.lastCompactionCompacted = true;
|
|
1830
|
+
finalCount = this.messages.length;
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
// The canonical branch contains different progress. Rebase onto it before
|
|
1834
|
+
// compacting so a stale caller cannot supersede newer history by generation.
|
|
1835
|
+
await this.adoptCompactionCheckpoint(newest);
|
|
1836
|
+
sourceFingerprint = computeSourceFingerprint(this.messages);
|
|
1837
|
+
}
|
|
1838
|
+
const attempt = await this.sessionManager.readCompactionAttemptState(conversationId);
|
|
1839
|
+
const attemptActive = !attempt?.expiresAt || Date.parse(attempt.expiresAt) > Date.now();
|
|
1840
|
+
if (mode === "automatic" &&
|
|
1841
|
+
attempt?.fingerprint === sourceFingerprint &&
|
|
1842
|
+
attempt.policyKey === policy.policyKey &&
|
|
1843
|
+
attemptActive) {
|
|
1844
|
+
if (attempt.outcome === "success" && attempt.checkpointId) {
|
|
1845
|
+
const checkpointPath = await this.sessionManager.findById(this.cwd, attempt.checkpointId);
|
|
1846
|
+
if (checkpointPath) {
|
|
1847
|
+
const checkpoint = await this.sessionManager.load(checkpointPath);
|
|
1848
|
+
if (checkpoint.header.sourceFingerprint === sourceFingerprint) {
|
|
1849
|
+
await this.adoptCompactionCheckpoint(checkpoint);
|
|
1850
|
+
this.lastCompactionCompacted = true;
|
|
1851
|
+
finalCount = this.messages.length;
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
else if (attempt.outcome === "failed" || attempt.outcome === "noop") {
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
try {
|
|
1861
|
+
const result = await runCompactor();
|
|
1862
|
+
finalCount = result.result.newCount;
|
|
1863
|
+
this.messages = result.messages;
|
|
1864
|
+
this.lastCompactionCompacted = result.result.compacted;
|
|
1865
|
+
if (!result.result.compacted) {
|
|
1866
|
+
await this.sessionManager.writeCompactionAttemptState(conversationId, {
|
|
1867
|
+
fingerprint: sourceFingerprint,
|
|
1868
|
+
policyKey: policy.policyKey,
|
|
1869
|
+
outcome: "noop",
|
|
1870
|
+
updatedAt: new Date().toISOString(),
|
|
1871
|
+
expiresAt: new Date(Date.now() + 30_000).toISOString(),
|
|
1872
|
+
});
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
this.providerContext = null;
|
|
1876
|
+
this.remapMarkerAnchors(result.result.anchorRemap);
|
|
1877
|
+
await this.persistCompactionCheckpoint(sourceFingerprint, result.result);
|
|
1878
|
+
await this.sessionManager.writeCompactionAttemptState(conversationId, {
|
|
1879
|
+
fingerprint: sourceFingerprint,
|
|
1880
|
+
policyKey: policy.policyKey,
|
|
1881
|
+
outcome: "success",
|
|
1882
|
+
checkpointId: this.sessionId,
|
|
1883
|
+
updatedAt: new Date().toISOString(),
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
catch (error) {
|
|
1887
|
+
await this.sessionManager
|
|
1888
|
+
.writeCompactionAttemptState(conversationId, {
|
|
1889
|
+
fingerprint: sourceFingerprint,
|
|
1890
|
+
policyKey: policy.policyKey,
|
|
1891
|
+
outcome: "failed",
|
|
1892
|
+
updatedAt: new Date().toISOString(),
|
|
1893
|
+
expiresAt: new Date(Date.now() + 30_000).toISOString(),
|
|
1894
|
+
})
|
|
1895
|
+
.catch(() => { });
|
|
1896
|
+
throw error;
|
|
1897
|
+
}
|
|
1305
1898
|
});
|
|
1306
1899
|
}
|
|
1307
1900
|
this.eventBus.emit("compaction_end", {
|
|
1308
|
-
compacted:
|
|
1309
|
-
originalCount
|
|
1310
|
-
newCount:
|
|
1901
|
+
compacted: this.lastCompactionCompacted,
|
|
1902
|
+
originalCount,
|
|
1903
|
+
newCount: finalCount,
|
|
1904
|
+
...(contextSelection
|
|
1905
|
+
? {
|
|
1906
|
+
selectionStrategy: contextSelection.strategy,
|
|
1907
|
+
selectedMessages: contextSelection.selectedMessages,
|
|
1908
|
+
selectedTokens: contextSelection.selectedTokens,
|
|
1909
|
+
droppedMessages: contextSelection.droppedMessages,
|
|
1910
|
+
queryTerms: contextSelection.queryTerms,
|
|
1911
|
+
...(contextSelection.fallbackReason
|
|
1912
|
+
? { selectionFallback: contextSelection.fallbackReason }
|
|
1913
|
+
: {}),
|
|
1914
|
+
}
|
|
1915
|
+
: {}),
|
|
1311
1916
|
});
|
|
1312
1917
|
}
|
|
1313
1918
|
async newSession(preserveConversation = false) {
|
|
@@ -1315,6 +1920,7 @@ export class AgentSession {
|
|
|
1315
1920
|
// explicit new sessions reset the conversation identity.
|
|
1316
1921
|
if (!preserveConversation) {
|
|
1317
1922
|
this.conversationId = "";
|
|
1923
|
+
this.checkpointGeneration = 0;
|
|
1318
1924
|
this.sessionPreview = "";
|
|
1319
1925
|
}
|
|
1320
1926
|
// A fresh session drops any in-flight plan state so its prompt is clean.
|
|
@@ -1329,10 +1935,9 @@ export class AgentSession {
|
|
|
1329
1935
|
this.appMarkers = [];
|
|
1330
1936
|
this.turnMetrics = [];
|
|
1331
1937
|
const basePrompt = this.customSystemPrompt ??
|
|
1332
|
-
(await buildSystemPrompt(this.cwd, this.skills, false, undefined, this.tools.map((tool) => tool.name), undefined, this.provider));
|
|
1938
|
+
(await buildSystemPrompt(this.cwd, this.skills, false, undefined, this.tools.map((tool) => tool.name), undefined, this.provider, this.promptEnvironment()));
|
|
1333
1939
|
this.baseSystemPrompt = basePrompt;
|
|
1334
1940
|
this.messages = [{ role: "system", content: this.withSystemPromptTail(basePrompt) }];
|
|
1335
|
-
this.syncUltraOrchestrationPrompt();
|
|
1336
1941
|
// Fresh conversation — new entries must not chain onto the old DAG's leaf.
|
|
1337
1942
|
this.currentLeafId = null;
|
|
1338
1943
|
// Transient sessions (Nolan chat/autopilot, subagent spawns) never touch the
|
|
@@ -1343,6 +1948,7 @@ export class AgentSession {
|
|
|
1343
1948
|
if (this.opts.transient) {
|
|
1344
1949
|
this.sessionId = "";
|
|
1345
1950
|
this.conversationId = "";
|
|
1951
|
+
this.checkpointGeneration = 0;
|
|
1346
1952
|
this.sessionPreview = "";
|
|
1347
1953
|
this.setSessionPath("");
|
|
1348
1954
|
this.lastPersistedIndex = this.messages.length;
|
|
@@ -1415,6 +2021,41 @@ export class AgentSession {
|
|
|
1415
2021
|
accountId: this.lastAccountId,
|
|
1416
2022
|
};
|
|
1417
2023
|
}
|
|
2024
|
+
/**
|
|
2025
|
+
* Tokens currently in context and the window they are measured against.
|
|
2026
|
+
*
|
|
2027
|
+
* Uses the same accounting as the compaction decision: authoritative provider
|
|
2028
|
+
* usage when we have it (it includes the system prompt and tool schemas),
|
|
2029
|
+
* plus a local estimate of anything appended after that sample. A client
|
|
2030
|
+
* reading this right after a compaction sees the drop, because compaction
|
|
2031
|
+
* clears the retained provider sample along with the messages it measured.
|
|
2032
|
+
*
|
|
2033
|
+
* `costUsd` is present only when EVERY recorded turn has an authoritative
|
|
2034
|
+
* price; a partial sum would read as a full session cost and understate it.
|
|
2035
|
+
*/
|
|
2036
|
+
getContextUsage() {
|
|
2037
|
+
const size = getContextWindow(this.model, {
|
|
2038
|
+
provider: this.provider,
|
|
2039
|
+
accountId: this.lastAccountId,
|
|
2040
|
+
});
|
|
2041
|
+
let used;
|
|
2042
|
+
const anchorIndex = this.providerContext
|
|
2043
|
+
? this.messages.lastIndexOf(this.providerContext.anchor)
|
|
2044
|
+
: -1;
|
|
2045
|
+
if (this.providerContext && anchorIndex >= 0) {
|
|
2046
|
+
used = calculateActiveContextTokens(this.messages, {
|
|
2047
|
+
usage: this.providerContext.usage,
|
|
2048
|
+
pendingMessages: this.messages.slice(anchorIndex + 1),
|
|
2049
|
+
});
|
|
2050
|
+
}
|
|
2051
|
+
else {
|
|
2052
|
+
used = calculateActiveContextTokens(this.messages);
|
|
2053
|
+
}
|
|
2054
|
+
const costUsd = this.turnMetrics.length > 0 && this.turnMetrics.every((m) => m.cost.status === "known")
|
|
2055
|
+
? this.turnMetrics.reduce((sum, m) => sum + (m.cost.status === "known" ? m.cost.usd : 0), 0)
|
|
2056
|
+
: undefined;
|
|
2057
|
+
return costUsd === undefined ? { used, size } : { used, size, costUsd };
|
|
2058
|
+
}
|
|
1418
2059
|
getPlanMode() {
|
|
1419
2060
|
return this.planModeRef.current;
|
|
1420
2061
|
}
|
|
@@ -1434,9 +2075,25 @@ export class AgentSession {
|
|
|
1434
2075
|
* as steering. Returns the new queue length. No-op semantics are the caller's
|
|
1435
2076
|
* concern. */
|
|
1436
2077
|
queueMessage(text, attachments = []) {
|
|
1437
|
-
this.
|
|
2078
|
+
this.queueSeq += 1;
|
|
2079
|
+
this.userQueue.push({ id: `q${this.queueSeq}`, text, attachments });
|
|
1438
2080
|
return this.userQueue.length;
|
|
1439
2081
|
}
|
|
2082
|
+
/** Pending queued messages (id + text), oldest first, for client display. */
|
|
2083
|
+
listQueuedMessages() {
|
|
2084
|
+
return this.userQueue.map((m) => ({ id: m.id, text: m.text }));
|
|
2085
|
+
}
|
|
2086
|
+
/** Cancel one pending message by id. Returns true if it was still queued.
|
|
2087
|
+
* A false return is the normal race rather than an error: the message drained
|
|
2088
|
+
* into the run between the client rendering the cancel affordance and the
|
|
2089
|
+
* click arriving. */
|
|
2090
|
+
cancelQueuedMessage(id) {
|
|
2091
|
+
const index = this.userQueue.findIndex((m) => m.id === id);
|
|
2092
|
+
if (index === -1)
|
|
2093
|
+
return false;
|
|
2094
|
+
this.userQueue.splice(index, 1);
|
|
2095
|
+
return true;
|
|
2096
|
+
}
|
|
1440
2097
|
/** Number of messages currently queued. */
|
|
1441
2098
|
getQueuedCount() {
|
|
1442
2099
|
return this.userQueue.length;
|
|
@@ -1446,7 +2103,13 @@ export class AgentSession {
|
|
|
1446
2103
|
* reviewing (no run in flight to steer it into) — unlike {@link drainQueue},
|
|
1447
2104
|
* attachments survive so queued media isn't silently dropped. */
|
|
1448
2105
|
takeNextQueuedMessage() {
|
|
1449
|
-
|
|
2106
|
+
const next = this.userQueue.shift();
|
|
2107
|
+
if (next === undefined)
|
|
2108
|
+
return null;
|
|
2109
|
+
// Strip the internal queue id: it exists only so clients can cancel a
|
|
2110
|
+
// specific pending message, and callers here feed the result straight into
|
|
2111
|
+
// a run.
|
|
2112
|
+
return { text: next.text, attachments: next.attachments };
|
|
1450
2113
|
}
|
|
1451
2114
|
/** Clear the queue, returning the combined text (to restore to the composer).
|
|
1452
2115
|
* Queued attachments are dropped on cancel — the composer only restores text. */
|
|
@@ -1480,7 +2143,6 @@ export class AgentSession {
|
|
|
1480
2143
|
else {
|
|
1481
2144
|
this.messages.unshift({ role: "system", content });
|
|
1482
2145
|
}
|
|
1483
|
-
this.syncUltraOrchestrationPrompt();
|
|
1484
2146
|
}
|
|
1485
2147
|
/**
|
|
1486
2148
|
* Toggle plan mode: flips the shared ref (so tools enforce read-only
|
|
@@ -1506,11 +2168,67 @@ export class AgentSession {
|
|
|
1506
2168
|
this.approvedPlanPath = approvedPlanPath;
|
|
1507
2169
|
await this.rebuildSystemPromptInPlace();
|
|
1508
2170
|
}
|
|
2171
|
+
/** Extra workspace roots added with `/add-dir`, in the order added. */
|
|
2172
|
+
getAdditionalRoots() {
|
|
2173
|
+
return [...this.additionalRoots];
|
|
2174
|
+
}
|
|
2175
|
+
/**
|
|
2176
|
+
* Add another workspace root. Tools already accept absolute paths, so this
|
|
2177
|
+
* only widens the write guard and tells the model the root exists. Rebuilding
|
|
2178
|
+
* the system prompt costs one cache-miss turn — the alternative (an uncached
|
|
2179
|
+
* suffix) would drift from the tool behaviour it describes.
|
|
2180
|
+
*
|
|
2181
|
+
* @returns the resolved root, or an error message for the user.
|
|
2182
|
+
*/
|
|
2183
|
+
async addDirectory(dir) {
|
|
2184
|
+
const resolved = path.resolve(this.cwd, dir.replace(/^~(?=[/\\]|$)/, os.homedir()));
|
|
2185
|
+
let stat;
|
|
2186
|
+
try {
|
|
2187
|
+
stat = await fs.stat(resolved);
|
|
2188
|
+
}
|
|
2189
|
+
catch {
|
|
2190
|
+
return { ok: false, error: `Not found: ${resolved}` };
|
|
2191
|
+
}
|
|
2192
|
+
if (!stat.isDirectory())
|
|
2193
|
+
return { ok: false, error: `Not a directory: ${resolved}` };
|
|
2194
|
+
const covered = [this.cwd, ...this.additionalRoots].some((root) => {
|
|
2195
|
+
const relative = path.relative(path.resolve(root), resolved);
|
|
2196
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
2197
|
+
});
|
|
2198
|
+
if (covered)
|
|
2199
|
+
return { ok: false, error: `Already in the workspace: ${resolved}` };
|
|
2200
|
+
// Drop roots the new one subsumes so the list stays minimal.
|
|
2201
|
+
this.additionalRoots = this.additionalRoots.filter((root) => {
|
|
2202
|
+
const relative = path.relative(resolved, path.resolve(root));
|
|
2203
|
+
return relative.startsWith("..") || path.isAbsolute(relative);
|
|
2204
|
+
});
|
|
2205
|
+
this.additionalRoots.push(resolved);
|
|
2206
|
+
await this.rebuildSystemPromptInPlace();
|
|
2207
|
+
return { ok: true, root: resolved };
|
|
2208
|
+
}
|
|
2209
|
+
/** Remove an exact root previously added with `/add-dir`. */
|
|
2210
|
+
async removeDirectory(dir) {
|
|
2211
|
+
const resolved = path.resolve(this.cwd, dir.replace(/^~(?=[/\\]|$)/, os.homedir()));
|
|
2212
|
+
const index = this.additionalRoots.findIndex((root) => path.resolve(root) === resolved);
|
|
2213
|
+
if (index === -1) {
|
|
2214
|
+
return { ok: false, error: `Not an additional workspace root: ${resolved}` };
|
|
2215
|
+
}
|
|
2216
|
+
this.additionalRoots.splice(index, 1);
|
|
2217
|
+
await this.rebuildSystemPromptInPlace();
|
|
2218
|
+
return { ok: true, root: resolved };
|
|
2219
|
+
}
|
|
2220
|
+
/** Environment facts that vary per session rather than per host. */
|
|
2221
|
+
promptEnvironment() {
|
|
2222
|
+
const networkAllow = this.settingsManager.get("networkMode") === "allowlist"
|
|
2223
|
+
? this.settingsManager.get("networkAllow")
|
|
2224
|
+
: [];
|
|
2225
|
+
return { additionalRoots: this.additionalRoots, networkAllow };
|
|
2226
|
+
}
|
|
1509
2227
|
/** Rebuild messages[0] from current plan-mode + approved-plan state. */
|
|
1510
2228
|
async rebuildSystemPromptInPlace() {
|
|
1511
2229
|
if (this.customSystemPrompt)
|
|
1512
2230
|
return;
|
|
1513
|
-
const rebuilt = await buildSystemPrompt(this.cwd, this.skills, this.planModeRef.current, this.approvedPlanPath, this.tools.map((tool) => tool.name), undefined, this.provider);
|
|
2231
|
+
const rebuilt = await buildSystemPrompt(this.cwd, this.skills, this.planModeRef.current, this.approvedPlanPath, this.tools.map((tool) => tool.name), undefined, this.provider, this.promptEnvironment());
|
|
1514
2232
|
this.baseSystemPrompt = rebuilt;
|
|
1515
2233
|
const content = this.withSystemPromptTail(rebuilt);
|
|
1516
2234
|
if (this.messages[0]?.role === "system") {
|
|
@@ -1519,18 +2237,47 @@ export class AgentSession {
|
|
|
1519
2237
|
else {
|
|
1520
2238
|
this.messages.unshift({ role: "system", content });
|
|
1521
2239
|
}
|
|
1522
|
-
this.syncUltraOrchestrationPrompt();
|
|
1523
2240
|
}
|
|
2241
|
+
/**
|
|
2242
|
+
* Compose the system message: stable prefix, then everything volatile behind
|
|
2243
|
+
* the `<!-- uncached -->` marker that providers split on for cache control.
|
|
2244
|
+
*
|
|
2245
|
+
* Anything that varies with the live model/provider/thinking level belongs in
|
|
2246
|
+
* the tail. Putting it in the prefix means every model switch rewrites cached
|
|
2247
|
+
* bytes and the next turn pays a full cache write instead of a read.
|
|
2248
|
+
*/
|
|
1524
2249
|
withSystemPromptTail(basePrompt) {
|
|
1525
|
-
|
|
2250
|
+
const tailParts = [];
|
|
2251
|
+
const orchestration = this.orchestrationPolicyTail();
|
|
2252
|
+
if (orchestration)
|
|
2253
|
+
tailParts.push(orchestration);
|
|
2254
|
+
const hostTail = this.opts.getSystemPromptTail?.();
|
|
2255
|
+
if (hostTail)
|
|
2256
|
+
tailParts.push(hostTail);
|
|
2257
|
+
if (tailParts.length === 0)
|
|
1526
2258
|
return basePrompt;
|
|
1527
|
-
return `${basePrompt}\n\n<!-- uncached -->\n${
|
|
2259
|
+
return `${basePrompt}\n\n<!-- uncached -->\n${tailParts.join("\n\n")}`;
|
|
2260
|
+
}
|
|
2261
|
+
/**
|
|
2262
|
+
* Sol/Terra async-orchestration guidance for the CURRENT model and thinking
|
|
2263
|
+
* level. Per-switch volatile by definition, so it is rendered as a tail part
|
|
2264
|
+
* rather than spliced into the cached prefix.
|
|
2265
|
+
*/
|
|
2266
|
+
orchestrationPolicyTail() {
|
|
2267
|
+
if (this.opts.orchestrationPrompt === false)
|
|
2268
|
+
return "";
|
|
2269
|
+
return applyAsyncSubagentPolicy("", this.provider, this.model, this.thinkingLevel, this.tools.map((tool) => tool.name)).trim();
|
|
1528
2270
|
}
|
|
2271
|
+
/**
|
|
2272
|
+
* Re-render the uncached tail of messages[0] in place. The cached prefix is
|
|
2273
|
+
* byte-identical afterwards, so this is safe to call on every model /
|
|
2274
|
+
* thinking-level change.
|
|
2275
|
+
*/
|
|
1529
2276
|
refreshSystemPromptTail() {
|
|
1530
|
-
if (!this.opts.getSystemPromptTail)
|
|
1531
|
-
return;
|
|
1532
2277
|
const content = this.withSystemPromptTail(this.baseSystemPrompt);
|
|
1533
2278
|
if (this.messages[0]?.role === "system") {
|
|
2279
|
+
if (this.messages[0].content === content)
|
|
2280
|
+
return;
|
|
1534
2281
|
this.messages[0] = { role: "system", content };
|
|
1535
2282
|
}
|
|
1536
2283
|
else {
|
|
@@ -1585,6 +2332,29 @@ export class AgentSession {
|
|
|
1585
2332
|
getAutopilotMarkers() {
|
|
1586
2333
|
return this.autopilotMarkers;
|
|
1587
2334
|
}
|
|
2335
|
+
/** Non-system messages that are actually on disk. Transcript markers anchor
|
|
2336
|
+
* against this (not the in-memory list, which can run ahead after a failed
|
|
2337
|
+
* run), so hosts computing marker-derived values must use the same base. */
|
|
2338
|
+
getPersistedTranscriptCount() {
|
|
2339
|
+
return this.persistedTranscriptCount();
|
|
2340
|
+
}
|
|
2341
|
+
/**
|
|
2342
|
+
* Rebase every transcript anchor (Nolan turns, autopilot verdicts, app markers)
|
|
2343
|
+
* onto a freshly compacted message list. Called right after `this.messages`
|
|
2344
|
+
* is replaced and before the markers are re-persisted into the continuation
|
|
2345
|
+
* file, so the new file carries positions that match its own transcript.
|
|
2346
|
+
*/
|
|
2347
|
+
remapMarkerAnchors(remap) {
|
|
2348
|
+
if (!remap)
|
|
2349
|
+
return;
|
|
2350
|
+
const move = (payload) => ({
|
|
2351
|
+
...payload,
|
|
2352
|
+
afterMessageCount: remapAnchorForCompaction(payload.afterMessageCount, remap),
|
|
2353
|
+
});
|
|
2354
|
+
this.nolanTurns = this.nolanTurns.map(move);
|
|
2355
|
+
this.autopilotMarkers = this.autopilotMarkers.map(move);
|
|
2356
|
+
this.appMarkers = this.appMarkers.map(move);
|
|
2357
|
+
}
|
|
1588
2358
|
/**
|
|
1589
2359
|
* Record one Nolan Grout (mentor agent) turn against this build session: the
|
|
1590
2360
|
* user's question + Nolan's reply. Kept in memory for the live transcript and
|
|
@@ -1596,7 +2366,7 @@ export class AgentSession {
|
|
|
1596
2366
|
* appendEntry's own handling.
|
|
1597
2367
|
*/
|
|
1598
2368
|
async persistNolanTurn(question, reply) {
|
|
1599
|
-
const afterMessageCount = this.
|
|
2369
|
+
const afterMessageCount = this.persistedTranscriptCount();
|
|
1600
2370
|
const payload = { version: 1, question, reply, afterMessageCount };
|
|
1601
2371
|
this.nolanTurns.push(payload);
|
|
1602
2372
|
if (!this.sessionPath)
|
|
@@ -1625,7 +2395,7 @@ export class AgentSession {
|
|
|
1625
2395
|
id: crypto.randomUUID(),
|
|
1626
2396
|
parentId: null,
|
|
1627
2397
|
timestamp: new Date().toISOString(),
|
|
1628
|
-
data: payload,
|
|
2398
|
+
data: stripRecordedPosition(payload),
|
|
1629
2399
|
};
|
|
1630
2400
|
await this.sessionManager.appendEntry(this.sessionPath, entry);
|
|
1631
2401
|
}
|
|
@@ -1639,7 +2409,7 @@ export class AgentSession {
|
|
|
1639
2409
|
* No-op persistence for transient sessions (kept in memory only).
|
|
1640
2410
|
*/
|
|
1641
2411
|
async persistAutopilotMarker(phase, extra) {
|
|
1642
|
-
const afterMessageCount = this.
|
|
2412
|
+
const afterMessageCount = this.persistedTranscriptCount();
|
|
1643
2413
|
const payload = {
|
|
1644
2414
|
version: 1,
|
|
1645
2415
|
phase,
|
|
@@ -1673,7 +2443,7 @@ export class AgentSession {
|
|
|
1673
2443
|
id: crypto.randomUUID(),
|
|
1674
2444
|
parentId: null,
|
|
1675
2445
|
timestamp: new Date().toISOString(),
|
|
1676
|
-
data: payload,
|
|
2446
|
+
data: stripRecordedPosition(payload),
|
|
1677
2447
|
};
|
|
1678
2448
|
await this.sessionManager.appendEntry(this.sessionPath, entry);
|
|
1679
2449
|
}
|
|
@@ -1694,7 +2464,7 @@ export class AgentSession {
|
|
|
1694
2464
|
* No-op persistence for transient sessions.
|
|
1695
2465
|
*/
|
|
1696
2466
|
async persistAppMarker(kind, data, anchorOffset = 0) {
|
|
1697
|
-
const afterMessageCount = this.
|
|
2467
|
+
const afterMessageCount = this.persistedTranscriptCount() + anchorOffset;
|
|
1698
2468
|
const payload = { version: 1, kind, afterMessageCount, data };
|
|
1699
2469
|
this.appMarkers.push(payload);
|
|
1700
2470
|
if (!this.sessionPath)
|
|
@@ -1709,6 +2479,33 @@ export class AgentSession {
|
|
|
1709
2479
|
};
|
|
1710
2480
|
await this.sessionManager.appendEntry(this.sessionPath, entry);
|
|
1711
2481
|
}
|
|
2482
|
+
/**
|
|
2483
|
+
* Open the run journal for one `RunLifecycle` generation.
|
|
2484
|
+
*
|
|
2485
|
+
* Never throws and never blocks the run: a session with no file (transient
|
|
2486
|
+
* children) writes nothing, and a write failure just means the run isn't
|
|
2487
|
+
* journalled — which is strictly better than failing the run over it.
|
|
2488
|
+
*/
|
|
2489
|
+
async persistRunStarted(generation) {
|
|
2490
|
+
if (!this.sessionPath)
|
|
2491
|
+
return;
|
|
2492
|
+
await this.sessionManager.appendRunStarted(this.sessionPath, {
|
|
2493
|
+
version: 1,
|
|
2494
|
+
generation,
|
|
2495
|
+
startedAt: new Date().toISOString(),
|
|
2496
|
+
afterMessageCount: this.persistedTranscriptCount(),
|
|
2497
|
+
});
|
|
2498
|
+
}
|
|
2499
|
+
/** Close the run journal. Its absence is what marks a run as crashed. */
|
|
2500
|
+
async persistRunFinished(generation, outcome) {
|
|
2501
|
+
if (!this.sessionPath)
|
|
2502
|
+
return;
|
|
2503
|
+
await this.sessionManager.appendRunFinished(this.sessionPath, {
|
|
2504
|
+
version: 1,
|
|
2505
|
+
generation,
|
|
2506
|
+
outcome,
|
|
2507
|
+
});
|
|
2508
|
+
}
|
|
1712
2509
|
/** Re-append the in-memory app markers to the current session file. Mirrors
|
|
1713
2510
|
* `rePersistNolanTurns` — called after a continuation/compaction file is
|
|
1714
2511
|
* created so display-only rows survive the rewrite. */
|
|
@@ -1722,7 +2519,7 @@ export class AgentSession {
|
|
|
1722
2519
|
id: crypto.randomUUID(),
|
|
1723
2520
|
parentId: null,
|
|
1724
2521
|
timestamp: new Date().toISOString(),
|
|
1725
|
-
data: payload,
|
|
2522
|
+
data: stripRecordedPosition(payload),
|
|
1726
2523
|
};
|
|
1727
2524
|
await this.sessionManager.appendEntry(this.sessionPath, entry);
|
|
1728
2525
|
}
|
|
@@ -1768,16 +2565,7 @@ export class AgentSession {
|
|
|
1768
2565
|
* effect on the next prompt, since the in-flight loop reads it at start. */
|
|
1769
2566
|
setThinkingLevel(level) {
|
|
1770
2567
|
this.thinkingLevel = level;
|
|
1771
|
-
this.
|
|
1772
|
-
}
|
|
1773
|
-
/** Sol/Terra Ultra delegates proactively; lower levels require an explicit request. */
|
|
1774
|
-
syncUltraOrchestrationPrompt() {
|
|
1775
|
-
if (this.opts.orchestrationPrompt === false)
|
|
1776
|
-
return;
|
|
1777
|
-
const systemMessage = this.messages[0];
|
|
1778
|
-
if (systemMessage?.role !== "system" || typeof systemMessage.content !== "string")
|
|
1779
|
-
return;
|
|
1780
|
-
systemMessage.content = applyAsyncSubagentPolicy(systemMessage.content, this.provider, this.model, this.thinkingLevel, this.tools.map((tool) => tool.name));
|
|
2568
|
+
this.refreshSystemPromptTail();
|
|
1781
2569
|
}
|
|
1782
2570
|
/** Replace the abort signal (e.g. after cancellation). */
|
|
1783
2571
|
setSignal(signal) {
|
|
@@ -1840,19 +2628,28 @@ export class AgentSession {
|
|
|
1840
2628
|
this.sessionManager.registerActivePath(nextPath);
|
|
1841
2629
|
}
|
|
1842
2630
|
async createNewSession() {
|
|
2631
|
+
const continuingConversation = Boolean(this.conversationId && this.sessionId);
|
|
1843
2632
|
const session = await this.sessionManager.create(this.cwd, this.provider, this.model, {
|
|
1844
2633
|
conversationId: this.conversationId || undefined,
|
|
2634
|
+
generation: continuingConversation ? this.checkpointGeneration + 1 : 0,
|
|
2635
|
+
parentSessionId: continuingConversation ? this.sessionId : undefined,
|
|
1845
2636
|
preview: this.sessionPreview || undefined,
|
|
1846
2637
|
});
|
|
1847
2638
|
this.sessionId = session.id;
|
|
2639
|
+
this.checkpointGeneration = session.header.generation ?? 0;
|
|
1848
2640
|
this.conversationId = session.header.conversationId ?? session.id;
|
|
1849
2641
|
this.setSessionPath(session.path);
|
|
1850
2642
|
this.lastPersistedIndex = this.messages.length;
|
|
1851
2643
|
}
|
|
1852
2644
|
async loadExistingSession(sessionPath) {
|
|
1853
|
-
|
|
2645
|
+
// A stale physical checkpoint is only an address, not the conversation tip.
|
|
2646
|
+
// Resolve every resume—not just over-threshold/deferred compaction resumes—
|
|
2647
|
+
// before reading history so the next prompt cannot continue an old branch.
|
|
2648
|
+
const canonicalPath = (await this.sessionManager.resolveCanonicalSession(sessionPath, this.cwd)) ?? sessionPath;
|
|
2649
|
+
const loaded = await this.sessionManager.load(canonicalPath);
|
|
1854
2650
|
// Use the leaf from the header to walk the correct branch
|
|
1855
2651
|
const loadedMessages = this.sessionManager.getMessages(loaded.entries, loaded.header.leafId);
|
|
2652
|
+
this.checkpointGeneration = loaded.header.generation ?? 0;
|
|
1856
2653
|
this.conversationId = loaded.header.conversationId ?? loaded.header.id;
|
|
1857
2654
|
const legacyLabel = [...loaded.entries]
|
|
1858
2655
|
.reverse()
|
|
@@ -1864,12 +2661,17 @@ export class AgentSession {
|
|
|
1864
2661
|
legacyLabel || loaded.header.preview || findUserSessionPrompt(loadedMessages);
|
|
1865
2662
|
// Restore Nolan's advisory turns (custom entries, not on the message branch) so
|
|
1866
2663
|
// they reappear in the transcript and survive into the continuation file.
|
|
1867
|
-
|
|
2664
|
+
// The leaf lets marker replay retain its independent file-order position.
|
|
2665
|
+
this.nolanTurns = this.sessionManager.getNolanTurns(loaded.entries, loaded.header.leafId);
|
|
1868
2666
|
// Restore autopilot verdict markers the same way (not on the message DAG).
|
|
1869
|
-
this.autopilotMarkers = this.sessionManager.getAutopilotMarkers(loaded.entries);
|
|
2667
|
+
this.autopilotMarkers = this.sessionManager.getAutopilotMarkers(loaded.entries, loaded.header.leafId);
|
|
1870
2668
|
// Restore app transcript markers (plan banner / task header / errors / hints).
|
|
1871
|
-
this.appMarkers = this.sessionManager.getAppMarkers(loaded.entries);
|
|
2669
|
+
this.appMarkers = this.sessionManager.getAppMarkers(loaded.entries, loaded.header.leafId);
|
|
1872
2670
|
this.turnMetrics = this.sessionManager.getTurnMetrics(loaded.entries);
|
|
2671
|
+
// A run that opened the journal and never closed it died mid-flight. Read
|
|
2672
|
+
// it here, before anything rewrites the file, and report it once the
|
|
2673
|
+
// transcript is in place.
|
|
2674
|
+
const interruptedRuns = this.sessionManager.getUnfinishedRuns(loaded.entries);
|
|
1873
2675
|
// Track the current leaf for subsequent entries
|
|
1874
2676
|
this.currentLeafId = loaded.header.leafId;
|
|
1875
2677
|
// Rebuild messages: keep system, add loaded. Older ezcoder-app sessions may
|
|
@@ -1894,63 +2696,41 @@ export class AgentSession {
|
|
|
1894
2696
|
provider: this.provider,
|
|
1895
2697
|
accountId: creds.accountId,
|
|
1896
2698
|
});
|
|
2699
|
+
this.sessionId = loaded.header.id;
|
|
2700
|
+
this.setSessionPath(loaded.path);
|
|
2701
|
+
this.lastPersistedIndex = this.messages.length;
|
|
2702
|
+
const loadPolicy = resolveCompactionPolicy({
|
|
2703
|
+
provider: this.provider,
|
|
2704
|
+
model: this.model,
|
|
2705
|
+
contextWindow,
|
|
2706
|
+
threshold: this.settingsManager.get("compactThreshold"),
|
|
2707
|
+
accountId: creds.accountId,
|
|
2708
|
+
approvedPlanPath: this.approvedPlanPath,
|
|
2709
|
+
});
|
|
2710
|
+
log("INFO", "compaction", "Restore compaction decision", {
|
|
2711
|
+
provider: this.provider,
|
|
2712
|
+
model: this.model,
|
|
2713
|
+
transport: this.provider === "openai" && creds.accountId ? "codex_oauth" : "public_api",
|
|
2714
|
+
contextWindow: String(contextWindow),
|
|
2715
|
+
activeTokens: "estimated",
|
|
2716
|
+
triggerLimit: String(loadPolicy.targetTokens),
|
|
2717
|
+
});
|
|
1897
2718
|
const needsLoadCompaction = this.settingsManager.get("autoCompact") &&
|
|
1898
|
-
shouldCompact(this.messages, contextWindow,
|
|
2719
|
+
shouldCompact(this.messages, contextWindow, loadPolicy.threshold);
|
|
1899
2720
|
if (needsLoadCompaction && this.opts.deferLoadCompaction) {
|
|
1900
|
-
//
|
|
1901
|
-
//
|
|
1902
|
-
|
|
2721
|
+
// Canonicalize again immediately before the first prompt is persisted:
|
|
2722
|
+
// another process may create the shared checkpoint after this load.
|
|
2723
|
+
this.deferredCompactionPending = true;
|
|
1903
2724
|
log("INFO", "session", "Restored session exceeds context — deferring compaction to first prompt");
|
|
1904
2725
|
}
|
|
1905
2726
|
else if (needsLoadCompaction) {
|
|
1906
2727
|
await this.subAgentManager?.hydrate(loaded.header.id);
|
|
1907
2728
|
log("INFO", "session", `Restored session exceeds context — auto-compacting`);
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
accountId: creds.accountId,
|
|
1913
|
-
projectId: creds.projectId,
|
|
1914
|
-
baseUrl: this.baseUrl ?? creds.baseUrl,
|
|
1915
|
-
contextWindow,
|
|
1916
|
-
signal: this.opts.signal,
|
|
1917
|
-
});
|
|
1918
|
-
this.messages = compacted.messages;
|
|
1919
|
-
log("INFO", "session", `Auto-compaction complete`, {
|
|
1920
|
-
before: String(compacted.result.originalCount),
|
|
1921
|
-
after: String(compacted.result.newCount),
|
|
1922
|
-
});
|
|
1923
|
-
// Compaction rewrote history, so the on-disk file no longer reflects
|
|
1924
|
-
// what's in memory — fork a fresh session file for the compacted state
|
|
1925
|
-
// (mirrors compact()'s own persistence) so `ezcoder continue` picks up
|
|
1926
|
-
// the summary instead of the full original transcript.
|
|
1927
|
-
const session = await this.sessionManager.create(this.cwd, this.provider, this.model, {
|
|
1928
|
-
conversationId: this.conversationId || undefined,
|
|
1929
|
-
preview: this.sessionPreview || undefined,
|
|
1930
|
-
});
|
|
1931
|
-
this.sessionId = session.id;
|
|
1932
|
-
this.conversationId = session.header.conversationId ?? session.id;
|
|
1933
|
-
this.setSessionPath(session.path);
|
|
1934
|
-
await this.subAgentManager?.rebindParentSession(this.sessionId);
|
|
1935
|
-
this.currentLeafId = null;
|
|
1936
|
-
// Re-persist (compacted) messages — skip system, it's rebuilt on load
|
|
1937
|
-
for (const msg of this.messages) {
|
|
1938
|
-
if (msg.role === "system")
|
|
1939
|
-
continue;
|
|
1940
|
-
await this.persistMessage(msg);
|
|
2729
|
+
await this.compact(creds, "automatic");
|
|
2730
|
+
if (this.lastCompactionCompacted) {
|
|
2731
|
+
await this.recordInterruptedRuns(interruptedRuns);
|
|
2732
|
+
return;
|
|
1941
2733
|
}
|
|
1942
|
-
this.lastPersistedIndex = this.messages.length;
|
|
1943
|
-
// Carry restored evidence and Nolan turns into the continuation file.
|
|
1944
|
-
await this.rePersistTurnMetrics();
|
|
1945
|
-
await this.rePersistNolanTurns();
|
|
1946
|
-
await this.rePersistAutopilotMarkers();
|
|
1947
|
-
await this.rePersistAppMarkers();
|
|
1948
|
-
// Record this load-time auto-compaction's counts for the resumed notice.
|
|
1949
|
-
await this.persistAppMarker("compaction", {
|
|
1950
|
-
originalCount: compacted.result.originalCount,
|
|
1951
|
-
newCount: compacted.result.newCount,
|
|
1952
|
-
});
|
|
1953
|
-
return;
|
|
1954
2734
|
}
|
|
1955
2735
|
// Plain resume (no compaction needed): keep using the original session
|
|
1956
2736
|
// file/id and append future turns to it in place. Forking a new file here
|
|
@@ -1960,13 +2740,39 @@ export class AgentSession {
|
|
|
1960
2740
|
this.sessionId = loaded.header.id;
|
|
1961
2741
|
this.setSessionPath(loaded.path);
|
|
1962
2742
|
this.lastPersistedIndex = this.messages.length;
|
|
2743
|
+
await this.recordInterruptedRuns(interruptedRuns);
|
|
2744
|
+
}
|
|
2745
|
+
/**
|
|
2746
|
+
* Surface runs that died mid-flight, without resuming them.
|
|
2747
|
+
*
|
|
2748
|
+
* Deliberately NOT auto-resumed: the dead run's tools already wrote files,
|
|
2749
|
+
* ran commands and made commits. Replaying it would duplicate those effects.
|
|
2750
|
+
* The user gets a transcript row and decides.
|
|
2751
|
+
*
|
|
2752
|
+
* Each detected run is also closed as `aborted`, so reopening the session
|
|
2753
|
+
* reports it once rather than on every load.
|
|
2754
|
+
*/
|
|
2755
|
+
async recordInterruptedRuns(runs) {
|
|
2756
|
+
for (const run of runs) {
|
|
2757
|
+
log("WARN", "session", "Restored a session with an unfinished run", {
|
|
2758
|
+
generation: String(run.generation),
|
|
2759
|
+
startedAt: run.startedAt,
|
|
2760
|
+
});
|
|
2761
|
+
await this.persistAppMarker("interrupted_run", {
|
|
2762
|
+
generation: run.generation,
|
|
2763
|
+
startedAt: run.startedAt,
|
|
2764
|
+
});
|
|
2765
|
+
await this.persistRunFinished(run.generation, "aborted");
|
|
2766
|
+
}
|
|
1963
2767
|
}
|
|
1964
2768
|
async prepareDynamicContext(_latestUserPrompt) {
|
|
1965
2769
|
return this.messages;
|
|
1966
2770
|
}
|
|
1967
2771
|
async persistMessage(message) {
|
|
1968
|
-
if (!this.sessionPreview &&
|
|
1969
|
-
|
|
2772
|
+
if (!this.sessionPreview &&
|
|
2773
|
+
message.role === "user" &&
|
|
2774
|
+
(message.provenance?.source === "human" || !message.provenance)) {
|
|
2775
|
+
this.sessionPreview = getUserSessionPrompt(message.content, message.provenance) ?? "";
|
|
1970
2776
|
}
|
|
1971
2777
|
// Transient sessions (subagent spawns) have no session file — skip.
|
|
1972
2778
|
if (!this.sessionPath)
|
|
@@ -1986,7 +2792,7 @@ export class AgentSession {
|
|
|
1986
2792
|
createSlashCommandContext() {
|
|
1987
2793
|
return {
|
|
1988
2794
|
switchModel: (provider, model) => this.switchModel(provider, model),
|
|
1989
|
-
compact: () => this.compact(),
|
|
2795
|
+
compact: () => this.compact(undefined, "manual"),
|
|
1990
2796
|
newSession: () => this.newSession(),
|
|
1991
2797
|
listSessions: async () => {
|
|
1992
2798
|
const sessions = await this.sessionManager.list(this.cwd);
|
|
@@ -2019,7 +2825,53 @@ export class AgentSession {
|
|
|
2019
2825
|
const lines = branches.map((b, i) => ` ${i + 1}. ${b.leafId.slice(0, 8)} — ${b.entryCount} entries (${b.leafId === this.currentLeafId ? "active" : "inactive"})`);
|
|
2020
2826
|
return `${branches.length} branch(es):\n${lines.join("\n")}`;
|
|
2021
2827
|
},
|
|
2828
|
+
addDirectory: (dir) => this.addDirectory(dir),
|
|
2829
|
+
removeDirectory: (dir) => this.removeDirectory(dir),
|
|
2830
|
+
getAdditionalRoots: () => this.getAdditionalRoots(),
|
|
2022
2831
|
};
|
|
2023
2832
|
}
|
|
2833
|
+
/**
|
|
2834
|
+
* Import a Claude Code / Codex / Cursor transcript as a resumable EZ Coder
|
|
2835
|
+
* session in this session's sessions directory. Never throws — a bad path or
|
|
2836
|
+
* an unrecognized format comes back as `{ ok: false, error }` so both the CLI
|
|
2837
|
+
* and the desktop app can show it verbatim.
|
|
2838
|
+
*/
|
|
2839
|
+
async importForeignTranscript(filePath, opts = {}) {
|
|
2840
|
+
try {
|
|
2841
|
+
const imported = await importForeignSession({
|
|
2842
|
+
filePath: resolveHomePath(filePath),
|
|
2843
|
+
sessionManager: this.sessionManager,
|
|
2844
|
+
provider: this.provider,
|
|
2845
|
+
model: this.model,
|
|
2846
|
+
cwd: opts.cwd ?? undefined,
|
|
2847
|
+
});
|
|
2848
|
+
log("INFO", "import", "Imported foreign transcript", {
|
|
2849
|
+
format: imported.format,
|
|
2850
|
+
messages: String(imported.messageCount),
|
|
2851
|
+
});
|
|
2852
|
+
return {
|
|
2853
|
+
ok: true,
|
|
2854
|
+
sessionId: imported.sessionId,
|
|
2855
|
+
sessionPath: imported.sessionPath,
|
|
2856
|
+
cwd: imported.cwd,
|
|
2857
|
+
format: imported.format,
|
|
2858
|
+
messageCount: imported.messageCount,
|
|
2859
|
+
dropped: describeDropped(imported.dropped),
|
|
2860
|
+
...(imported.preview ? { preview: imported.preview } : {}),
|
|
2861
|
+
};
|
|
2862
|
+
}
|
|
2863
|
+
catch (err) {
|
|
2864
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
/** Expand a leading `~` so `/import ~/.codex/...` works from any shell. */
|
|
2869
|
+
function resolveHomePath(filePath) {
|
|
2870
|
+
if (filePath === "~")
|
|
2871
|
+
return os.homedir();
|
|
2872
|
+
if (filePath.startsWith("~/") || filePath.startsWith("~\\")) {
|
|
2873
|
+
return path.join(os.homedir(), filePath.slice(2));
|
|
2874
|
+
}
|
|
2875
|
+
return filePath;
|
|
2024
2876
|
}
|
|
2025
2877
|
//# sourceMappingURL=agent-session.js.map
|