mixdog 0.9.2 → 0.9.4
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/package.json +8 -3
- package/scripts/anthropic-maxtokens-test.mjs +119 -0
- package/scripts/bench/lead-review-tasks-r3.json +20 -0
- package/scripts/bench/lead-review-tasks.json +20 -0
- package/scripts/bench/r4-mixed-tasks.json +20 -0
- package/scripts/bench/review-tasks.json +20 -0
- package/scripts/bench/round-codex.json +114 -0
- package/scripts/bench/round-mixdog-lead-r3.json +269 -0
- package/scripts/bench/round-mixdog-lead.json +269 -0
- package/scripts/bench/round-mixdog.json +126 -0
- package/scripts/bench/round-r10-bigsample.json +679 -0
- package/scripts/bench/round-r11-codexalign.json +257 -0
- package/scripts/bench/round-r4-codex.json +114 -0
- package/scripts/bench/round-r4-mixed.json +225 -0
- package/scripts/bench/round-r5-gpt-lead.json +259 -0
- package/scripts/bench/round-r6-codex.json +114 -0
- package/scripts/bench/round-r6-solo.json +257 -0
- package/scripts/bench/round-r7-full.json +254 -0
- package/scripts/bench/round-r8-fulldefault.json +255 -0
- package/scripts/bench-run.mjs +215 -29
- package/scripts/build-tui.mjs +13 -1
- package/scripts/explore-bench.mjs +124 -0
- package/scripts/freevar-smoke.mjs +95 -0
- package/scripts/hook-bus-test.mjs +191 -0
- package/scripts/internal-comms-bench.mjs +1 -0
- package/scripts/internal-comms-smoke.mjs +10 -9
- package/scripts/mouse-probe.mjs +45 -0
- package/scripts/output-style-bench.mjs +13 -6
- package/scripts/output-style-smoke.mjs +4 -4
- package/scripts/path-suffix-test.mjs +57 -0
- package/scripts/provider-toolcall-test.mjs +7 -3
- package/scripts/recall-bench.mjs +207 -0
- package/scripts/recall-usecase-cases.json +18 -0
- package/scripts/recall-usecase-probe.json +6 -0
- package/scripts/session-bench.mjs +152 -6
- package/scripts/tool-smoke.mjs +30 -67
- package/scripts/tui-render-smoke.mjs +90 -0
- package/scripts/webhook-smoke.mjs +208 -0
- package/src/agents/debugger/AGENT.md +5 -2
- package/src/agents/heavy-worker/AGENT.md +21 -11
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +3 -2
- package/src/agents/worker/AGENT.md +21 -11
- package/src/lib/rules-builder.cjs +4 -0
- package/src/mixdog-session-runtime.mjs +933 -3731
- package/src/output-styles/default.md +34 -9
- package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
- package/src/output-styles/minimal.md +4 -1
- package/src/output-styles/simple.md +22 -7
- package/src/repl.mjs +5 -5
- package/src/rules/agent/00-common.md +2 -0
- package/src/rules/agent/30-explorer.md +8 -11
- package/src/rules/lead/lead-brief.md +12 -0
- package/src/rules/lead/lead-tool.md +2 -9
- package/src/rules/shared/01-tool.md +11 -5
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
- package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
- package/src/runtime/agent/orchestrator/config.mjs +69 -2
- package/src/runtime/agent/orchestrator/context/collect.mjs +51 -0
- package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +63 -21
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +97 -1343
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +78 -10
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +1 -13
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +44 -1014
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +18 -4
- package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +86 -11
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +348 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +19 -12
- package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +41 -1142
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +732 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +303 -2119
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +140 -995
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +227 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +436 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +38 -12
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +7 -8
- package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
- package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
- package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
- package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
- package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
- package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +274 -0
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
- package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +47 -0
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +182 -0
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +409 -1304
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +230 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +149 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
- package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +226 -2114
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
- package/src/runtime/agent/orchestrator/stall-policy.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +33 -42
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -87
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +78 -304
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +11 -6
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +551 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1080 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +36 -4277
- package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
- package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
- package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -23
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +10 -74
- package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
- package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
- package/src/runtime/channels/backends/discord-access.mjs +32 -0
- package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
- package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
- package/src/runtime/channels/backends/discord.mjs +12 -292
- package/src/runtime/channels/index.mjs +241 -894
- package/src/runtime/channels/lib/backend-dispatch.mjs +44 -0
- package/src/runtime/channels/lib/boot-profile.mjs +23 -0
- package/src/runtime/channels/lib/crash-log.mjs +106 -0
- package/src/runtime/channels/lib/event-pipeline.mjs +18 -1
- package/src/runtime/channels/lib/event-queue.mjs +63 -4
- package/src/runtime/channels/lib/inbound-routing.mjs +111 -0
- package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +9 -1
- package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
- package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
- package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
- package/src/runtime/channels/lib/session-discovery.mjs +56 -4
- package/src/runtime/channels/lib/telegram-format.mjs +19 -22
- package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
- package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
- package/src/runtime/channels/lib/webhook/deliveries.mjs +312 -0
- package/src/runtime/channels/lib/webhook/log.mjs +42 -0
- package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
- package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
- package/src/runtime/channels/lib/webhook.mjs +36 -570
- package/src/runtime/channels/lib/whisper-language.mjs +42 -0
- package/src/runtime/channels/tool-defs.mjs +11 -130
- package/src/runtime/memory/index.mjs +258 -2050
- package/src/runtime/memory/lib/core-memory-store.mjs +351 -1
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
- package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
- package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
- package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
- package/src/runtime/memory/lib/http-wire.mjs +57 -0
- package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
- package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +72 -837
- package/src/runtime/memory/lib/memory-embed.mjs +149 -0
- package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +22 -2
- package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
- package/src/runtime/memory/lib/memory.mjs +20 -0
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
- package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
- package/src/runtime/memory/lib/query-handlers.mjs +780 -0
- package/src/runtime/memory/lib/recall-format.mjs +238 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
- package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
- package/src/runtime/memory/tool-defs.mjs +6 -14
- package/src/runtime/search/lib/http-fetch.mjs +274 -0
- package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
- package/src/runtime/search/lib/web-tools.mjs +24 -602
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +26 -1
- package/src/runtime/shared/background-tasks.mjs +2 -3
- package/src/runtime/shared/buffered-appender.mjs +149 -0
- package/src/runtime/shared/launcher-control.mjs +2 -2
- package/src/runtime/shared/task-notification-envelope.mjs +98 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -2
- package/src/runtime/shared/tool-primitives.mjs +308 -0
- package/src/runtime/shared/tool-result-summary.mjs +515 -0
- package/src/runtime/shared/tool-surface.mjs +80 -898
- package/src/runtime/shared/transcript-writer.mjs +52 -2
- package/src/runtime/shared/update-checker.mjs +7 -4
- package/src/session-runtime/config-helpers.mjs +291 -0
- package/src/session-runtime/config-lifecycle.mjs +232 -0
- package/src/session-runtime/cwd-plugins.mjs +226 -0
- package/src/session-runtime/effort.mjs +128 -0
- package/src/session-runtime/fs-utils.mjs +10 -0
- package/src/session-runtime/mcp-glue.mjs +177 -0
- package/src/session-runtime/model-capabilities.mjs +130 -0
- package/src/session-runtime/model-recency.mjs +111 -0
- package/src/session-runtime/native-search.mjs +247 -0
- package/src/session-runtime/output-styles.mjs +126 -0
- package/src/session-runtime/plugin-mcp.mjs +114 -0
- package/src/session-runtime/prewarm.mjs +142 -0
- package/src/session-runtime/provider-models.mjs +278 -0
- package/src/session-runtime/provider-usage.mjs +120 -0
- package/src/session-runtime/quick-model-rows.mjs +170 -0
- package/src/session-runtime/quick-search-models.mjs +46 -0
- package/src/session-runtime/session-hooks.mjs +93 -0
- package/src/session-runtime/session-text.mjs +100 -0
- package/src/session-runtime/settings-api.mjs +319 -0
- package/src/session-runtime/statusline-route.mjs +35 -0
- package/src/session-runtime/tool-catalog.mjs +720 -0
- package/src/session-runtime/tool-defs.mjs +84 -0
- package/src/session-runtime/warmup-schedulers.mjs +201 -0
- package/src/session-runtime/workflow.mjs +358 -0
- package/src/standalone/agent-tool/helpers.mjs +237 -0
- package/src/standalone/agent-tool/notify.mjs +107 -0
- package/src/standalone/agent-tool/provider-init.mjs +143 -0
- package/src/standalone/agent-tool/render.mjs +152 -0
- package/src/standalone/agent-tool/tool-def.mjs +55 -0
- package/src/standalone/agent-tool.mjs +155 -677
- package/src/standalone/channel-worker.mjs +7 -9
- package/src/standalone/explore-tool.mjs +40 -12
- package/src/standalone/hook-bus/config.mjs +207 -0
- package/src/standalone/hook-bus/constants.mjs +90 -0
- package/src/standalone/hook-bus/handlers.mjs +481 -0
- package/src/standalone/hook-bus/payload.mjs +31 -0
- package/src/standalone/hook-bus/rules.mjs +77 -0
- package/src/standalone/hook-bus.mjs +110 -746
- package/src/standalone/memory-runtime-proxy.mjs +7 -0
- package/src/standalone/opencode-go-login.mjs +125 -0
- package/src/standalone/provider-admin.mjs +15 -19
- package/src/standalone/usage-dashboard.mjs +3 -1
- package/src/tui/App.jsx +1163 -7571
- package/src/tui/app/app-format.mjs +206 -0
- package/src/tui/app/channel-pickers.mjs +510 -0
- package/src/tui/app/clipboard.mjs +67 -0
- package/src/tui/app/core-memory-picker.mjs +210 -0
- package/src/tui/app/extension-pickers.mjs +506 -0
- package/src/tui/app/input-parsers.mjs +193 -0
- package/src/tui/app/maintenance-pickers.mjs +324 -0
- package/src/tui/app/model-options.mjs +330 -0
- package/src/tui/app/model-picker.mjs +365 -0
- package/src/tui/app/onboarding-steps.mjs +400 -0
- package/src/tui/app/project-picker.mjs +247 -0
- package/src/tui/app/provider-setup-picker.mjs +580 -0
- package/src/tui/app/resume-picker.mjs +55 -0
- package/src/tui/app/route-pickers.mjs +419 -0
- package/src/tui/app/settings-picker.mjs +490 -0
- package/src/tui/app/slash-commands.mjs +101 -0
- package/src/tui/app/slash-dispatch.mjs +427 -0
- package/src/tui/app/text-layout.mjs +46 -0
- package/src/tui/app/theme-effort-pickers.mjs +154 -0
- package/src/tui/app/transcript-window.mjs +671 -0
- package/src/tui/app/use-mouse-input.mjs +460 -0
- package/src/tui/app/use-prompt-handlers.mjs +310 -0
- package/src/tui/app/use-transcript-scroll.mjs +510 -0
- package/src/tui/app/use-transcript-window.mjs +589 -0
- package/src/tui/components/ConfirmBar.jsx +1 -1
- package/src/tui/components/Picker.jsx +32 -4
- package/src/tui/components/PromptInput.jsx +259 -80
- package/src/tui/components/SlashCommandPalette.jsx +8 -1
- package/src/tui/components/StatusLine.jsx +63 -12
- package/src/tui/components/TextEntryPanel.jsx +11 -0
- package/src/tui/components/ToolExecution.jsx +56 -588
- package/src/tui/components/TranscriptItem.jsx +105 -0
- package/src/tui/components/UsagePanel.jsx +18 -4
- package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
- package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
- package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
- package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
- package/src/tui/components/tool-execution/text-format.mjs +161 -0
- package/src/tui/components/tool-output-format.mjs +2 -2
- package/src/tui/display-width.mjs +20 -3
- package/src/tui/dist/index.mjs +18034 -17188
- package/src/tui/engine/agent-envelope.mjs +296 -0
- package/src/tui/engine/agent-job-feed.mjs +133 -0
- package/src/tui/engine/boot-profile.mjs +21 -0
- package/src/tui/engine/labels.mjs +67 -0
- package/src/tui/engine/notice-text.mjs +112 -0
- package/src/tui/engine/notification-plan.mjs +76 -0
- package/src/tui/engine/queue-helpers.mjs +161 -0
- package/src/tui/engine/render-timing.mjs +17 -0
- package/src/tui/engine/session-stats.mjs +46 -0
- package/src/tui/engine/tool-approval.mjs +94 -0
- package/src/tui/engine/tool-call-fields.mjs +23 -0
- package/src/tui/engine/tool-card-results.mjs +234 -0
- package/src/tui/engine/tool-result-status.mjs +135 -0
- package/src/tui/engine/tool-result-text.mjs +126 -0
- package/src/tui/engine.mjs +405 -1385
- package/src/tui/figures.mjs +5 -0
- package/src/tui/index.jsx +105 -0
- package/src/tui/input-editing.mjs +60 -10
- package/src/tui/keyboard-protocol.mjs +2 -2
- package/src/tui/lib/voice-recorder.mjs +35 -19
- package/src/tui/markdown/format-token.mjs +11 -9
- package/src/tui/paste-attachments.mjs +38 -0
- package/src/tui/statusline-ansi-bridge.mjs +11 -3
- package/src/tui/theme.mjs +6 -0
- package/src/tui/themes/base.mjs +2 -2
- package/src/tui/themes/kanagawa.mjs +4 -4
- package/src/tui/themes/teal.mjs +4 -5
- package/src/tui/themes/utils.mjs +1 -1
- package/src/ui/statusline-agents.mjs +213 -0
- package/src/ui/statusline-format.mjs +146 -0
- package/src/ui/statusline-segments.mjs +148 -0
- package/src/ui/statusline.mjs +77 -462
- package/src/ui/tool-card.mjs +0 -1
- package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
- package/src/workflows/default/WORKFLOW.md +16 -9
- package/src/workflows/sequential/WORKFLOW.md +16 -11
- package/src/workflows/solo/WORKFLOW.md +5 -1
- package/vendor/ink/build/display-width.js +19 -3
- package/vendor/ink/build/ink.js +103 -6
- package/vendor/ink/build/log-update.js +17 -3
- package/vendor/ink/build/wrap-text.js +125 -0
- package/scripts/_test-folder-dialog.mjs +0 -30
- package/scripts/fix-brief-fn.mjs +0 -35
- package/scripts/fix-format-tool-surface.mjs +0 -24
- package/scripts/fix-tool-exec-visible.mjs +0 -42
- package/scripts/patch-agent-brief.mjs +0 -48
- package/scripts/patch-app.mjs +0 -21
- package/scripts/patch-app2.mjs +0 -18
- package/scripts/patch-dist-brief.mjs +0 -96
- package/scripts/patch-tool-exec.mjs +0 -70
- package/src/examples/schedules/SCHEDULE.example.md +0 -32
- package/src/examples/webhooks/WEBHOOK.example.md +0 -40
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
- package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
- package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
- package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
- package/src/standalone/agent-task-status.test.mjs +0 -76
- package/src/tui/components/tool-output-format.test.mjs +0 -399
- package/src/tui/display-width.test.mjs +0 -35
- package/src/tui/engine-runtime-notification.test.mjs +0 -115
- package/src/tui/engine-tool-result-text.test.mjs +0 -75
- package/src/tui/markdown/format-token.test.mjs +0 -354
- package/src/tui/markdown/render-ansi.test.mjs +0 -108
- package/src/tui/markdown/stream-fence.test.mjs +0 -26
- package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
- package/src/tui/prompt-history-store.test.mjs +0 -52
- package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
- package/src/tui/transcript-tool-failures.test.mjs +0 -111
- package/src/ui/markdown.test.mjs +0 -70
- package/src/ui/statusline-context-label.test.mjs +0 -15
- package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
- package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
|
@@ -1,76 +1,161 @@
|
|
|
1
1
|
import { createRequire } from 'module';
|
|
2
2
|
import { fileURLToPath } from 'url';
|
|
3
3
|
import { randomBytes, createHash } from 'crypto';
|
|
4
|
-
import {
|
|
5
|
-
import { getProvider, providerInputExcludesCache } from '../providers/registry.mjs';
|
|
6
|
-
import { getModelMetadataSync } from '../providers/model-catalog.mjs';
|
|
7
|
-
import { fetchOAuthUsageSnapshot } from '../providers/oauth-usage.mjs';
|
|
4
|
+
import { getProvider } from '../providers/registry.mjs';
|
|
8
5
|
// Image content is kept in-memory and in the model-visible history so multi-turn
|
|
9
|
-
// recognition
|
|
6
|
+
// recognition works reliably (live transcript always retains images). The
|
|
10
7
|
// stored-history placeholder swap now happens only at disk-serialization time
|
|
11
8
|
// inside the session store, so it is no longer imported here.
|
|
12
9
|
import {
|
|
13
|
-
recallFastTrackCompactMessages,
|
|
14
|
-
semanticCompactMessages,
|
|
15
|
-
pruneToolOutputsUnanchored,
|
|
16
|
-
effectiveBudget as compactEffectiveBudget,
|
|
17
|
-
compactTypeIsRecallFastTrack,
|
|
18
|
-
compactTypeIsSemantic,
|
|
19
10
|
normalizeCompactType,
|
|
20
11
|
DEFAULT_COMPACT_TYPE,
|
|
21
12
|
SUMMARY_PREFIX,
|
|
22
|
-
drainSessionCycle1,
|
|
23
13
|
} from './compact.mjs';
|
|
24
|
-
import { estimateMessagesTokens,
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import { PATCH_TOOL_DEFS } from '../tools/patch-tool-defs.mjs';
|
|
29
|
-
import { CODE_GRAPH_TOOL_DEFS } from '../tools/code-graph-tool-defs.mjs';
|
|
30
|
-
import { collectSkillsCached, buildSkillManifest, buildSkillToolDefs, composeSystemPrompt } from '../context/collect.mjs';
|
|
31
|
-
import { saveSession, saveSessionAsync, loadSession, listStoredSessionSummaries, sweepStaleSessions, markSessionClosed, publishHeartbeat, deleteHeartbeat, setLiveSession } from './store.mjs';
|
|
14
|
+
import { estimateMessagesTokens, estimateTranscriptContextUsage } from './context-utils.mjs';
|
|
15
|
+
import { executeInternalTool } from '../internal-tools.mjs';
|
|
16
|
+
import { collectSkillsCached, buildSkillManifest, composeSystemPrompt } from '../context/collect.mjs';
|
|
17
|
+
import { saveSession, saveSessionAsync, loadSession, listStoredSessionSummaries, sweepStaleSessions, markSessionClosed, bumpSessionGeneration, setLiveSession } from './store.mjs';
|
|
32
18
|
import { clearReadDedupSession, tryPrefetchCached, setPrefetchCached } from './read-dedup.mjs';
|
|
33
19
|
import { clearOffloadSession } from './tool-result-offload.mjs';
|
|
34
20
|
import { classifyResultKind } from './result-classification.mjs';
|
|
35
21
|
import { createAbortController } from '../../../shared/abort-controller.mjs';
|
|
36
|
-
import { isInternalRuntimeNotificationText as contractIsInternalRuntimeNotificationText } from '../../../shared/tool-execution-contract.mjs';
|
|
37
22
|
import { logLlmCall } from '../../../shared/llm/usage-log.mjs';
|
|
38
|
-
import { resolvePluginData, mixdogRoot } from '../../../shared/plugin-paths.mjs';
|
|
39
|
-
import { updateJsonAtomicSync } from '../../../shared/atomic-file.mjs';
|
|
40
23
|
import { appendAgentTrace } from '../agent-trace.mjs';
|
|
41
24
|
import { isAgentOwner } from '../agent-owner.mjs';
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
25
|
+
import { getHiddenAgent } from '../internal-agents.mjs';
|
|
26
|
+
import { loadConfig } from '../config.mjs';
|
|
27
|
+
import { buildProviderCacheOpts, cacheCapabilityForProvider } from '../agent-runtime/cache-strategy.mjs';
|
|
28
|
+
import { normalizeAutoClearConfig, resolveAutoClearIdleMs } from '../../../../session-runtime/config-helpers.mjs';
|
|
45
29
|
import {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
30
|
+
recordStandaloneStatusTelemetry,
|
|
31
|
+
} from './manager/status-telemetry.mjs';
|
|
32
|
+
import {
|
|
33
|
+
normalizeStaleCompactingStage,
|
|
34
|
+
runSessionCompaction,
|
|
35
|
+
} from './manager/compaction-runner.mjs';
|
|
36
|
+
// Split modules — see manager/ directory. manager.mjs is now a thin facade that
|
|
37
|
+
// orchestrates these cohesive units while keeping the exact public surface.
|
|
38
|
+
import {
|
|
39
|
+
_buildSharedRules,
|
|
40
|
+
_buildAgentRules,
|
|
41
|
+
_buildLeadRules,
|
|
42
|
+
_buildLeadMetaContext,
|
|
43
|
+
_buildAgentSpecific,
|
|
44
|
+
} from './manager/rules-cache.mjs';
|
|
45
|
+
import {
|
|
46
|
+
applyToolPermissionNarrowing,
|
|
47
|
+
finalizeSessionToolList,
|
|
48
|
+
resolveSessionTools,
|
|
49
|
+
previewSessionTools,
|
|
50
|
+
permissionFromToolSpec,
|
|
51
|
+
} from './manager/tool-resolution.mjs';
|
|
52
|
+
import {
|
|
53
|
+
guessContextWindow,
|
|
54
|
+
positiveContextWindow,
|
|
55
|
+
preserveBufferConfigFields,
|
|
56
|
+
resolveSessionContextMeta,
|
|
57
|
+
compactTriggerForSession,
|
|
58
|
+
} from './manager/context-meta.mjs';
|
|
59
|
+
import {
|
|
60
|
+
promptContentText,
|
|
61
|
+
hasModelVisiblePromptContent,
|
|
62
|
+
promptContentBytes,
|
|
63
|
+
prefixUserTurnContent,
|
|
64
|
+
prefixSessionStartContent,
|
|
65
|
+
buildCurrentTimeBlock,
|
|
66
|
+
buildSessionStartBlock,
|
|
67
|
+
hasUserConversationMessage,
|
|
68
|
+
isInternalRuntimeNotificationText,
|
|
69
|
+
} from './manager/prompt-utils.mjs';
|
|
70
|
+
import {
|
|
71
|
+
_mergePendingMessageEntries,
|
|
72
|
+
enqueuePendingMessage,
|
|
73
|
+
drainPendingMessages,
|
|
74
|
+
_dropPendingMessageState,
|
|
75
|
+
} from './manager/pending-messages.mjs';
|
|
76
|
+
import {
|
|
77
|
+
bumpUsageMetricsTurnId,
|
|
78
|
+
resolveUsageMetricsTurnId,
|
|
79
|
+
bumpUsageMetricsEpoch,
|
|
80
|
+
resolveUsageMetricsEpoch,
|
|
81
|
+
usageMetricsSourceKey,
|
|
82
|
+
usageMetricsIdempotencyKey,
|
|
83
|
+
uncachedInputTokensForProvider,
|
|
84
|
+
applyAskTerminalUsageTotals,
|
|
85
|
+
persistIterationMetrics,
|
|
86
|
+
} from './manager/usage-metrics.mjs';
|
|
87
|
+
// Runtime-liveness map + accessors — extracted (pass-3) to
|
|
88
|
+
// manager/runtime-liveness.mjs. State is a module-level singleton there,
|
|
89
|
+
// preserving the baseline single-process shape. manager.mjs keeps askSession's
|
|
90
|
+
// controller/generation lifecycle and calls these via imports. The two
|
|
91
|
+
// _get/_entries accessors expose the private Map for in-place mutation from
|
|
92
|
+
// closeSession / idle-sweep / askSession.
|
|
93
|
+
import {
|
|
94
|
+
configureRuntimeLiveness,
|
|
95
|
+
updateSessionStage,
|
|
96
|
+
markSessionAskStart,
|
|
97
|
+
markSessionStreamDelta,
|
|
98
|
+
markSessionToolCall,
|
|
99
|
+
markSessionDone,
|
|
100
|
+
markSessionEmptyFinal,
|
|
101
|
+
markSessionError,
|
|
102
|
+
markSessionCancelled,
|
|
103
|
+
getSessionRuntime,
|
|
104
|
+
isSessionCompactionBlocked,
|
|
105
|
+
getSessionProgressSnapshot,
|
|
106
|
+
forEachSessionRuntime,
|
|
107
|
+
hideSessionFromList,
|
|
108
|
+
getSessionAbortSignal,
|
|
109
|
+
getSessionLastProgressAt,
|
|
110
|
+
linkParentSignalToSession,
|
|
111
|
+
_touchRuntime,
|
|
112
|
+
_stopToolActivityHeartbeat,
|
|
113
|
+
_unlinkParentAbortListener,
|
|
114
|
+
_clearSessionRuntime,
|
|
115
|
+
_getRuntimeEntry,
|
|
116
|
+
_runtimeEntries,
|
|
117
|
+
} from './manager/runtime-liveness.mjs';
|
|
118
|
+
// Facade re-exports — external importers (loop.mjs, loop/tool-exec.mjs,
|
|
119
|
+
// agent-dispatch.mjs, abort-lookup.mjs, statusline-agents.mjs) resolve these
|
|
120
|
+
// through manager.mjs unchanged.
|
|
121
|
+
export {
|
|
122
|
+
updateSessionStage,
|
|
123
|
+
markSessionAskStart,
|
|
124
|
+
markSessionStreamDelta,
|
|
125
|
+
markSessionToolCall,
|
|
126
|
+
markSessionDone,
|
|
127
|
+
markSessionEmptyFinal,
|
|
128
|
+
markSessionError,
|
|
129
|
+
markSessionCancelled,
|
|
130
|
+
getSessionRuntime,
|
|
131
|
+
isSessionCompactionBlocked,
|
|
132
|
+
getSessionProgressSnapshot,
|
|
133
|
+
forEachSessionRuntime,
|
|
134
|
+
hideSessionFromList,
|
|
135
|
+
getSessionAbortSignal,
|
|
136
|
+
getSessionLastProgressAt,
|
|
137
|
+
linkParentSignalToSession,
|
|
138
|
+
};
|
|
139
|
+
// Wire the store deps the liveness module needs without importing store.mjs
|
|
140
|
+
// back into it via manager.mjs (avoids re-entry / keeps one store contract).
|
|
141
|
+
configureRuntimeLiveness({ loadSession, saveSessionAsync });
|
|
142
|
+
// Re-export split-module public/test surface unchanged so every importer of the
|
|
143
|
+
// old symbol names keeps resolving through the facade.
|
|
144
|
+
export { previewSessionTools };
|
|
145
|
+
export { _mergePendingMessageEntries, enqueuePendingMessage, drainPendingMessages };
|
|
146
|
+
export { isInternalRuntimeNotificationText as _isInternalRuntimeNotificationText };
|
|
147
|
+
// Usage-metrics surface — re-exported unchanged so loop.mjs / smoke scripts keep
|
|
148
|
+
// resolving these through the facade.
|
|
149
|
+
export {
|
|
150
|
+
bumpUsageMetricsTurnId,
|
|
151
|
+
resolveUsageMetricsTurnId,
|
|
152
|
+
bumpUsageMetricsEpoch,
|
|
153
|
+
resolveUsageMetricsEpoch,
|
|
154
|
+
usageMetricsSourceKey,
|
|
155
|
+
usageMetricsIdempotencyKey,
|
|
156
|
+
applyAskTerminalUsageTotals,
|
|
157
|
+
persistIterationMetrics,
|
|
158
|
+
};
|
|
74
159
|
let _codeGraphRuntimePromise = null;
|
|
75
160
|
let _agentLoopPromise = null;
|
|
76
161
|
let _bashSessionRuntimePromise = null;
|
|
@@ -93,127 +178,9 @@ function _closeBashSessionLazy(sessionId, reason) {
|
|
|
93
178
|
.then((mod) => { if (typeof mod.closeBashSession === 'function') mod.closeBashSession(sessionId, reason); })
|
|
94
179
|
.catch(() => {});
|
|
95
180
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const RULES_DIR = join(PLUGIN_ROOT, 'rules');
|
|
100
|
-
const mtime = maxMtimeRecursive([
|
|
101
|
-
join(RULES_DIR, 'shared'),
|
|
102
|
-
]);
|
|
103
|
-
if (_sharedRulesCache !== null && mtime <= _sharedRulesMtime) {
|
|
104
|
-
return _sharedRulesCache;
|
|
105
|
-
}
|
|
106
|
-
try {
|
|
107
|
-
const built = _rulesBuilder.buildSharedToolContent({ PLUGIN_ROOT, DATA_DIR: resolvePluginData() });
|
|
108
|
-
_sharedRulesCache = built;
|
|
109
|
-
_sharedRulesMtime = mtime;
|
|
110
|
-
return built;
|
|
111
|
-
} catch (e) {
|
|
112
|
-
throw new Error(`[session] shared tool rules build failed: ${e.message}`);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function _buildAgentRules(profile = 'full') {
|
|
117
|
-
if (!_rulesBuilder || typeof _rulesBuilder.buildAgentRoleContent !== 'function') return '';
|
|
118
|
-
const key = String(profile || 'full');
|
|
119
|
-
const PLUGIN_ROOT = mixdogRoot();
|
|
120
|
-
const DATA_DIR = resolvePluginData();
|
|
121
|
-
const RULES_DIR = join(PLUGIN_ROOT, 'rules');
|
|
122
|
-
const mtime = maxMtimeRecursive([
|
|
123
|
-
join(RULES_DIR, 'agent'),
|
|
124
|
-
join(DATA_DIR, 'mixdog-config.json'),
|
|
125
|
-
]);
|
|
126
|
-
const cached = _agentRulesCacheByProfile.get(key);
|
|
127
|
-
if (cached && mtime <= cached.mtime) {
|
|
128
|
-
return cached.value;
|
|
129
|
-
}
|
|
130
|
-
try {
|
|
131
|
-
const built = _rulesBuilder.buildAgentRoleContent({ PLUGIN_ROOT, DATA_DIR, profile: key });
|
|
132
|
-
_agentRulesCacheByProfile.set(key, { mtime, value: built });
|
|
133
|
-
return built;
|
|
134
|
-
} catch (e) {
|
|
135
|
-
throw new Error(`[session] agent role rules build failed: ${e.message}`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function _buildLeadRules() {
|
|
140
|
-
if (!_rulesBuilder || typeof _rulesBuilder.buildLeadRoleContent !== 'function') return '';
|
|
141
|
-
const PLUGIN_ROOT = mixdogRoot();
|
|
142
|
-
const DATA_DIR = resolvePluginData();
|
|
143
|
-
const RULES_DIR = join(PLUGIN_ROOT, 'rules');
|
|
144
|
-
const mtime = maxMtimeRecursive([
|
|
145
|
-
join(RULES_DIR, 'lead'),
|
|
146
|
-
join(DATA_DIR, 'mixdog-config.json'),
|
|
147
|
-
]);
|
|
148
|
-
if (_leadRulesCache !== null && mtime <= _leadRulesMtime) {
|
|
149
|
-
return _leadRulesCache;
|
|
150
|
-
}
|
|
151
|
-
try {
|
|
152
|
-
const built = _rulesBuilder.buildLeadRoleContent({ PLUGIN_ROOT, DATA_DIR });
|
|
153
|
-
_leadRulesCache = built;
|
|
154
|
-
_leadRulesMtime = mtime;
|
|
155
|
-
return built;
|
|
156
|
-
} catch (e) {
|
|
157
|
-
throw new Error(`[session] lead role rules build failed: ${e.message}`);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function _buildLeadMetaContext() {
|
|
162
|
-
if (!_rulesBuilder || typeof _rulesBuilder.buildLeadMetaContent !== 'function') return '';
|
|
163
|
-
const PLUGIN_ROOT = mixdogRoot();
|
|
164
|
-
const DATA_DIR = resolvePluginData();
|
|
165
|
-
const RULES_DIR = join(PLUGIN_ROOT, 'rules');
|
|
166
|
-
const mtime = maxMtimeRecursive([
|
|
167
|
-
join(RULES_DIR, 'lead'),
|
|
168
|
-
join(DATA_DIR, 'history'),
|
|
169
|
-
join(DATA_DIR, 'mixdog-config.json'),
|
|
170
|
-
join(DATA_DIR, 'user-workflow.md'),
|
|
171
|
-
join(PLUGIN_ROOT, 'output-styles'),
|
|
172
|
-
join(DATA_DIR, 'output-styles'),
|
|
173
|
-
]);
|
|
174
|
-
if (_leadMetaCache !== null && mtime <= _leadMetaMtime) {
|
|
175
|
-
return _leadMetaCache;
|
|
176
|
-
}
|
|
177
|
-
try {
|
|
178
|
-
const built = _rulesBuilder.buildLeadMetaContent({ PLUGIN_ROOT, DATA_DIR });
|
|
179
|
-
_leadMetaCache = built;
|
|
180
|
-
_leadMetaMtime = mtime;
|
|
181
|
-
return built;
|
|
182
|
-
} catch (e) {
|
|
183
|
-
throw new Error(`[session] lead meta context build failed: ${e.message}`);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// BP4-adjacent agent-specific data cache — keyed by agent. webhook / schedule
|
|
188
|
-
// agents each have their own scoped instruction set; other agents return ''.
|
|
189
|
-
const _roleSpecificCache = new Map(); // agent → { value, mtime }
|
|
190
|
-
function _buildAgentSpecific(currentAgent) {
|
|
191
|
-
if (!_rulesBuilder || typeof _rulesBuilder.buildAgentRoleSpecificContent !== 'function') return '';
|
|
192
|
-
if (!currentAgent) return '';
|
|
193
|
-
const PLUGIN_ROOT = mixdogRoot();
|
|
194
|
-
const DATA_DIR = resolvePluginData();
|
|
195
|
-
const RULES_DIR = join(PLUGIN_ROOT, 'rules');
|
|
196
|
-
const roleInstructionDir = getAgentInstructionDir(currentAgent);
|
|
197
|
-
const mtime = maxMtimeRecursive([
|
|
198
|
-
join(RULES_DIR, 'shared'),
|
|
199
|
-
join(DATA_DIR, 'mixdog-config.json'),
|
|
200
|
-
join(DATA_DIR, 'webhooks'),
|
|
201
|
-
join(DATA_DIR, 'schedules'),
|
|
202
|
-
...(roleInstructionDir ? [join(DATA_DIR, roleInstructionDir)] : []),
|
|
203
|
-
join(PLUGIN_ROOT, 'defaults', 'agents.json'),
|
|
204
|
-
]);
|
|
205
|
-
const entry = _roleSpecificCache.get(currentAgent);
|
|
206
|
-
if (entry && mtime <= entry.mtime) {
|
|
207
|
-
return entry.value;
|
|
208
|
-
}
|
|
209
|
-
try {
|
|
210
|
-
const built = _rulesBuilder.buildAgentRoleSpecificContent({ PLUGIN_ROOT, DATA_DIR, currentAgent });
|
|
211
|
-
_roleSpecificCache.set(currentAgent, { mtime, value: built });
|
|
212
|
-
return built;
|
|
213
|
-
} catch (e) {
|
|
214
|
-
throw new Error(`[session] agent-specific rules build failed (agent: ${currentAgent}): ${e.message}`);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
181
|
+
// Re-export the rules builders so any deep importer of the old symbol names
|
|
182
|
+
// keeps working through the facade.
|
|
183
|
+
export { _buildSharedRules, _buildAgentRules, _buildLeadRules, _buildLeadMetaContext, _buildAgentSpecific };
|
|
217
184
|
|
|
218
185
|
// Agent Runtime is optional — injected via setAgentRuntime() during plugin init
|
|
219
186
|
// so session creation never depends on a circular import. If never injected,
|
|
@@ -250,7 +217,6 @@ export class SessionClosedError extends Error {
|
|
|
250
217
|
this.reason = closeReason || null;
|
|
251
218
|
}
|
|
252
219
|
}
|
|
253
|
-
const HEARTBEAT_THROTTLE_MS = 60_000; // 60s
|
|
254
220
|
// Cap how long the terminal unwind blocks on the post-result session save.
|
|
255
221
|
// The result is already produced (and relayed for agent surfaces) before this
|
|
256
222
|
// save, so a stalled disk write must not hold askSession() open — otherwise the
|
|
@@ -258,34 +224,10 @@ const HEARTBEAT_THROTTLE_MS = 60_000; // 60s
|
|
|
258
224
|
// notification never fires. A slow write finishes in the background.
|
|
259
225
|
const TERMINAL_SAVE_TIMEOUT_MS = nonNegativeIntEnv('MIXDOG_TERMINAL_SAVE_TIMEOUT_MS', 5_000);
|
|
260
226
|
|
|
261
|
-
//
|
|
262
|
-
// (
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
// Sorted deterministically by name — protects BP_1 hash stability from
|
|
266
|
-
// listTools() ordering churn. Anthropic / OpenAI / Gemini all hash the
|
|
267
|
-
// tools array verbatim, so any reorder rewrites the prefix.
|
|
268
|
-
// No cache: getMcpTools() and getInternalTools() are O(n) in-memory reads;
|
|
269
|
-
// the sort overhead on ~30 tools is negligible.
|
|
270
|
-
function _getMcpTools() {
|
|
271
|
-
const mcp = getMcpTools() || [];
|
|
272
|
-
const internalRaw = getInternalTools() || [];
|
|
273
|
-
const internal = internalRaw.map(t => ({
|
|
274
|
-
name: t.name,
|
|
275
|
-
description: typeof t.description === 'string' ? t.description : '',
|
|
276
|
-
inputSchema: t.inputSchema || { type: 'object', properties: {} },
|
|
277
|
-
// Keep annotations so the permission filter / role invariants can
|
|
278
|
-
// tell read-only from write-capable internal tools, and so
|
|
279
|
-
// agentHidden can be read during deny filtering.
|
|
280
|
-
annotations: t.annotations || {},
|
|
281
|
-
}));
|
|
282
|
-
return [...mcp, ...internal].sort((a, b) => {
|
|
283
|
-
const an = a?.name || '';
|
|
284
|
-
const bn = b?.name || '';
|
|
285
|
-
return an < bn ? -1 : an > bn ? 1 : 0;
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
|
|
227
|
+
// Session tool resolution + permission narrowing moved to
|
|
228
|
+
// manager/tool-resolution.mjs (imported above). The following section-level
|
|
229
|
+
// docs describe the toolSpec contract those helpers implement.
|
|
230
|
+
//
|
|
289
231
|
// Phase D-2 — profile.tools resolution.
|
|
290
232
|
//
|
|
291
233
|
// `toolSpec` may be:
|
|
@@ -308,934 +250,12 @@ function _getMcpTools() {
|
|
|
308
250
|
// surface intentionally tiny; runtime permission guards in loop.mjs remain
|
|
309
251
|
// the fail-safe either way.
|
|
310
252
|
|
|
311
|
-
const SESSION_ROUTE_TOOL_ORDER = [
|
|
312
|
-
'code_graph',
|
|
313
|
-
'find',
|
|
314
|
-
'glob',
|
|
315
|
-
'list',
|
|
316
|
-
'grep',
|
|
317
|
-
'read',
|
|
318
|
-
'apply_patch',
|
|
319
|
-
'shell',
|
|
320
|
-
'task',
|
|
321
|
-
];
|
|
322
|
-
const SESSION_ROUTE_TOOL_RANK = new Map(SESSION_ROUTE_TOOL_ORDER.map((name, index) => [name, index]));
|
|
323
|
-
const FILESYSTEM_TOOL_NAMES = new Set([
|
|
324
|
-
'code_graph',
|
|
325
|
-
'find',
|
|
326
|
-
'glob',
|
|
327
|
-
'list',
|
|
328
|
-
'grep',
|
|
329
|
-
'read',
|
|
330
|
-
'apply_patch',
|
|
331
|
-
]);
|
|
332
|
-
const READONLY_TOOL_NAMES = new Set([
|
|
333
|
-
'code_graph',
|
|
334
|
-
'find',
|
|
335
|
-
'glob',
|
|
336
|
-
'list',
|
|
337
|
-
'grep',
|
|
338
|
-
'read',
|
|
339
|
-
]);
|
|
340
|
-
|
|
341
|
-
const AGENT_STRING_PERMISSION_READ_ALLOW = Object.freeze([
|
|
342
|
-
'code_graph',
|
|
343
|
-
'find',
|
|
344
|
-
'glob',
|
|
345
|
-
'list',
|
|
346
|
-
'grep',
|
|
347
|
-
'read',
|
|
348
|
-
'explore',
|
|
349
|
-
'search',
|
|
350
|
-
'web_fetch',
|
|
351
|
-
'Skill',
|
|
352
|
-
]);
|
|
353
|
-
|
|
354
|
-
function stringToolPermissionAllowList(toolPermission) {
|
|
355
|
-
if (toolPermission === 'read') return AGENT_STRING_PERMISSION_READ_ALLOW;
|
|
356
|
-
if (toolPermission === 'read-write') return AGENT_STRING_PERMISSION_READ_WRITE_ALLOW;
|
|
357
|
-
if (toolPermission === 'none') return [];
|
|
358
|
-
return null;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const AGENT_STRING_PERMISSION_READ_WRITE_ALLOW = Object.freeze([
|
|
362
|
-
'code_graph',
|
|
363
|
-
'find',
|
|
364
|
-
'glob',
|
|
365
|
-
'list',
|
|
366
|
-
'grep',
|
|
367
|
-
'read',
|
|
368
|
-
'apply_patch',
|
|
369
|
-
'explore',
|
|
370
|
-
'search',
|
|
371
|
-
'web_fetch',
|
|
372
|
-
'Skill',
|
|
373
|
-
]);
|
|
374
|
-
|
|
375
|
-
function applyToolPermissionNarrowing(tools, toolPermission, warnRole = null) {
|
|
376
|
-
if (toolPermission === 'none') return [];
|
|
377
|
-
const allowList = stringToolPermissionAllowList(toolPermission);
|
|
378
|
-
if (allowList) {
|
|
379
|
-
const allowSet = new Set(allowList.map((n) => String(n).toLowerCase()));
|
|
380
|
-
return tools.filter((t) => allowSet.has(String(t?.name || '').toLowerCase()));
|
|
381
|
-
}
|
|
382
|
-
if (toolPermission && typeof toolPermission === 'object') {
|
|
383
|
-
const allowSet = Array.isArray(toolPermission.allow) && toolPermission.allow.length > 0
|
|
384
|
-
? new Set(toolPermission.allow.map(n => String(n).toLowerCase()))
|
|
385
|
-
: null;
|
|
386
|
-
const denySet = Array.isArray(toolPermission.deny) && toolPermission.deny.length > 0
|
|
387
|
-
? new Set(toolPermission.deny.map(n => String(n).toLowerCase()))
|
|
388
|
-
: null;
|
|
389
|
-
if (allowSet || denySet) {
|
|
390
|
-
const filtered = tools.filter(t => {
|
|
391
|
-
const name = String(t?.name || '').toLowerCase();
|
|
392
|
-
if (denySet && denySet.has(name)) return false;
|
|
393
|
-
if (allowSet && !allowSet.has(name)) return false;
|
|
394
|
-
return true;
|
|
395
|
-
});
|
|
396
|
-
if (filtered.length === 0) {
|
|
397
|
-
process.stderr.write(`[session] WARN: role permission intersection produced 0 tools — failing closed (role=${warnRole || 'unknown'})\n`);
|
|
398
|
-
}
|
|
399
|
-
return filtered;
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
return tools;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function recursiveWrapperToolNameForPublicAgent(agent) {
|
|
406
|
-
if (!agent) return null;
|
|
407
|
-
const key = String(agent).trim();
|
|
408
|
-
if (key === 'explore') return 'explore';
|
|
409
|
-
for (const hiddenName of listHiddenAgentNames()) {
|
|
410
|
-
const def = getHiddenAgent(hiddenName);
|
|
411
|
-
const invokedBy = typeof def?.invokedBy === 'string' ? def.invokedBy.trim() : '';
|
|
412
|
-
if (hiddenName === key && invokedBy) return invokedBy;
|
|
413
|
-
if (invokedBy && invokedBy === key) return invokedBy;
|
|
414
|
-
}
|
|
415
|
-
return null;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
function finalizeSessionToolList(tools, {
|
|
419
|
-
schemaAllowedTools = null,
|
|
420
|
-
disallowedTools = null,
|
|
421
|
-
ownerIsAgent = false,
|
|
422
|
-
resolvedAgent = null,
|
|
423
|
-
} = {}) {
|
|
424
|
-
let out = Array.isArray(tools) ? tools : [];
|
|
425
|
-
const hasCallerAllow = Array.isArray(schemaAllowedTools);
|
|
426
|
-
if (hasCallerAllow) {
|
|
427
|
-
const allowSet = new Set(schemaAllowedTools.map(n => String(n).toLowerCase()));
|
|
428
|
-
out = out.filter(t => allowSet.has(String(t?.name || '').toLowerCase()));
|
|
429
|
-
}
|
|
430
|
-
const callerDeny = Array.isArray(disallowedTools) ? disallowedTools.map(n => String(n)) : [];
|
|
431
|
-
if (callerDeny.length) {
|
|
432
|
-
const denySet = new Set(callerDeny.map(n => n.toLowerCase()));
|
|
433
|
-
out = out.filter(t => !denySet.has(String(t?.name || '').toLowerCase()));
|
|
434
|
-
}
|
|
435
|
-
const recursiveDeny = ownerIsAgent ? recursiveWrapperToolNameForPublicAgent(resolvedAgent) : null;
|
|
436
|
-
if (recursiveDeny) {
|
|
437
|
-
const deny = recursiveDeny.toLowerCase();
|
|
438
|
-
out = out.filter(t => String(t?.name || '').toLowerCase() !== deny);
|
|
439
|
-
}
|
|
440
|
-
if (ownerIsAgent) {
|
|
441
|
-
out = out.filter(t => !t?.annotations?.agentHidden);
|
|
442
|
-
out = orderSessionTools(out);
|
|
443
|
-
}
|
|
444
|
-
return out;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
function orderSessionTools(tools) {
|
|
448
|
-
return tools.map((tool, index) => ({ tool, index }))
|
|
449
|
-
.sort((a, b) => {
|
|
450
|
-
const ar = SESSION_ROUTE_TOOL_RANK.get(a.tool?.name) ?? 10_000;
|
|
451
|
-
const br = SESSION_ROUTE_TOOL_RANK.get(b.tool?.name) ?? 10_000;
|
|
452
|
-
if (ar !== br) return ar - br;
|
|
453
|
-
return a.index - b.index;
|
|
454
|
-
})
|
|
455
|
-
.map((entry) => entry.tool);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
const ALL_BUILTIN_SESSION_TOOLS = orderSessionTools(_dedupByName([
|
|
459
|
-
...BUILTIN_TOOLS,
|
|
460
|
-
...PATCH_TOOL_DEFS,
|
|
461
|
-
...CODE_GRAPH_TOOL_DEFS,
|
|
462
|
-
]));
|
|
463
|
-
|
|
464
|
-
function resolveSessionTools(toolSpec, skills, { ownerIsAgentSession = false } = {}) {
|
|
465
|
-
const mcp = _getMcpTools();
|
|
466
|
-
// Agent sessions freeze the skill meta-tool into the schema
|
|
467
|
-
// unconditionally — concrete skill resolution is cwd-scoped at tool-call
|
|
468
|
-
// time (loop.mjs), so the schema bytes stay bit-identical across roles /
|
|
469
|
-
// cwds and the provider cache shard does not fragment.
|
|
470
|
-
const skillTools = buildSkillToolDefs(skills, { ownerIsAgentSession });
|
|
471
|
-
return _computeBaseTools(toolSpec, mcp, skillTools);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
export function previewSessionTools(toolSpec, skills = [], options = {}) {
|
|
475
|
-
return resolveSessionTools(toolSpec, skills, options);
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// Dedup by name, first occurrence wins. BUILTIN_TOOLS is passed in ahead
|
|
479
|
-
// of the MCP-registered internal tools so plugin-side definitions take
|
|
480
|
-
// precedence when both surfaces declare the same name (e.g. read / grep / glob).
|
|
481
|
-
// Without this merge, Anthropic rejected the request with
|
|
482
|
-
// "tools: Tool names must be unique" and the orchestrator burned up to
|
|
483
|
-
// 20 iterations retrying before the final answer landed.
|
|
484
|
-
function _dedupByName(tools) {
|
|
485
|
-
const seen = new Map();
|
|
486
|
-
for (const t of tools) {
|
|
487
|
-
const n = t?.name;
|
|
488
|
-
if (!n || seen.has(n)) continue;
|
|
489
|
-
seen.set(n, t);
|
|
490
|
-
}
|
|
491
|
-
return [...seen.values()];
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
// Agent visibility is declared per-tool via annotations.agentHidden.
|
|
495
|
-
// Tools with agentHidden:true are stripped from agent sessions at schema
|
|
496
|
-
// build time (see deny filtering below). No code-level name list needed.
|
|
497
|
-
|
|
498
|
-
function _computeBaseTools(toolSpec, mcp, skillTools) {
|
|
499
|
-
if (Array.isArray(toolSpec)) {
|
|
500
|
-
if (toolSpec.length === 0) {
|
|
501
|
-
// Explicit "no tools" — skill meta tools still travel so the model
|
|
502
|
-
// can at least discover and invoke skills if that is the one
|
|
503
|
-
// dynamic surface the profile retains.
|
|
504
|
-
return _dedupByName([...skillTools]);
|
|
505
|
-
}
|
|
506
|
-
if (toolSpec.includes('full')) {
|
|
507
|
-
return _dedupByName([...ALL_BUILTIN_SESSION_TOOLS, ...mcp, ...skillTools]);
|
|
508
|
-
}
|
|
509
|
-
const byName = new Map();
|
|
510
|
-
const add = (tool) => { if (tool?.name && !byName.has(tool.name)) byName.set(tool.name, tool); };
|
|
511
|
-
const addMany = (arr) => { for (const t of arr) add(t); };
|
|
512
|
-
for (const tagRaw of toolSpec) {
|
|
513
|
-
const tag = String(tagRaw || '').trim();
|
|
514
|
-
switch (tag) {
|
|
515
|
-
case 'tools:filesystem':
|
|
516
|
-
addMany(ALL_BUILTIN_SESSION_TOOLS.filter(t => FILESYSTEM_TOOL_NAMES.has(t.name)));
|
|
517
|
-
break;
|
|
518
|
-
case 'tools:readonly':
|
|
519
|
-
addMany(ALL_BUILTIN_SESSION_TOOLS.filter(t => READONLY_TOOL_NAMES.has(t.name)));
|
|
520
|
-
break;
|
|
521
|
-
case 'tools:shell':
|
|
522
|
-
case 'tools:git':
|
|
523
|
-
case 'tools:analysis':
|
|
524
|
-
// Shell-class toolset. `tools:git` / `tools:analysis` exist so
|
|
525
|
-
// profile authors can name the intent (git workflows / data
|
|
526
|
-
// analysis) without inventing new toolset ids.
|
|
527
|
-
addMany(ALL_BUILTIN_SESSION_TOOLS.filter(t => t.name === 'shell' || t.name === 'task'));
|
|
528
|
-
break;
|
|
529
|
-
case 'tools:mcp':
|
|
530
|
-
addMany(mcp);
|
|
531
|
-
break;
|
|
532
|
-
case 'tools:search':
|
|
533
|
-
// Name-pattern match: picks up `search` and any future tool
|
|
534
|
-
// whose name contains `search`. `recall` and `explore` deliberately do NOT match
|
|
535
|
-
// — they need `tools:mcp` (full mcp surface) or their own
|
|
536
|
-
// toolset id if a role wants targeted retrieval. Public agent
|
|
537
|
-
// roles never reach the wrapper bodies regardless: see the
|
|
538
|
-
// isBlockedPublicWrapperCall guard in session/loop.mjs.
|
|
539
|
-
addMany(mcp.filter(t => /search/i.test(t?.name || '')));
|
|
540
|
-
break;
|
|
541
|
-
default:
|
|
542
|
-
process.stderr.write(`[session] unknown toolset id "${tag}" (profile.tools); skipping\n`);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
return _dedupByName([...byName.values(), ...skillTools]);
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
switch (toolSpec) {
|
|
549
|
-
case 'mcp':
|
|
550
|
-
return _dedupByName([...mcp, ...skillTools]);
|
|
551
|
-
case 'readonly': {
|
|
552
|
-
const readTools = ALL_BUILTIN_SESSION_TOOLS.filter(t => READONLY_TOOL_NAMES.has(t.name));
|
|
553
|
-
return _dedupByName([...readTools, ...mcp, ...skillTools]);
|
|
554
|
-
}
|
|
555
|
-
case 'full':
|
|
556
|
-
default:
|
|
557
|
-
return _dedupByName([...ALL_BUILTIN_SESSION_TOOLS, ...mcp, ...skillTools]);
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
function permissionFromToolSpec(toolSpec) {
|
|
562
|
-
if (toolSpec === 'readonly') return 'read';
|
|
563
|
-
if (toolSpec === 'mcp') return 'mcp';
|
|
564
|
-
if (Array.isArray(toolSpec)) {
|
|
565
|
-
const tags = new Set(toolSpec.map(t => String(t || '').trim()));
|
|
566
|
-
const hasWriteOrShell = tags.has('full')
|
|
567
|
-
|| tags.has('tools:filesystem')
|
|
568
|
-
|| tags.has('tools:shell')
|
|
569
|
-
|| tags.has('tools:git')
|
|
570
|
-
|| tags.has('tools:analysis');
|
|
571
|
-
if (tags.has('tools:readonly') && !hasWriteOrShell) return 'read';
|
|
572
|
-
}
|
|
573
|
-
return null;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
253
|
let nextId = Date.now();
|
|
577
|
-
// Known context windows for the current-generation models this plugin
|
|
578
|
-
// routes to. Anything not listed falls through to guessContextWindow() —
|
|
579
|
-
// local llama/mistral/phi default to 8192, everything else 128000. Keep
|
|
580
|
-
// this map trimmed to live models; older generations slow down reads
|
|
581
|
-
// without buying anything.
|
|
582
|
-
const CONTEXT_WINDOWS = {
|
|
583
|
-
// OpenAI GPT-5.x family (openai / openai-oauth)
|
|
584
|
-
'gpt-5.5': 272000,
|
|
585
|
-
'gpt-5.4': 272000,
|
|
586
|
-
'gpt-5.4-mini': 272000,
|
|
587
|
-
'gpt-5.4-nano': 272000,
|
|
588
|
-
// Anthropic Claude 4.x
|
|
589
|
-
'claude-opus-4-8': 1000000,
|
|
590
|
-
'claude-opus-4-7': 1000000,
|
|
591
|
-
'claude-sonnet-4-6': 1000000,
|
|
592
|
-
'claude-haiku-4-5-20251001': 200000,
|
|
593
|
-
// Google Gemini 3.x
|
|
594
|
-
'gemini-3.1-pro': 1000000,
|
|
595
|
-
'gemini-3-pro': 1000000,
|
|
596
|
-
'gemini-3.5-flash': 1000000,
|
|
597
|
-
'gemini-3-flash': 1000000,
|
|
598
|
-
// xAI Grok (catalog polyfill mirror — model-catalog PRICING_OVERRIDES)
|
|
599
|
-
'grok-build-0.1': 256000,
|
|
600
|
-
'grok-4.20': 1000000,
|
|
601
|
-
};
|
|
602
|
-
// Family-pattern fallback used only when both the provider catalog and the
|
|
603
|
-
// exact-id table miss (cold metadata, before the LiteLLM/models.dev catalog
|
|
604
|
-
// warms). Keep these aligned with the catalog so /context, gateway, and the
|
|
605
|
-
// runtime agree on the boundary the first time a model is routed. Local models
|
|
606
|
-
// (llama/mistral/phi/qwen/gemma) stay small so an unknown local id never claims
|
|
607
|
-
// a giant window.
|
|
608
|
-
function guessContextWindow(model) {
|
|
609
|
-
if (CONTEXT_WINDOWS[model])
|
|
610
|
-
return CONTEXT_WINDOWS[model];
|
|
611
|
-
const m = String(model || '').toLowerCase();
|
|
612
|
-
// Local/self-hosted families — never inflate an unknown local id.
|
|
613
|
-
if (m.includes('llama') || m.includes('mistral') || m.includes('mixtral')
|
|
614
|
-
|| m.includes('phi') || m.includes('qwen') || m.includes('gemma')
|
|
615
|
-
|| m.includes('deepseek-r1') || m.includes('codellama'))
|
|
616
|
-
return 8192;
|
|
617
|
-
// Current hosted families by name pattern.
|
|
618
|
-
if (m.startsWith('claude-opus') || m.startsWith('claude-sonnet')) return 1000000;
|
|
619
|
-
if (m.startsWith('claude-haiku') || m.startsWith('claude-')) return 200000;
|
|
620
|
-
if (m.startsWith('gemini-3') || m.startsWith('gemini-2')) return 1000000;
|
|
621
|
-
if (m.startsWith('gpt-5')) return 272000;
|
|
622
|
-
if (m.startsWith('grok-build')) return 256000;
|
|
623
|
-
if (m.startsWith('grok-')) return 1000000;
|
|
624
|
-
if (m.startsWith('deepseek-v')) return 1000000;
|
|
625
|
-
return 128000;
|
|
626
|
-
}
|
|
627
|
-
function positiveContextWindow(value) {
|
|
628
|
-
const n = Number(value);
|
|
629
|
-
return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
|
|
630
|
-
}
|
|
631
|
-
function envFlag(name, fallback = false) {
|
|
632
|
-
const v = process.env[name];
|
|
633
|
-
if (v === undefined) return fallback;
|
|
634
|
-
return !['0', 'false', 'off', 'no'].includes(String(v).trim().toLowerCase());
|
|
635
|
-
}
|
|
636
|
-
function boundedPercent(value, fallback = null) {
|
|
637
|
-
const n = Number(value);
|
|
638
|
-
if (Number.isFinite(n) && n > 0 && n <= 100) return n;
|
|
639
|
-
return fallback;
|
|
640
|
-
}
|
|
641
|
-
function providerNameOf(provider) {
|
|
642
|
-
if (typeof provider === 'string') return provider.toLowerCase();
|
|
643
|
-
return String(provider?.name || provider?.id || '').toLowerCase();
|
|
644
|
-
}
|
|
645
|
-
// Carry the percent/ratio-named buffer config from a compaction config object
|
|
646
|
-
// onto session.compaction so the shared compact-policy parser honors configured
|
|
647
|
-
// buffer
|
|
648
|
-
// percent/ratio. Only finite positive values are copied; absent fields stay
|
|
649
|
-
// undefined so the default-ratio fallback still applies.
|
|
650
|
-
function preserveBufferConfigFields(cfg = {}) {
|
|
651
|
-
const out = {};
|
|
652
|
-
for (const key of ['bufferPercent', 'bufferPct', 'bufferRatio', 'bufferFraction']) {
|
|
653
|
-
const n = Number(cfg?.[key]);
|
|
654
|
-
if (Number.isFinite(n) && n > 0) out[key] = n;
|
|
655
|
-
}
|
|
656
|
-
return out;
|
|
657
|
-
}
|
|
658
|
-
const COMPACT_TARGET_RATIO = 0.02;
|
|
659
|
-
const COMPACT_TARGET_MIN_TOKENS = 4_000;
|
|
660
|
-
const COMPACT_TARGET_MAX_TOKENS = 16_000;
|
|
661
|
-
function compactTargetRatio() {
|
|
662
|
-
const raw = process.env.MIXDOG_AGENT_COMPACT_TARGET_PERCENT
|
|
663
|
-
?? process.env.MIXDOG_COMPACT_TARGET_PERCENT
|
|
664
|
-
?? COMPACT_TARGET_RATIO;
|
|
665
|
-
const n = Number(raw);
|
|
666
|
-
if (!Number.isFinite(n) || n <= 0) return COMPACT_TARGET_RATIO;
|
|
667
|
-
return n > 1 ? n / 100 : n;
|
|
668
|
-
}
|
|
669
|
-
function compactTargetTokensForBoundary(boundaryTokens) {
|
|
670
|
-
const boundary = positiveContextWindow(boundaryTokens);
|
|
671
|
-
if (!boundary) return null;
|
|
672
|
-
const explicit = positiveContextWindow(
|
|
673
|
-
process.env.MIXDOG_AGENT_COMPACT_TARGET_TOKENS
|
|
674
|
-
?? process.env.MIXDOG_COMPACT_TARGET_TOKENS,
|
|
675
|
-
);
|
|
676
|
-
if (explicit) return Math.max(1, Math.min(boundary, explicit));
|
|
677
|
-
const minTarget = Math.min(boundary, positiveContextWindow(process.env.MIXDOG_COMPACT_TARGET_MIN_TOKENS) || COMPACT_TARGET_MIN_TOKENS);
|
|
678
|
-
const maxTarget = Math.min(boundary, positiveContextWindow(process.env.MIXDOG_COMPACT_TARGET_MAX_TOKENS) || COMPACT_TARGET_MAX_TOKENS);
|
|
679
|
-
const byRatio = Math.max(1, Math.floor(boundary * compactTargetRatio()));
|
|
680
|
-
return Math.max(1, Math.min(boundary, maxTarget, Math.max(minTarget, byRatio)));
|
|
681
|
-
}
|
|
682
|
-
function defaultEffectiveContextWindowPercent(provider) {
|
|
683
|
-
// Gateway/statusline route metadata reserves a universal 10% headroom from
|
|
684
|
-
// the raw catalog window. Keep session compaction on the same effective
|
|
685
|
-
// capacity so /context, the TUI statusline, and gateway telemetry agree.
|
|
686
|
-
return 90;
|
|
687
|
-
}
|
|
688
|
-
const PROVIDER_SYNTHETIC_CONTEXT_DEFAULT = 1_000_000;
|
|
689
|
-
function providerRawContextWindow(info, catalogInfo) {
|
|
690
|
-
if (!info || typeof info !== 'object') return null;
|
|
691
|
-
const fromApiFields = positiveContextWindow(info.context_window)
|
|
692
|
-
|| positiveContextWindow(info.max_context_window);
|
|
693
|
-
if (fromApiFields) return fromApiFields;
|
|
694
|
-
const fromCache = positiveContextWindow(info.contextWindow)
|
|
695
|
-
|| positiveContextWindow(info.maxContextWindow);
|
|
696
|
-
if (!fromCache) return null;
|
|
697
|
-
const catalogWindow = positiveContextWindow(catalogInfo?.contextWindow)
|
|
698
|
-
|| positiveContextWindow(catalogInfo?.maxContextWindow)
|
|
699
|
-
|| positiveContextWindow(catalogInfo?.context_window)
|
|
700
|
-
|| positiveContextWindow(catalogInfo?.max_context_window);
|
|
701
|
-
if (fromCache === PROVIDER_SYNTHETIC_CONTEXT_DEFAULT
|
|
702
|
-
&& catalogWindow
|
|
703
|
-
&& fromCache !== catalogWindow) {
|
|
704
|
-
return null;
|
|
705
|
-
}
|
|
706
|
-
return fromCache;
|
|
707
|
-
}
|
|
708
|
-
function resolveSessionContextMeta(provider, model, seed = {}) {
|
|
709
|
-
const info = typeof provider?.getCachedModelInfo === 'function'
|
|
710
|
-
? provider.getCachedModelInfo(model)
|
|
711
|
-
: null;
|
|
712
|
-
const catalogInfo = getModelMetadataSync(model, providerNameOf(provider));
|
|
713
|
-
const rawContextWindow = providerRawContextWindow(info, catalogInfo)
|
|
714
|
-
|| positiveContextWindow(catalogInfo?.contextWindow)
|
|
715
|
-
|| positiveContextWindow(catalogInfo?.maxContextWindow)
|
|
716
|
-
|| positiveContextWindow(catalogInfo?.context_window)
|
|
717
|
-
|| positiveContextWindow(catalogInfo?.max_context_window)
|
|
718
|
-
|| positiveContextWindow(seed.rawContextWindow)
|
|
719
|
-
|| positiveContextWindow(seed.raw_context_window)
|
|
720
|
-
|| positiveContextWindow(seed.contextWindow)
|
|
721
|
-
|| guessContextWindow(model);
|
|
722
|
-
const effectiveContextWindowPercent = boundedPercent(
|
|
723
|
-
seed.effectiveContextWindowPercent
|
|
724
|
-
?? seed.effective_context_window_percent
|
|
725
|
-
?? info?.effectiveContextWindowPercent
|
|
726
|
-
?? info?.effective_context_window_percent
|
|
727
|
-
?? catalogInfo?.effectiveContextWindowPercent
|
|
728
|
-
?? catalogInfo?.effective_context_window_percent,
|
|
729
|
-
defaultEffectiveContextWindowPercent(provider),
|
|
730
|
-
);
|
|
731
|
-
const pct = boundedPercent(effectiveContextWindowPercent, 100);
|
|
732
|
-
const contextWindow = Math.max(1, Math.floor(rawContextWindow * pct / 100));
|
|
733
|
-
const compactBoundaryTokens = contextWindow;
|
|
734
|
-
const rawCompactLimit = positiveContextWindow(
|
|
735
|
-
seed.autoCompactTokenLimit
|
|
736
|
-
?? seed.auto_compact_token_limit
|
|
737
|
-
?? info?.autoCompactTokenLimit
|
|
738
|
-
?? info?.auto_compact_token_limit
|
|
739
|
-
?? catalogInfo?.autoCompactTokenLimit
|
|
740
|
-
?? catalogInfo?.auto_compact_token_limit,
|
|
741
|
-
);
|
|
742
|
-
// Legacy-data migration: old implementations derived autoCompactTokenLimit
|
|
743
|
-
// from the full effective/raw window and persisted it onto the session.
|
|
744
|
-
// A resumed session therefore re-seeds autoCompactTokenLimit == boundary
|
|
745
|
-
// (or the raw window), which compactTriggerForSession / loop policy used to
|
|
746
|
-
// honor as an explicit trigger, collapsing the compaction buffer to 0. Only
|
|
747
|
-
// accept an explicit limit that is STRICTLY BELOW the boundary; a value at
|
|
748
|
-
// or above the boundary is a derived full-window artifact and is dropped to
|
|
749
|
-
// null so the trigger falls back to the default boundary trigger.
|
|
750
|
-
const explicitCompactLimit = rawCompactLimit && rawCompactLimit < compactBoundaryTokens
|
|
751
|
-
? rawCompactLimit
|
|
752
|
-
: null;
|
|
753
|
-
// Do NOT derive the auto-compact limit from the full effective window.
|
|
754
|
-
// Setting it to contextWindow makes autoTriggerTokens == boundary and the
|
|
755
|
-
// compaction buffer collapse to 0 (loop.mjs:708-713 / compactTriggerForSession),
|
|
756
|
-
// so auto-compact only fires when the context is already at the limit —
|
|
757
|
-
// at which point semantic compact fails ("result exceeds budget" /
|
|
758
|
-
// "summary cannot fit") and the turn can no longer be resumed.
|
|
759
|
-
// Leave it null unless the provider/catalog/seed supplies an explicit
|
|
760
|
-
// limit; the downstream buffer logic (default 10%, capped 25%) then
|
|
761
|
-
// triggers compaction with headroom, matching the reference auto-compact threshold.
|
|
762
|
-
const autoCompactTokenLimit = explicitCompactLimit || null;
|
|
763
|
-
return {
|
|
764
|
-
contextWindow,
|
|
765
|
-
rawContextWindow,
|
|
766
|
-
effectiveContextWindowPercent,
|
|
767
|
-
autoCompactTokenLimit: autoCompactTokenLimit || null,
|
|
768
|
-
compactBoundaryTokens,
|
|
769
|
-
};
|
|
770
|
-
}
|
|
771
|
-
function compactTriggerForSession(session, boundaryTokens) {
|
|
772
|
-
return resolveCompactTriggerTokens(session, boundaryTokens);
|
|
773
|
-
}
|
|
774
254
|
// Test-only exports for the legacy auto-compact-limit migration + buffer-config
|
|
775
255
|
// preservation (see scripts/compact-trigger-migration-smoke.mjs).
|
|
776
256
|
export const _resolveSessionContextMeta = resolveSessionContextMeta;
|
|
777
257
|
export const _compactTriggerForSession = compactTriggerForSession;
|
|
778
258
|
export const _preserveBufferConfigFields = preserveBufferConfigFields;
|
|
779
|
-
// 'compacting' is a transient in-flight stage written just before semantic /
|
|
780
|
-
// recall-fasttrack compaction runs. If the process crashes or only partially
|
|
781
|
-
// saves while it is set, a later load/resume reads a session that is NOT
|
|
782
|
-
// actually compacting but whose UI marker (App.jsx / ContextPanel) shows
|
|
783
|
-
// "Compacting conversation" permanently. Normalize that stale transient stage
|
|
784
|
-
// to 'interrupted' so the surface recovers. Terminal stages (post_turn /
|
|
785
|
-
// manual / auto_clear / *_failed / overflow_failed) are intentionally left as
|
|
786
|
-
// the durable record of the last real outcome.
|
|
787
|
-
function normalizeStaleCompactingStage(session) {
|
|
788
|
-
const c = session?.compaction;
|
|
789
|
-
if (!c || typeof c !== 'object') return false;
|
|
790
|
-
if (c.lastStage !== 'compacting' && c.inProgress !== true) return false;
|
|
791
|
-
c.lastStage = 'interrupted';
|
|
792
|
-
c.inProgress = false;
|
|
793
|
-
c.lastCheckedAt = Date.now();
|
|
794
|
-
return true;
|
|
795
|
-
}
|
|
796
|
-
function compactTargetBudget(boundaryTokens, reserveTokens, _sourceTokens = null, _ratio = null) {
|
|
797
|
-
const boundary = positiveContextWindow(boundaryTokens);
|
|
798
|
-
if (!boundary) return null;
|
|
799
|
-
const reserve = Math.max(0, Number(reserveTokens) || 0);
|
|
800
|
-
const targetEffective = compactTargetTokensForBoundary(boundary) || boundary;
|
|
801
|
-
return Math.max(1, Math.min(boundary, targetEffective + reserve));
|
|
802
|
-
}
|
|
803
|
-
function semanticCompactionEnabledForSession(session) {
|
|
804
|
-
const cfg = session?.compaction || {};
|
|
805
|
-
if (process.env.MIXDOG_AGENT_COMPACT_SEMANTIC !== undefined) return envFlag('MIXDOG_AGENT_COMPACT_SEMANTIC', true);
|
|
806
|
-
if (process.env.MIXDOG_COMPACT_SEMANTIC !== undefined) return envFlag('MIXDOG_COMPACT_SEMANTIC', true);
|
|
807
|
-
if (cfg.semantic === false || cfg.semantic === 'false' || cfg.semantic === 'off') return false;
|
|
808
|
-
if (cfg.semantic === true || cfg.semantic === 'true' || cfg.semantic === 'on' || cfg.semantic === 'auto') return true;
|
|
809
|
-
return true;
|
|
810
|
-
}
|
|
811
|
-
function compactTypeForSession(session) {
|
|
812
|
-
const cfg = session?.compaction || {};
|
|
813
|
-
const configured = process.env.MIXDOG_AGENT_COMPACT_TYPE
|
|
814
|
-
?? process.env.MIXDOG_COMPACT_TYPE
|
|
815
|
-
?? cfg.type
|
|
816
|
-
?? cfg.compactType
|
|
817
|
-
?? cfg.compact_type;
|
|
818
|
-
return normalizeCompactType(configured, DEFAULT_COMPACT_TYPE);
|
|
819
|
-
}
|
|
820
|
-
function addCompactUsageToSession(session, usage) {
|
|
821
|
-
if (!session || !usage) return;
|
|
822
|
-
const inputTokens = usage.inputTokens || 0;
|
|
823
|
-
const outputTokens = usage.outputTokens || 0;
|
|
824
|
-
const cachedTokens = usage.cachedTokens || 0;
|
|
825
|
-
const cacheWriteTokens = usage.cacheWriteTokens || 0;
|
|
826
|
-
const uncachedInputTokens = uncachedInputTokensForProvider(session.provider, inputTokens, cachedTokens, cacheWriteTokens);
|
|
827
|
-
session.totalInputTokens = (session.totalInputTokens || 0) + inputTokens;
|
|
828
|
-
session.totalOutputTokens = (session.totalOutputTokens || 0) + outputTokens;
|
|
829
|
-
session.totalCachedReadTokens = (session.totalCachedReadTokens || 0) + cachedTokens;
|
|
830
|
-
session.totalCacheWriteTokens = (session.totalCacheWriteTokens || 0) + cacheWriteTokens;
|
|
831
|
-
session.totalUncachedInputTokens = (session.totalUncachedInputTokens || 0) + uncachedInputTokens;
|
|
832
|
-
session.tokensCumulative = (session.tokensCumulative || 0) + inputTokens + outputTokens;
|
|
833
|
-
}
|
|
834
|
-
async function runRecallFastTrackForSession(session, messages, opts = {}) {
|
|
835
|
-
const sessionId = opts.sessionId || session?.id || null;
|
|
836
|
-
if (!sessionId) throw new Error('recall-fasttrack requires a session id');
|
|
837
|
-
const query = `session:${sessionId}:all-chunks`;
|
|
838
|
-
const querySha = createHash('sha256').update(query).digest('hex').slice(0, 16);
|
|
839
|
-
const callerCtx = {
|
|
840
|
-
callerSessionId: sessionId,
|
|
841
|
-
callerCwd: session?.cwd || undefined,
|
|
842
|
-
routingSessionId: sessionId,
|
|
843
|
-
clientHostPid: session?.clientHostPid,
|
|
844
|
-
signal: opts.signal || null,
|
|
845
|
-
};
|
|
846
|
-
const hydrateLimit = positiveContextWindow(session?.compaction?.recallIngestLimit)
|
|
847
|
-
|| Math.max(500, Math.min(5000, messages.length || 0));
|
|
848
|
-
try {
|
|
849
|
-
await executeInternalTool('memory', {
|
|
850
|
-
action: 'ingest_session',
|
|
851
|
-
sessionId,
|
|
852
|
-
messages,
|
|
853
|
-
cwd: session?.cwd,
|
|
854
|
-
limit: hydrateLimit,
|
|
855
|
-
}, callerCtx);
|
|
856
|
-
} catch (err) {
|
|
857
|
-
try { process.stderr.write(`[session] recall-fasttrack ingest skipped (sess=${sessionId}): ${err?.message || err}\n`); } catch {}
|
|
858
|
-
}
|
|
859
|
-
const dumpArgs = {
|
|
860
|
-
action: 'dump_session_roots',
|
|
861
|
-
sessionId,
|
|
862
|
-
includeRaw: true,
|
|
863
|
-
limit: positiveContextWindow(session?.compaction?.recallChunkLimit ?? session?.compaction?.recallLimit) || hydrateLimit,
|
|
864
|
-
};
|
|
865
|
-
const runTool = (name, args) => executeInternalTool(name, args, callerCtx);
|
|
866
|
-
let recallText = await executeInternalTool('memory', dumpArgs, callerCtx);
|
|
867
|
-
let cycle1Text = '';
|
|
868
|
-
const hasRawRows = /(?:^|\n)# raw_pending\s+\d+\s+id=/i.test(String(recallText || ''));
|
|
869
|
-
if (hasRawRows) {
|
|
870
|
-
try {
|
|
871
|
-
// Drain this session's cycle1 in window×concurrency units until no
|
|
872
|
-
// raw rows remain, so the injected root is fully chunked rather than
|
|
873
|
-
// carrying the unprocessed transcript tail (single-pass left raw in).
|
|
874
|
-
const drained = await drainSessionCycle1(runTool, {
|
|
875
|
-
sessionId,
|
|
876
|
-
dumpArgs,
|
|
877
|
-
deadlineMs: positiveContextWindow(session?.compaction?.recallCycle1DeadlineMs) || 120_000,
|
|
878
|
-
maxPasses: positiveContextWindow(session?.compaction?.recallCycle1MaxPasses) || 0,
|
|
879
|
-
cycleArgs: {
|
|
880
|
-
min_batch: 1,
|
|
881
|
-
session_cap: 1,
|
|
882
|
-
batch_size: positiveContextWindow(session?.compaction?.recallCycle1BatchSize) || 100,
|
|
883
|
-
rows_per_session: positiveContextWindow(session?.compaction?.recallRowsPerSession) || 100,
|
|
884
|
-
window_size: positiveContextWindow(session?.compaction?.recallWindowSize) || 20,
|
|
885
|
-
concurrency: positiveContextWindow(session?.compaction?.recallConcurrency) || 5,
|
|
886
|
-
},
|
|
887
|
-
});
|
|
888
|
-
recallText = drained.recallText;
|
|
889
|
-
cycle1Text = drained.cycle1Text;
|
|
890
|
-
if (drained.rawRemaining > 0) {
|
|
891
|
-
try { process.stderr.write(`[session] recall-fasttrack drained passes=${drained.passes} rawRemaining=${drained.rawRemaining} (sess=${sessionId})\n`); } catch {}
|
|
892
|
-
}
|
|
893
|
-
} catch (err) {
|
|
894
|
-
try { process.stderr.write(`[session] recall-fasttrack cycle1 skipped (sess=${sessionId}): ${err?.message || err}\n`); } catch {}
|
|
895
|
-
}
|
|
896
|
-
} else {
|
|
897
|
-
cycle1Text = 'cycle1: skipped (session chunks already hydrated)';
|
|
898
|
-
}
|
|
899
|
-
return { query, querySha, recallText: [`session_id=${sessionId}`, cycle1Text, recallText].map(v => String(v || '').trim()).filter(Boolean).join('\n\n') };
|
|
900
|
-
}
|
|
901
|
-
// Element-identity change detection (mirrors loop.mjs messagesArrayChanged): two
|
|
902
|
-
// arrays are "unchanged" only when same length AND every slot is the same object
|
|
903
|
-
// reference. Used to reject a no-op prune (which returns a fresh array whose
|
|
904
|
-
// elements are the untouched originals) from being accepted as a recovery.
|
|
905
|
-
function messagesChanged(before, after) {
|
|
906
|
-
if (!Array.isArray(before) || !Array.isArray(after)) return before !== after;
|
|
907
|
-
if (before.length !== after.length) return true;
|
|
908
|
-
for (let i = 0; i < before.length; i += 1) {
|
|
909
|
-
if (before[i] !== after[i]) return true;
|
|
910
|
-
}
|
|
911
|
-
return false;
|
|
912
|
-
}
|
|
913
|
-
async function runSessionCompaction(session, opts = {}) {
|
|
914
|
-
if (!session || session.closed === true) return null;
|
|
915
|
-
const mode = opts.mode === 'auto' ? 'auto' : 'manual';
|
|
916
|
-
const force = opts.force === true || mode === 'manual';
|
|
917
|
-
if (mode === 'auto' && session.compaction?.auto === false) return null;
|
|
918
|
-
const messages = Array.isArray(session.messages) ? session.messages : [];
|
|
919
|
-
if (messages.length < 3 && !force) return null;
|
|
920
|
-
const boundary = positiveContextWindow(session.compactBoundaryTokens)
|
|
921
|
-
|| positiveContextWindow(session.autoCompactTokenLimit)
|
|
922
|
-
|| positiveContextWindow(session.contextWindow);
|
|
923
|
-
if (!boundary) {
|
|
924
|
-
if (force) throw new Error('compact: no context window is available for this session');
|
|
925
|
-
return null;
|
|
926
|
-
}
|
|
927
|
-
// Reserve must mirror loop.mjs (buildCompactPolicy): request reserve (tool
|
|
928
|
-
// schema) PLUS the configured reserve (session.compaction.reservedTokens or
|
|
929
|
-
// MIXDOG_AGENT_COMPACT_RESERVED_TOKENS env). The old request-only value left
|
|
930
|
-
// the manual / auto-clear compact budget without the configured headroom the
|
|
931
|
-
// loop path reserves, so a compacted transcript could overflow on next send.
|
|
932
|
-
const requestReserveTokens = estimateRequestReserveTokens(session.tools || []);
|
|
933
|
-
const configuredReserveTokens = positiveContextWindow(session.compaction?.reservedTokens)
|
|
934
|
-
|| positiveContextWindow(process.env.MIXDOG_AGENT_COMPACT_RESERVED_TOKENS)
|
|
935
|
-
|| 0;
|
|
936
|
-
const reserveTokens = requestReserveTokens + configuredReserveTokens;
|
|
937
|
-
const beforeMessageTokens = estimateMessagesTokens(messages);
|
|
938
|
-
const triggerTokens = compactTriggerForSession(session, boundary)
|
|
939
|
-
|| positiveContextWindow(session.compaction?.triggerTokens)
|
|
940
|
-
|| boundary;
|
|
941
|
-
const bufferTokens = Math.max(0, boundary - triggerTokens);
|
|
942
|
-
const bufferRatio = boundary ? (bufferTokens / boundary) : compactBufferRatioForSession(session);
|
|
943
|
-
const pressureTokens = estimateTranscriptContextUsage(messages, session.tools || []);
|
|
944
|
-
const beforeTokens = pressureTokens;
|
|
945
|
-
const compactType = compactTypeForSession(session);
|
|
946
|
-
if (!force && pressureTokens < triggerTokens) return {
|
|
947
|
-
changed: false,
|
|
948
|
-
reason: 'below threshold',
|
|
949
|
-
compactType,
|
|
950
|
-
beforeMessages: messages.length,
|
|
951
|
-
afterMessages: messages.length,
|
|
952
|
-
beforeTokens,
|
|
953
|
-
afterTokens: beforeTokens,
|
|
954
|
-
beforeMessageTokens,
|
|
955
|
-
afterMessageTokens: beforeMessageTokens,
|
|
956
|
-
pressureTokens,
|
|
957
|
-
triggerTokens,
|
|
958
|
-
bufferTokens,
|
|
959
|
-
bufferRatio,
|
|
960
|
-
boundaryTokens: boundary,
|
|
961
|
-
budgetTokens: boundary,
|
|
962
|
-
targetBudgetTokens: boundary,
|
|
963
|
-
reserveTokens,
|
|
964
|
-
semanticCompact: false,
|
|
965
|
-
};
|
|
966
|
-
const budgetSourceTokens = force ? Math.max(pressureTokens, triggerTokens) : pressureTokens;
|
|
967
|
-
const compactBudget = compactTargetBudget(boundary, reserveTokens, budgetSourceTokens);
|
|
968
|
-
const budget = compactBudget || boundary;
|
|
969
|
-
try { opts.onStageChange?.('compacting'); } catch { /* best-effort */ }
|
|
970
|
-
const provider = opts.provider || getProvider(session.provider) || null;
|
|
971
|
-
let compacted;
|
|
972
|
-
let compactError = null;
|
|
973
|
-
let semanticCompactResult = null;
|
|
974
|
-
let semanticCompactError = null;
|
|
975
|
-
let recallFastTrackResult = null;
|
|
976
|
-
let recallFastTrackError = null;
|
|
977
|
-
if (compactTypeIsRecallFastTrack(compactType)) {
|
|
978
|
-
try {
|
|
979
|
-
const recallPayload = await runRecallFastTrackForSession(session, messages, opts);
|
|
980
|
-
recallFastTrackResult = recallFastTrackCompactMessages(messages, budget, {
|
|
981
|
-
reserveTokens,
|
|
982
|
-
force: true,
|
|
983
|
-
recallText: recallPayload.recallText,
|
|
984
|
-
query: recallPayload.query,
|
|
985
|
-
querySha: recallPayload.querySha,
|
|
986
|
-
// Ingest just ran on the live transcript, so an empty recall dump
|
|
987
|
-
// means the memory pipeline is broken — do NOT erase history
|
|
988
|
-
// behind an empty summary shell. Empty recall now throws and is
|
|
989
|
-
// handled by the semantic fallback below (or recorded failure).
|
|
990
|
-
allowEmptyRecall: false,
|
|
991
|
-
tailTurns: positiveContextWindow(session.compaction?.tailTurns) || 2,
|
|
992
|
-
keepTokens: positiveContextWindow(session.compaction?.keepTokens ?? session.compaction?.keep?.tokens),
|
|
993
|
-
preserveRecentTokens: positiveContextWindow(session.compaction?.preserveRecentTokens),
|
|
994
|
-
});
|
|
995
|
-
if (Array.isArray(recallFastTrackResult?.messages)) {
|
|
996
|
-
compacted = recallFastTrackResult.messages;
|
|
997
|
-
}
|
|
998
|
-
} catch (err) {
|
|
999
|
-
recallFastTrackError = err;
|
|
1000
|
-
compactError = err;
|
|
1001
|
-
try {
|
|
1002
|
-
process.stderr.write(`[session] recall-fasttrack ${mode} compact failed (sess=${session.id || 'unknown'}): ${err?.message || err}\n`);
|
|
1003
|
-
} catch { /* best-effort */ }
|
|
1004
|
-
// Degraded-compact fallback: recall-fasttrack failed (empty recall,
|
|
1005
|
-
// ingest error, fit failure). Before recording a hard failure, try
|
|
1006
|
-
// the semantic path once so auto-clear/manual compaction still makes
|
|
1007
|
-
// progress WITHOUT shipping an empty-recall summary. History is only
|
|
1008
|
-
// replaced when the semantic summary actually succeeds.
|
|
1009
|
-
if (semanticCompactionEnabledForSession(session)
|
|
1010
|
-
&& provider && typeof provider.send === 'function') {
|
|
1011
|
-
try {
|
|
1012
|
-
semanticCompactResult = await semanticCompactMessages(
|
|
1013
|
-
provider,
|
|
1014
|
-
messages,
|
|
1015
|
-
opts.model || session.model,
|
|
1016
|
-
budget,
|
|
1017
|
-
{
|
|
1018
|
-
reserveTokens,
|
|
1019
|
-
providerName: session.provider || provider?.name || null,
|
|
1020
|
-
sessionId: opts.sessionId || session.id || null,
|
|
1021
|
-
signal: opts.signal || null,
|
|
1022
|
-
promptCacheKey: session.promptCacheKey || null,
|
|
1023
|
-
providerCacheKey: session.promptCacheKey || null,
|
|
1024
|
-
timeoutMs: positiveContextWindow(session.compaction?.timeoutMs) || 30_000,
|
|
1025
|
-
tailTurns: positiveContextWindow(session.compaction?.tailTurns) || 2,
|
|
1026
|
-
keepTokens: positiveContextWindow(session.compaction?.keepTokens ?? session.compaction?.keep?.tokens),
|
|
1027
|
-
preserveRecentTokens: positiveContextWindow(session.compaction?.preserveRecentTokens),
|
|
1028
|
-
force: true,
|
|
1029
|
-
},
|
|
1030
|
-
);
|
|
1031
|
-
if (Array.isArray(semanticCompactResult?.messages)) {
|
|
1032
|
-
compacted = semanticCompactResult.messages;
|
|
1033
|
-
compactError = null;
|
|
1034
|
-
addCompactUsageToSession(session, semanticCompactResult.usage);
|
|
1035
|
-
try {
|
|
1036
|
-
process.stderr.write(`[session] degraded compact: recall-fasttrack failed, semantic fallback succeeded (sess=${session.id || 'unknown'}, mode=${mode})\n`);
|
|
1037
|
-
} catch { /* best-effort */ }
|
|
1038
|
-
}
|
|
1039
|
-
} catch (fallbackErr) {
|
|
1040
|
-
semanticCompactError = fallbackErr;
|
|
1041
|
-
try {
|
|
1042
|
-
process.stderr.write(`[session] degraded compact: semantic fallback also failed (sess=${session.id || 'unknown'}): ${fallbackErr?.message || fallbackErr}\n`);
|
|
1043
|
-
} catch { /* best-effort */ }
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
} else if (compactTypeIsSemantic(compactType)) {
|
|
1048
|
-
try {
|
|
1049
|
-
if (!semanticCompactionEnabledForSession(session)) {
|
|
1050
|
-
throw new Error('semantic compact is disabled for this session');
|
|
1051
|
-
}
|
|
1052
|
-
if (!provider || typeof provider.send !== 'function') {
|
|
1053
|
-
throw new Error(`semantic compact provider unavailable: ${session.provider || 'unknown'}`);
|
|
1054
|
-
}
|
|
1055
|
-
semanticCompactResult = await semanticCompactMessages(
|
|
1056
|
-
provider,
|
|
1057
|
-
messages,
|
|
1058
|
-
opts.model || session.model,
|
|
1059
|
-
budget,
|
|
1060
|
-
{
|
|
1061
|
-
reserveTokens,
|
|
1062
|
-
providerName: session.provider || provider?.name || null,
|
|
1063
|
-
sessionId: opts.sessionId || session.id || null,
|
|
1064
|
-
signal: opts.signal || null,
|
|
1065
|
-
promptCacheKey: session.promptCacheKey || null,
|
|
1066
|
-
providerCacheKey: session.promptCacheKey || null,
|
|
1067
|
-
timeoutMs: positiveContextWindow(session.compaction?.timeoutMs) || 30_000,
|
|
1068
|
-
tailTurns: positiveContextWindow(session.compaction?.tailTurns) || 2,
|
|
1069
|
-
keepTokens: positiveContextWindow(session.compaction?.keepTokens ?? session.compaction?.keep?.tokens),
|
|
1070
|
-
preserveRecentTokens: positiveContextWindow(session.compaction?.preserveRecentTokens),
|
|
1071
|
-
force: true,
|
|
1072
|
-
},
|
|
1073
|
-
);
|
|
1074
|
-
if (Array.isArray(semanticCompactResult?.messages)) {
|
|
1075
|
-
compacted = semanticCompactResult.messages;
|
|
1076
|
-
addCompactUsageToSession(session, semanticCompactResult.usage);
|
|
1077
|
-
}
|
|
1078
|
-
} catch (err) {
|
|
1079
|
-
semanticCompactError = err;
|
|
1080
|
-
compactError = err;
|
|
1081
|
-
try {
|
|
1082
|
-
process.stderr.write(`[session] semantic ${mode} compact failed (sess=${session.id || 'unknown'}): ${err?.message || err}\n`);
|
|
1083
|
-
} catch { /* best-effort */ }
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
if (!compacted && !compactError) {
|
|
1087
|
-
compactError = new Error(`${compactType} compact produced no messages`);
|
|
1088
|
-
}
|
|
1089
|
-
// Anchor-independent prune safety net (mirror loop.mjs compact catch): when a
|
|
1090
|
-
// non-recall (semantic) compact failed, try one non-LLM prune that needs no
|
|
1091
|
-
// user anchor before recording failure, so Lead manual / auto-clear paths
|
|
1092
|
-
// recover the same transcripts the loop path does. Gated off the recall
|
|
1093
|
-
// path — a recall failure keeps its original contract (no silent prune).
|
|
1094
|
-
if (!compacted && !recallFastTrackError) {
|
|
1095
|
-
try {
|
|
1096
|
-
const acceptThreshold = compactEffectiveBudget(budget, { reserveTokens });
|
|
1097
|
-
const salvaged = pruneToolOutputsUnanchored(messages, budget, { reserveTokens });
|
|
1098
|
-
// pruneToolOutputsUnanchored ALWAYS returns a fresh reconciled array
|
|
1099
|
-
// (never the input identity), so `salvaged !== messages` is always
|
|
1100
|
-
// true and cannot detect a no-op. Compare by element identity so a
|
|
1101
|
-
// transcript that already fit (nothing pruned) is NOT falsely accepted
|
|
1102
|
-
// as a recovery — that would clear compactError and unconditionally
|
|
1103
|
-
// invalidate providerState for an unchanged transcript.
|
|
1104
|
-
if (Array.isArray(salvaged)
|
|
1105
|
-
&& messagesChanged(messages, salvaged)
|
|
1106
|
-
&& estimateMessagesTokens(salvaged) <= acceptThreshold) {
|
|
1107
|
-
compacted = salvaged;
|
|
1108
|
-
compactError = null;
|
|
1109
|
-
try {
|
|
1110
|
-
process.stderr.write(`[session] compact fallback prune recovered (sess=${session.id || 'unknown'}, mode=${mode})\n`);
|
|
1111
|
-
} catch { /* best-effort */ }
|
|
1112
|
-
}
|
|
1113
|
-
} catch { /* fall through to failure record */ }
|
|
1114
|
-
}
|
|
1115
|
-
if (!compacted) {
|
|
1116
|
-
const now = Date.now();
|
|
1117
|
-
session.compaction = {
|
|
1118
|
-
...(session.compaction || {}),
|
|
1119
|
-
auto: mode === 'auto' ? true : session.compaction?.auto !== false,
|
|
1120
|
-
boundaryTokens: boundary,
|
|
1121
|
-
triggerTokens,
|
|
1122
|
-
bufferTokens,
|
|
1123
|
-
bufferRatio,
|
|
1124
|
-
reserveTokens,
|
|
1125
|
-
lastStage: mode === 'auto' ? 'post_turn_failed' : 'manual_failed',
|
|
1126
|
-
lastBeforeTokens: beforeTokens,
|
|
1127
|
-
lastAfterTokens: beforeTokens,
|
|
1128
|
-
lastBeforeMessageTokens: beforeMessageTokens,
|
|
1129
|
-
lastAfterMessageTokens: beforeMessageTokens,
|
|
1130
|
-
lastPressureTokens: pressureTokens,
|
|
1131
|
-
lastCheckedAt: now,
|
|
1132
|
-
lastChanged: false,
|
|
1133
|
-
type: compactType,
|
|
1134
|
-
compactType,
|
|
1135
|
-
lastCompactType: compactType,
|
|
1136
|
-
lastSemantic: false,
|
|
1137
|
-
lastSemanticError: semanticCompactError?.message || null,
|
|
1138
|
-
lastRecallFastTrack: false,
|
|
1139
|
-
lastRecallFastTrackError: recallFastTrackError?.message || null,
|
|
1140
|
-
lastError: compactError?.message || semanticCompactError?.message || recallFastTrackError?.message || String(compactError || semanticCompactError || recallFastTrackError || 'compact failed'),
|
|
1141
|
-
};
|
|
1142
|
-
return {
|
|
1143
|
-
changed: false,
|
|
1144
|
-
error: session.compaction.lastError,
|
|
1145
|
-
compactType,
|
|
1146
|
-
beforeMessages: messages.length,
|
|
1147
|
-
afterMessages: messages.length,
|
|
1148
|
-
beforeTokens,
|
|
1149
|
-
afterTokens: beforeTokens,
|
|
1150
|
-
beforeMessageTokens,
|
|
1151
|
-
afterMessageTokens: beforeMessageTokens,
|
|
1152
|
-
pressureTokens,
|
|
1153
|
-
triggerTokens,
|
|
1154
|
-
bufferTokens,
|
|
1155
|
-
bufferRatio,
|
|
1156
|
-
boundaryTokens: boundary,
|
|
1157
|
-
budgetTokens: boundary,
|
|
1158
|
-
targetBudgetTokens: budget,
|
|
1159
|
-
reserveTokens,
|
|
1160
|
-
semanticCompact: false,
|
|
1161
|
-
semanticError: semanticCompactError?.message || null,
|
|
1162
|
-
recallFastTrack: false,
|
|
1163
|
-
recallFastTrackError: recallFastTrackError?.message || null,
|
|
1164
|
-
};
|
|
1165
|
-
}
|
|
1166
|
-
let beforeEncoded = '';
|
|
1167
|
-
let afterEncoded = '';
|
|
1168
|
-
try { beforeEncoded = JSON.stringify(messages); } catch { beforeEncoded = ''; }
|
|
1169
|
-
try { afterEncoded = JSON.stringify(compacted); } catch { afterEncoded = ''; }
|
|
1170
|
-
const afterMessageTokens = estimateMessagesTokens(compacted);
|
|
1171
|
-
const afterTokens = afterMessageTokens + reserveTokens;
|
|
1172
|
-
const changed = beforeEncoded && afterEncoded
|
|
1173
|
-
? beforeEncoded !== afterEncoded
|
|
1174
|
-
: (compacted.length !== messages.length || afterMessageTokens !== beforeMessageTokens);
|
|
1175
|
-
const unchangedReason = changed ? null : (force ? 'nothing to compact' : 'below threshold');
|
|
1176
|
-
const now = Date.now();
|
|
1177
|
-
session.messages = compacted;
|
|
1178
|
-
session.providerState = undefined;
|
|
1179
|
-
session.compaction = {
|
|
1180
|
-
...(session.compaction || {}),
|
|
1181
|
-
auto: mode === 'auto' ? true : session.compaction?.auto !== false,
|
|
1182
|
-
boundaryTokens: boundary,
|
|
1183
|
-
triggerTokens,
|
|
1184
|
-
bufferTokens,
|
|
1185
|
-
bufferRatio,
|
|
1186
|
-
reserveTokens,
|
|
1187
|
-
type: compactType,
|
|
1188
|
-
compactType,
|
|
1189
|
-
lastCompactType: compactType,
|
|
1190
|
-
lastStage: mode === 'auto' ? 'post_turn' : 'manual',
|
|
1191
|
-
lastBeforeTokens: beforeTokens,
|
|
1192
|
-
lastAfterTokens: afterTokens,
|
|
1193
|
-
lastBeforeMessageTokens: beforeMessageTokens,
|
|
1194
|
-
lastAfterMessageTokens: afterMessageTokens,
|
|
1195
|
-
lastPressureTokens: pressureTokens,
|
|
1196
|
-
lastCheckedAt: now,
|
|
1197
|
-
lastChanged: changed,
|
|
1198
|
-
lastChangedAt: changed ? now : session.compaction?.lastChangedAt || null,
|
|
1199
|
-
lastCompactAt: changed ? now : session.compaction?.lastCompactAt || null,
|
|
1200
|
-
lastSemantic: semanticCompactResult?.semantic === true,
|
|
1201
|
-
lastSemanticError: semanticCompactError?.message || null,
|
|
1202
|
-
lastRecallFastTrack: recallFastTrackResult?.recallFastTrack === true,
|
|
1203
|
-
lastRecallFastTrackError: recallFastTrackError?.message || null,
|
|
1204
|
-
lastRecallFastTrackQuerySha: recallFastTrackResult?.query ? createHash('sha256').update(recallFastTrackResult.query).digest('hex').slice(0, 16) : null,
|
|
1205
|
-
lastSemanticUsage: semanticCompactResult?.usage ? {
|
|
1206
|
-
inputTokens: semanticCompactResult.usage.inputTokens || 0,
|
|
1207
|
-
outputTokens: semanticCompactResult.usage.outputTokens || 0,
|
|
1208
|
-
cachedTokens: semanticCompactResult.usage.cachedTokens || 0,
|
|
1209
|
-
cacheWriteTokens: semanticCompactResult.usage.cacheWriteTokens || 0,
|
|
1210
|
-
} : null,
|
|
1211
|
-
compactCount: (session.compaction?.compactCount || 0) + (changed ? 1 : 0),
|
|
1212
|
-
};
|
|
1213
|
-
if (changed && mode === 'auto') session.lastContextTokensStaleAfterCompact = true;
|
|
1214
|
-
return {
|
|
1215
|
-
changed,
|
|
1216
|
-
reason: unchangedReason,
|
|
1217
|
-
compactType,
|
|
1218
|
-
beforeMessages: messages.length,
|
|
1219
|
-
afterMessages: compacted.length,
|
|
1220
|
-
beforeTokens,
|
|
1221
|
-
afterTokens,
|
|
1222
|
-
beforeMessageTokens,
|
|
1223
|
-
afterMessageTokens,
|
|
1224
|
-
pressureTokens,
|
|
1225
|
-
triggerTokens,
|
|
1226
|
-
bufferTokens,
|
|
1227
|
-
bufferRatio,
|
|
1228
|
-
boundaryTokens: boundary,
|
|
1229
|
-
budgetTokens: boundary,
|
|
1230
|
-
targetBudgetTokens: budget,
|
|
1231
|
-
reserveTokens,
|
|
1232
|
-
semanticCompact: semanticCompactResult?.semantic === true,
|
|
1233
|
-
semanticError: semanticCompactError?.message || null,
|
|
1234
|
-
recallFastTrack: recallFastTrackResult?.recallFastTrack === true,
|
|
1235
|
-
recallFastTrackError: recallFastTrackError?.message || null,
|
|
1236
|
-
usage: semanticCompactResult?.usage || null,
|
|
1237
|
-
};
|
|
1238
|
-
}
|
|
1239
259
|
// Provider-scoped unified cache key. Goal: all orchestrator-internal
|
|
1240
260
|
// dispatches (agent/maintenance/mcp/scheduler/webhook) targeting the
|
|
1241
261
|
// same provider land in a single server-side cache shard, so the
|
|
@@ -1579,6 +599,34 @@ export function createSession(opts) {
|
|
|
1579
599
|
if (!provider)
|
|
1580
600
|
throw new Error(`Provider "${providerName}" not found or not enabled`);
|
|
1581
601
|
const id = `sess_${process.pid}_${nextId++}_${Date.now()}_${randomBytes(16).toString('hex')}`;
|
|
602
|
+
// Provider cache strategy — agentRuntime.resolveSync() above is a
|
|
603
|
+
// best-effort injection point (setAgentRuntime() has no live caller
|
|
604
|
+
// today, so that branch never fires); build it directly here so every
|
|
605
|
+
// session still gets a cache strategy. Lead sessions (opts.agent ===
|
|
606
|
+
// 'lead', or no agent at all — raw/CLI callers) get their BP4 message
|
|
607
|
+
// tail TTL linked to the user's autoClear idle-sweep config; hidden and
|
|
608
|
+
// public agents keep the flat 5m default (see cache-strategy.mjs docs).
|
|
609
|
+
// Scoped to explicit-breakpoint (Anthropic-family) providers only — the
|
|
610
|
+
// non-Anthropic branches of buildProviderCacheOpts (e.g. the 'openai'
|
|
611
|
+
// cacheRetention:'24h' shape) were never exercised by createSession
|
|
612
|
+
// before this change, and are left untouched to avoid altering live
|
|
613
|
+
// OpenAI/other-provider request shape as a side effect of this fix.
|
|
614
|
+
if (!providerCacheOpts && cacheCapabilityForProvider(providerName) === 'explicit-breakpoint') {
|
|
615
|
+
try {
|
|
616
|
+
let autoClear = null;
|
|
617
|
+
if (!opts.agent || opts.agent === 'lead') {
|
|
618
|
+
const loadedConfig = loadConfig({ secrets: false });
|
|
619
|
+
const normalizedAutoClear = normalizeAutoClearConfig(loadedConfig?.autoClear);
|
|
620
|
+
autoClear = {
|
|
621
|
+
...normalizedAutoClear,
|
|
622
|
+
idleMs: resolveAutoClearIdleMs(loadedConfig, providerName),
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
providerCacheOpts = buildProviderCacheOpts(providerName, id, opts.agent, { autoClear });
|
|
626
|
+
} catch {
|
|
627
|
+
providerCacheOpts = null;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
1582
630
|
const messages = [];
|
|
1583
631
|
const ownerIsAgent = isAgentOwner(opts.owner);
|
|
1584
632
|
const resolvedAgent = opts.agent || opts.role || profile?.taskType || null;
|
|
@@ -1818,69 +866,11 @@ export function createSession(opts) {
|
|
|
1818
866
|
return session;
|
|
1819
867
|
}
|
|
1820
868
|
|
|
1821
|
-
//
|
|
1822
|
-
//
|
|
1823
|
-
//
|
|
1824
|
-
//
|
|
1825
|
-
//
|
|
1826
|
-
// stage, lastStreamDeltaAt, lastToolCall, lastError, updatedAt,
|
|
1827
|
-
// controller?: AbortController, // set while an ask is in flight
|
|
1828
|
-
// generation?: number, // snapshot taken at ask start
|
|
1829
|
-
// closed?: boolean, // flipped by closeSession()
|
|
1830
|
-
// }
|
|
1831
|
-
const _runtimeState = new Map();
|
|
1832
|
-
const _toolActivityHeartbeats = new Map();
|
|
1833
|
-
const VALID_STAGES = new Set([
|
|
1834
|
-
'connecting', 'requesting', 'streaming', 'tool_running', 'idle', 'error', 'done', 'cancelling',
|
|
1835
|
-
]);
|
|
1836
|
-
function _touchRuntime(id) {
|
|
1837
|
-
let entry = _runtimeState.get(id);
|
|
1838
|
-
if (!entry) {
|
|
1839
|
-
entry = { stage: 'idle', lastStreamDeltaAt: null, lastToolCall: null, lastError: null, updatedAt: Date.now() };
|
|
1840
|
-
_runtimeState.set(id, entry);
|
|
1841
|
-
}
|
|
1842
|
-
return entry;
|
|
1843
|
-
}
|
|
1844
|
-
|
|
1845
|
-
function _stopToolActivityHeartbeat(id) {
|
|
1846
|
-
if (!id) return;
|
|
1847
|
-
const timer = _toolActivityHeartbeats.get(id);
|
|
1848
|
-
if (!timer) return;
|
|
1849
|
-
try { clearInterval(timer); } catch { /* ignore */ }
|
|
1850
|
-
_toolActivityHeartbeats.delete(id);
|
|
1851
|
-
}
|
|
1852
|
-
|
|
1853
|
-
function _touchSessionActivityProgress(id) {
|
|
1854
|
-
const entry = _runtimeState.get(id);
|
|
1855
|
-
if (!entry || entry.closed || entry.controller?.signal?.aborted) return;
|
|
1856
|
-
if (entry.stage !== 'tool_running') return;
|
|
1857
|
-
const now = Date.now();
|
|
1858
|
-
entry.lastProgressAt = now;
|
|
1859
|
-
entry.updatedAt = now;
|
|
1860
|
-
publishHeartbeat(id, now);
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
|
-
function _startToolActivityHeartbeat(id) {
|
|
1864
|
-
_stopToolActivityHeartbeat(id);
|
|
1865
|
-
if (!(DEFAULT_ACTIVITY_HEARTBEAT_MS > 0)) return;
|
|
1866
|
-
const timer = setInterval(() => _touchSessionActivityProgress(id), DEFAULT_ACTIVITY_HEARTBEAT_MS);
|
|
1867
|
-
if (typeof timer.unref === 'function') timer.unref();
|
|
1868
|
-
_toolActivityHeartbeats.set(id, timer);
|
|
1869
|
-
}
|
|
1870
|
-
|
|
1871
|
-
export function updateSessionStage(id, stage) {
|
|
1872
|
-
if (!id || !VALID_STAGES.has(stage)) return;
|
|
1873
|
-
const entry = _touchRuntime(id);
|
|
1874
|
-
const now = Date.now();
|
|
1875
|
-
entry.stage = stage;
|
|
1876
|
-
if (stage === 'connecting' || stage === 'requesting') {
|
|
1877
|
-
entry.modelRequestStartedAt = now;
|
|
1878
|
-
}
|
|
1879
|
-
entry.lastProgressAt = now;
|
|
1880
|
-
entry.updatedAt = now;
|
|
1881
|
-
if (stage !== 'tool_running') _stopToolActivityHeartbeat(id);
|
|
1882
|
-
}
|
|
1883
|
-
|
|
869
|
+
// Runtime liveness map + its accessors moved to manager/runtime-liveness.mjs
|
|
870
|
+
// (imported/re-exported above). _runtimeState is now private to that module;
|
|
871
|
+
// manager.mjs internals that mutate entries in place use _getRuntimeEntry /
|
|
872
|
+
// _runtimeEntries. updateSessionRoute stays here (it's a persistence helper on
|
|
873
|
+
// the session store, not a liveness accessor).
|
|
1884
874
|
export function updateSessionRoute(id, route = {}) {
|
|
1885
875
|
if (!id) return null;
|
|
1886
876
|
const session = loadSession(id);
|
|
@@ -1936,475 +926,15 @@ export function updateSessionRoute(id, route = {}) {
|
|
|
1936
926
|
return session;
|
|
1937
927
|
}
|
|
1938
928
|
|
|
1939
|
-
/**
|
|
1940
|
-
* Reset heartbeat-visible fields for a new ask. Preserves controller/generation/
|
|
1941
|
-
* closed (lifecycle) but clears the previous run's streaming state so stale
|
|
1942
|
-
* lastToolCall / lastStreamDeltaAt from the previous ask don't leak into the
|
|
1943
|
-
* new one.
|
|
1944
|
-
*/
|
|
1945
|
-
export function markSessionAskStart(id) {
|
|
1946
|
-
if (!id) return;
|
|
1947
|
-
_stopToolActivityHeartbeat(id);
|
|
1948
|
-
const entry = _touchRuntime(id);
|
|
1949
|
-
entry.usageMetricsTurnIncremental = false;
|
|
1950
|
-
const sessionForTurn = entry.session ?? loadSession(id);
|
|
1951
|
-
if (sessionForTurn) bumpUsageMetricsTurnId(sessionForTurn);
|
|
1952
|
-
entry.stage = 'connecting';
|
|
1953
|
-
entry.lastStreamDeltaAt = null;
|
|
1954
|
-
entry.lastToolCall = null;
|
|
1955
|
-
entry.toolStartedAt = null;
|
|
1956
|
-
entry.lastError = null;
|
|
1957
|
-
// A new ask starts a fresh turn lifecycle — clear any stale empty-final
|
|
1958
|
-
// classification from the prior turn so inspectBridgeEntry doesn't keep
|
|
1959
|
-
// short-circuiting to 'empty-synthesis' (which would disable stall
|
|
1960
|
-
// detection for the entire new turn).
|
|
1961
|
-
entry.emptyFinal = false;
|
|
1962
|
-
entry.emptyFinalAt = null;
|
|
1963
|
-
// askStartedAt is the watchdog's fallback reference when a session
|
|
1964
|
-
// hangs before any stream delta arrives. Without it, a provider that
|
|
1965
|
-
// never returns a first token would stall forever because the watchdog
|
|
1966
|
-
// keys solely on lastStreamDeltaAt.
|
|
1967
|
-
const now = Date.now();
|
|
1968
|
-
entry.askStartedAt = now;
|
|
1969
|
-
entry.modelRequestStartedAt = now;
|
|
1970
|
-
entry.lastProgressAt = now;
|
|
1971
|
-
entry.updatedAt = now;
|
|
1972
|
-
// Publish heartbeat immediately so the status aggregator picks the
|
|
1973
|
-
// session up in the connecting / requesting window. Without this the
|
|
1974
|
-
// .hb file only landed on the first stream chunk — producing a 3–10s
|
|
1975
|
-
// (xhigh: 30s+) invisible gap where agent sessions ran but the CC
|
|
1976
|
-
// statusline showed no maintenance/agent badge. STREAM_FRESH_MS (5 min)
|
|
1977
|
-
// still drops a session whose provider truly never returns a chunk;
|
|
1978
|
-
// markSessionStreamDelta keeps refreshing once chunks arrive.
|
|
1979
|
-
publishHeartbeat(id, now);
|
|
1980
|
-
}
|
|
1981
|
-
export async function markSessionStreamDelta(id) {
|
|
1982
|
-
if (!id) return;
|
|
1983
|
-
// Non-creating lookup: a live ask ALWAYS has a runtime entry (markSessionAskStart
|
|
1984
|
-
// creates it before streaming begins). _touchRuntime would instead resurrect a
|
|
1985
|
-
// blank entry — and closeSession()/idle-sweep clear _runtimeState on a deferred
|
|
1986
|
-
// tick while a detached provider stream may still be trickling deltas. A delta
|
|
1987
|
-
// arriving after that clear must NOT re-create an entry or it would republish the
|
|
1988
|
-
// .hb heartbeat that markSessionClosed deleted, orphaning a dead session's
|
|
1989
|
-
// heartbeat indefinitely (the disk tombstone blocks ask resumption but not this
|
|
1990
|
-
// path). Skip a missing, tombstoned, or aborted entry — never refresh liveness.
|
|
1991
|
-
const entry = _runtimeState.get(id);
|
|
1992
|
-
if (!entry || entry.closed || entry.controller?.signal?.aborted) return;
|
|
1993
|
-
_stopToolActivityHeartbeat(id);
|
|
1994
|
-
const now = Date.now();
|
|
1995
|
-
entry.lastStreamDeltaAt = now;
|
|
1996
|
-
entry.lastProgressAt = now;
|
|
1997
|
-
// Only promote to 'streaming' if we were in a pre-stream stage; never downgrade
|
|
1998
|
-
// mid-tool (tool_running has its own delta source if the tool streams back).
|
|
1999
|
-
if (entry.stage === 'connecting' || entry.stage === 'requesting') {
|
|
2000
|
-
entry.stage = 'streaming';
|
|
2001
|
-
}
|
|
2002
|
-
// Lightweight heartbeat (≤5s self-throttled) for the status aggregator.
|
|
2003
|
-
// Disk-side session.lastHeartbeatAt below is the heavy 60s zombie-reaper
|
|
2004
|
-
// signal; the .hb file is the fast fresh-session signal consumed by the
|
|
2005
|
-
// status line.
|
|
2006
|
-
publishHeartbeat(id, now);
|
|
2007
|
-
const session = entry.session;
|
|
2008
|
-
if (session && now - (session.lastHeartbeatAt || 0) > HEARTBEAT_THROTTLE_MS) {
|
|
2009
|
-
session.lastHeartbeatAt = now;
|
|
2010
|
-
await saveSessionAsync(session, { expectedGeneration: session.generation });
|
|
2011
|
-
}
|
|
2012
|
-
entry.updatedAt = now;
|
|
2013
|
-
}
|
|
2014
|
-
export function markSessionToolCall(id, toolName) {
|
|
2015
|
-
if (!id) return;
|
|
2016
|
-
const entry = _touchRuntime(id);
|
|
2017
|
-
entry.stage = 'tool_running';
|
|
2018
|
-
entry.lastToolCall = toolName || null;
|
|
2019
|
-
entry.toolStartedAt = Date.now();
|
|
2020
|
-
entry.lastProgressAt = entry.toolStartedAt;
|
|
2021
|
-
entry.updatedAt = entry.toolStartedAt;
|
|
2022
|
-
publishHeartbeat(id, entry.toolStartedAt);
|
|
2023
|
-
_startToolActivityHeartbeat(id);
|
|
2024
|
-
}
|
|
2025
|
-
// Parent AbortSignal listeners are dropped on askSession unwind (finally /
|
|
2026
|
-
// terminal return) and on error/cancel/close — not in markSessionDone, which
|
|
2027
|
-
// also runs between queued follow-up turns within one ask.
|
|
2028
|
-
export function markSessionDone(id, { empty = false } = {}) {
|
|
2029
|
-
if (!id) return;
|
|
2030
|
-
_stopToolActivityHeartbeat(id);
|
|
2031
|
-
const entry = _touchRuntime(id);
|
|
2032
|
-
entry.stage = 'done';
|
|
2033
|
-
entry.lastError = null;
|
|
2034
|
-
entry.askStartedAt = null;
|
|
2035
|
-
entry.toolStartedAt = null;
|
|
2036
|
-
// Non-empty completion: drop any stale empty-final flag so a subsequent
|
|
2037
|
-
// ask on the same reusable runtime entry starts clean. Empty-final
|
|
2038
|
-
// completions preserve the flag (set by markSessionEmptyFinal just prior).
|
|
2039
|
-
if (!empty) {
|
|
2040
|
-
entry.emptyFinal = false;
|
|
2041
|
-
entry.emptyFinalAt = null;
|
|
2042
|
-
}
|
|
2043
|
-
const doneTs = Date.now();
|
|
2044
|
-
entry.doneAt = doneTs;
|
|
2045
|
-
entry.lastProgressAt = doneTs;
|
|
2046
|
-
entry.updatedAt = doneTs;
|
|
2047
|
-
// Terminal stage — drop the heartbeat so the status badge releases
|
|
2048
|
-
// immediately. A subsequent ask on the same session re-publishes via
|
|
2049
|
-
// markSessionStreamDelta on the first chunk.
|
|
2050
|
-
deleteHeartbeat(id);
|
|
2051
|
-
}
|
|
2052
|
-
// Tag a session as having completed with empty final synthesis (no
|
|
2053
|
-
// content/reasoning). Distinct from `markSessionDone`: still a success
|
|
2054
|
-
// (no abort), but the stall watchdog and post-mortem tools can
|
|
2055
|
-
// distinguish "finished empty" from "finished with content" without
|
|
2056
|
-
// mistaking the silence for a stall.
|
|
2057
|
-
export function markSessionEmptyFinal(id) {
|
|
2058
|
-
if (!id) return;
|
|
2059
|
-
const entry = _touchRuntime(id);
|
|
2060
|
-
entry.emptyFinal = true;
|
|
2061
|
-
entry.emptyFinalAt = Date.now();
|
|
2062
|
-
}
|
|
2063
|
-
export function markSessionError(id, msg) {
|
|
2064
|
-
if (!id) return;
|
|
2065
|
-
_stopToolActivityHeartbeat(id);
|
|
2066
|
-
const entry = _touchRuntime(id);
|
|
2067
|
-
entry.stage = 'error';
|
|
2068
|
-
entry.lastError = msg ? String(msg).slice(0, 200) : null;
|
|
2069
|
-
entry.askStartedAt = null;
|
|
2070
|
-
entry.toolStartedAt = null;
|
|
2071
|
-
// Error path is a non-empty completion (we have an error message, not a
|
|
2072
|
-
// silent empty final). Clear the flag so the next ask starts clean.
|
|
2073
|
-
entry.emptyFinal = false;
|
|
2074
|
-
entry.emptyFinalAt = null;
|
|
2075
|
-
const errTs = Date.now();
|
|
2076
|
-
entry.doneAt = errTs;
|
|
2077
|
-
entry.lastProgressAt = errTs;
|
|
2078
|
-
entry.updatedAt = errTs;
|
|
2079
|
-
deleteHeartbeat(id);
|
|
2080
|
-
_unlinkParentAbortListener(entry);
|
|
2081
|
-
}
|
|
2082
|
-
export function markSessionCancelled(id) {
|
|
2083
|
-
if (!id) return;
|
|
2084
|
-
_stopToolActivityHeartbeat(id);
|
|
2085
|
-
const entry = _touchRuntime(id);
|
|
2086
|
-
entry.stage = 'done';
|
|
2087
|
-
entry.lastError = null;
|
|
2088
|
-
entry.askStartedAt = null;
|
|
2089
|
-
entry.toolStartedAt = null;
|
|
2090
|
-
entry.emptyFinal = false;
|
|
2091
|
-
entry.emptyFinalAt = null;
|
|
2092
|
-
const doneTs = Date.now();
|
|
2093
|
-
entry.doneAt = doneTs;
|
|
2094
|
-
entry.lastProgressAt = doneTs;
|
|
2095
|
-
entry.updatedAt = doneTs;
|
|
2096
|
-
deleteHeartbeat(id);
|
|
2097
|
-
_unlinkParentAbortListener(entry);
|
|
2098
|
-
}
|
|
2099
|
-
export function getSessionRuntime(id) {
|
|
2100
|
-
return id ? (_runtimeState.get(id) || null) : null;
|
|
2101
|
-
}
|
|
2102
|
-
|
|
2103
|
-
const _COMPACTION_BLOCKED_STAGES = new Set([
|
|
2104
|
-
'connecting', 'requesting', 'streaming', 'tool_running', 'cancelling',
|
|
2105
|
-
]);
|
|
2106
|
-
|
|
2107
|
-
export function isSessionCompactionBlocked(sessionId) {
|
|
2108
|
-
if (!sessionId) return false;
|
|
2109
|
-
const entry = _runtimeState.get(sessionId);
|
|
2110
|
-
if (!entry || entry.closed === true) return false;
|
|
2111
|
-
if (entry.controller && !entry.controller.signal?.aborted) return true;
|
|
2112
|
-
return _COMPACTION_BLOCKED_STAGES.has(entry.stage);
|
|
2113
|
-
}
|
|
2114
|
-
|
|
2115
|
-
export function getSessionProgressSnapshot(sessionId) {
|
|
2116
|
-
const entry = _runtimeState.get(sessionId);
|
|
2117
|
-
if (!entry) return null;
|
|
2118
|
-
const askStartedAt = entry.askStartedAt || 0;
|
|
2119
|
-
const modelRequestStartedAt = entry.modelRequestStartedAt || askStartedAt;
|
|
2120
|
-
const firstActivityAt = Math.max(
|
|
2121
|
-
entry.lastStreamDeltaAt || 0,
|
|
2122
|
-
entry.toolStartedAt || 0,
|
|
2123
|
-
);
|
|
2124
|
-
const stage = entry.stage || 'idle';
|
|
2125
|
-
const waitingForFirstActivity = Boolean(
|
|
2126
|
-
modelRequestStartedAt
|
|
2127
|
-
&& (stage === 'connecting' || stage === 'requesting')
|
|
2128
|
-
&& firstActivityAt <= modelRequestStartedAt
|
|
2129
|
-
);
|
|
2130
|
-
return {
|
|
2131
|
-
stage,
|
|
2132
|
-
askStartedAt,
|
|
2133
|
-
modelRequestStartedAt,
|
|
2134
|
-
firstActivityAt,
|
|
2135
|
-
lastStreamDeltaAt: entry.lastStreamDeltaAt || 0,
|
|
2136
|
-
toolStartedAt: entry.toolStartedAt || 0,
|
|
2137
|
-
lastProgressAt: entry.lastProgressAt || 0,
|
|
2138
|
-
updatedAt: entry.updatedAt || 0,
|
|
2139
|
-
hasFirstActivity: Boolean(firstActivityAt && (!askStartedAt || firstActivityAt >= askStartedAt)),
|
|
2140
|
-
waitingForFirstActivity,
|
|
2141
|
-
};
|
|
2142
|
-
}
|
|
2143
|
-
|
|
2144
|
-
/**
|
|
2145
|
-
* Iterate all active session runtimes. Used by the stream watchdog.
|
|
2146
|
-
* Returns an iterable of [sessionId, entry] pairs; consumers should
|
|
2147
|
-
* treat entries as read-only snapshots and avoid mutating them.
|
|
2148
|
-
*/
|
|
2149
|
-
export function forEachSessionRuntime() {
|
|
2150
|
-
return _runtimeState.entries();
|
|
2151
|
-
}
|
|
2152
|
-
|
|
2153
929
|
// --- Incremental metric persistence (fix A) ---
|
|
2154
|
-
//
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
return next;
|
|
2163
|
-
}
|
|
2164
|
-
|
|
2165
|
-
export function resolveUsageMetricsTurnId(session, delta = {}) {
|
|
2166
|
-
if (delta.usageMetricsTurnId != null && Number.isFinite(Number(delta.usageMetricsTurnId))) {
|
|
2167
|
-
return Number(delta.usageMetricsTurnId);
|
|
2168
|
-
}
|
|
2169
|
-
return Number(session?.usageMetricsTurnId) || 0;
|
|
2170
|
-
}
|
|
2171
|
-
|
|
2172
|
-
/** Advance loop metrics epoch when agentLoop resets its iteration counter (post-compact). */
|
|
2173
|
-
export function bumpUsageMetricsEpoch(session) {
|
|
2174
|
-
if (!session || typeof session !== 'object') return 0;
|
|
2175
|
-
const next = (Number(session.usageMetricsEpoch) || 0) + 1;
|
|
2176
|
-
session.usageMetricsEpoch = next;
|
|
2177
|
-
return next;
|
|
2178
|
-
}
|
|
2179
|
-
|
|
2180
|
-
/**
|
|
2181
|
-
* Resolve usage-metrics epoch for idempotency (exported for regression smoke).
|
|
2182
|
-
* Prefers session.usageMetricsEpoch (bumped in loop on compact reset) and optional
|
|
2183
|
-
* delta.usageMetricsEpoch; falls back to iteration regression when loop did not bump.
|
|
2184
|
-
*/
|
|
2185
|
-
export function resolveUsageMetricsEpoch(session, delta = {}) {
|
|
2186
|
-
if (!session) return 0;
|
|
2187
|
-
let epoch = Number(session.usageMetricsEpoch) || 0;
|
|
2188
|
-
if (delta.usageMetricsEpoch != null && Number.isFinite(Number(delta.usageMetricsEpoch))) {
|
|
2189
|
-
epoch = Math.max(epoch, Number(delta.usageMetricsEpoch));
|
|
2190
|
-
}
|
|
2191
|
-
const idx = Number(delta.iterationIndex);
|
|
2192
|
-
const prevLastIdx = typeof session.lastIterationIndex === 'number'
|
|
2193
|
-
? session.lastIterationIndex
|
|
2194
|
-
: null;
|
|
2195
|
-
if (
|
|
2196
|
-
(delta.usageMetricsEpoch == null || !Number.isFinite(Number(delta.usageMetricsEpoch)))
|
|
2197
|
-
&& prevLastIdx !== null
|
|
2198
|
-
&& Number.isFinite(idx)
|
|
2199
|
-
&& idx < prevLastIdx
|
|
2200
|
-
) {
|
|
2201
|
-
epoch += 1;
|
|
2202
|
-
}
|
|
2203
|
-
return epoch;
|
|
2204
|
-
}
|
|
2205
|
-
|
|
2206
|
-
export function usageMetricsSourceKey(delta = {}) {
|
|
2207
|
-
const raw = delta.source ?? delta.usageSource;
|
|
2208
|
-
if (raw == null || raw === '') return 'provider_send';
|
|
2209
|
-
return String(raw);
|
|
2210
|
-
}
|
|
2211
|
-
|
|
2212
|
-
/** Idempotency key for incremental usage persistence (exported for regression smoke). */
|
|
2213
|
-
export function usageMetricsIdempotencyKey(sessionId, session, delta = {}) {
|
|
2214
|
-
const turnId = resolveUsageMetricsTurnId(session, delta);
|
|
2215
|
-
const epoch = resolveUsageMetricsEpoch(session, delta);
|
|
2216
|
-
const source = usageMetricsSourceKey(delta);
|
|
2217
|
-
return `${sessionId}:${turnId}:${epoch}:${delta.iterationIndex}:${source}`;
|
|
2218
|
-
}
|
|
2219
|
-
|
|
2220
|
-
function uncachedInputTokensForProvider(provider, inputTokens, cachedReadTokens = 0, cacheWriteTokens = 0) {
|
|
2221
|
-
const input = Number(inputTokens) || 0;
|
|
2222
|
-
if (input <= 0) return 0;
|
|
2223
|
-
// Anthropic-style providers report input_tokens excluding cache reads; OpenAI
|
|
2224
|
-
// Responses/Gemini-style providers report input_tokens inclusive of cached
|
|
2225
|
-
// prefix tokens. Keep both views so UI can show the real context footprint
|
|
2226
|
-
// and the fresh/new token portion without mistaking cache hits for a cache
|
|
2227
|
-
// break.
|
|
2228
|
-
if (providerInputExcludesCache(provider)) return input + (Number(cacheWriteTokens) || 0);
|
|
2229
|
-
return Math.max(input - (Number(cachedReadTokens) || 0) - (Number(cacheWriteTokens) || 0), 0);
|
|
2230
|
-
}
|
|
2231
|
-
|
|
2232
|
-
/**
|
|
2233
|
-
* Apply terminal ask usage to session totals. Skips lifetime totals when incremental
|
|
2234
|
-
* per-iteration persistence already counted this turn (askSession path).
|
|
2235
|
-
*/
|
|
2236
|
-
export function applyAskTerminalUsageTotals(session, result, options = {}) {
|
|
2237
|
-
if (!session || !result?.usage) return;
|
|
2238
|
-
const skipTotals = options.skipTotalsIfIncremental === true;
|
|
2239
|
-
if (!skipTotals) {
|
|
2240
|
-
const inputTokens = result.usage.inputTokens || 0;
|
|
2241
|
-
const outputTokens = result.usage.outputTokens || 0;
|
|
2242
|
-
const cachedTokens = result.usage.cachedTokens || 0;
|
|
2243
|
-
const cacheWriteTokens = result.usage.cacheWriteTokens || 0;
|
|
2244
|
-
const uncachedInputTokens = uncachedInputTokensForProvider(session.provider, inputTokens, cachedTokens, cacheWriteTokens);
|
|
2245
|
-
session.totalInputTokens = (session.totalInputTokens || 0) + inputTokens;
|
|
2246
|
-
session.totalOutputTokens = (session.totalOutputTokens || 0) + outputTokens;
|
|
2247
|
-
session.tokensCumulative = (session.tokensCumulative || 0)
|
|
2248
|
-
+ inputTokens
|
|
2249
|
-
+ outputTokens;
|
|
2250
|
-
session.totalCachedReadTokens = (session.totalCachedReadTokens || 0) + cachedTokens;
|
|
2251
|
-
session.totalCacheWriteTokens = (session.totalCacheWriteTokens || 0) + cacheWriteTokens;
|
|
2252
|
-
session.totalUncachedInputTokens = (session.totalUncachedInputTokens || 0) + uncachedInputTokens;
|
|
2253
|
-
}
|
|
2254
|
-
const _lastTurn = result.lastTurnUsage || result.usage || {};
|
|
2255
|
-
const _lastInputTokens = _lastTurn.inputTokens || 0;
|
|
2256
|
-
const _lastCachedReadTokens = _lastTurn.cachedTokens || 0;
|
|
2257
|
-
const _lastCacheWriteTokens = _lastTurn.cacheWriteTokens || 0;
|
|
2258
|
-
session.lastInputTokens = _lastInputTokens;
|
|
2259
|
-
session.lastOutputTokens = _lastTurn.outputTokens || 0;
|
|
2260
|
-
session.lastCachedReadTokens = _lastCachedReadTokens;
|
|
2261
|
-
session.lastCacheWriteTokens = _lastCacheWriteTokens;
|
|
2262
|
-
session.lastUncachedInputTokens = uncachedInputTokensForProvider(
|
|
2263
|
-
session.provider,
|
|
2264
|
-
_lastInputTokens,
|
|
2265
|
-
_lastCachedReadTokens,
|
|
2266
|
-
_lastCacheWriteTokens,
|
|
2267
|
-
);
|
|
2268
|
-
const _inputExcludesCache = providerInputExcludesCache(session.provider);
|
|
2269
|
-
session.lastContextTokens = _inputExcludesCache
|
|
2270
|
-
? _lastInputTokens + _lastCachedReadTokens + _lastCacheWriteTokens
|
|
2271
|
-
: _lastInputTokens;
|
|
2272
|
-
session.lastContextTokensUpdatedAt = Date.now();
|
|
2273
|
-
session.lastContextTokensStaleAfterCompact = false;
|
|
2274
|
-
}
|
|
2275
|
-
|
|
2276
|
-
/**
|
|
2277
|
-
* Persist incremental usage delta immediately after each provider.send iteration.
|
|
2278
|
-
* Idempotency key `sessionId:turnId:epoch:iterationIndex:source` scopes retries
|
|
2279
|
-
* per ask, compaction epoch, iteration, and usage source.
|
|
2280
|
-
*/
|
|
2281
|
-
export async function persistIterationMetrics(delta) {
|
|
2282
|
-
if (!delta || !delta.sessionId) return;
|
|
2283
|
-
const { sessionId, iterationIndex, deltaInput, deltaOutput, deltaCachedRead, deltaCacheWrite, ts } = delta;
|
|
2284
|
-
const runtimeEntry = _runtimeState.get(sessionId);
|
|
2285
|
-
const session = runtimeEntry?.session ?? loadSession(sessionId);
|
|
2286
|
-
if (!session || session.closed) return;
|
|
2287
|
-
const epoch = resolveUsageMetricsEpoch(session, delta);
|
|
2288
|
-
if (epoch !== (Number(session.usageMetricsEpoch) || 0)) {
|
|
2289
|
-
session.usageMetricsEpoch = epoch;
|
|
2290
|
-
}
|
|
2291
|
-
let seen = _metricSeenIter.get(sessionId);
|
|
2292
|
-
if (!seen) {
|
|
2293
|
-
seen = new Set();
|
|
2294
|
-
_metricSeenIter.set(sessionId, seen);
|
|
2295
|
-
}
|
|
2296
|
-
const ikey = usageMetricsIdempotencyKey(sessionId, session, delta);
|
|
2297
|
-
const isReplay = seen.has(ikey);
|
|
2298
|
-
seen.add(ikey);
|
|
2299
|
-
if (!isReplay) {
|
|
2300
|
-
if (runtimeEntry) runtimeEntry.usageMetricsTurnIncremental = true;
|
|
2301
|
-
const deltaUncachedInput = delta.deltaUncachedInput != null
|
|
2302
|
-
? Number(delta.deltaUncachedInput) || 0
|
|
2303
|
-
: uncachedInputTokensForProvider(session.provider, deltaInput, deltaCachedRead, deltaCacheWrite);
|
|
2304
|
-
session.totalInputTokens = (session.totalInputTokens || 0) + (deltaInput || 0);
|
|
2305
|
-
session.totalOutputTokens = (session.totalOutputTokens || 0) + (deltaOutput || 0);
|
|
2306
|
-
session.tokensCumulative = (session.tokensCumulative || 0) + (deltaInput || 0) + (deltaOutput || 0);
|
|
2307
|
-
// Cache totals — additive fields, default 0 on legacy sessions; both
|
|
2308
|
-
// are undefined-safe so the schema migrates lazily as new iterations
|
|
2309
|
-
// land. Keeps live + terminal aggregates in lock-step (loop.mjs already
|
|
2310
|
-
// includes cached_read / cache_write in its terminal usage rollup).
|
|
2311
|
-
session.totalCachedReadTokens = (session.totalCachedReadTokens || 0) + (deltaCachedRead || 0);
|
|
2312
|
-
session.totalCacheWriteTokens = (session.totalCacheWriteTokens || 0) + (deltaCacheWrite || 0);
|
|
2313
|
-
session.totalUncachedInputTokens = (session.totalUncachedInputTokens || 0) + deltaUncachedInput;
|
|
2314
|
-
// Window snapshot updated per iteration so agent type=list reflects the
|
|
2315
|
-
// most-recent provider-reported input size even for short dispatches
|
|
2316
|
-
// that finish before askSession's terminal save lands.
|
|
2317
|
-
session.lastInputTokens = deltaInput || 0;
|
|
2318
|
-
session.lastOutputTokens = deltaOutput || 0;
|
|
2319
|
-
session.lastCachedReadTokens = deltaCachedRead || 0;
|
|
2320
|
-
session.lastCacheWriteTokens = deltaCacheWrite || 0;
|
|
2321
|
-
session.lastUncachedInputTokens = deltaUncachedInput;
|
|
2322
|
-
// Normalized last-call context footprint: how many prompt tokens the
|
|
2323
|
-
// model actually saw on the most-recent send, comparable ACROSS
|
|
2324
|
-
// providers. Anthropic reports input_tokens EXCLUDING cache (cache_read
|
|
2325
|
-
// is a separate field), so the cached portion must be added back to
|
|
2326
|
-
// reflect real context size; openai/grok/gemini already fold cached
|
|
2327
|
-
// tokens INTO the input count, so input alone is the footprint.
|
|
2328
|
-
const _inputExcludesCache = providerInputExcludesCache(session.provider);
|
|
2329
|
-
session.lastContextTokens = _inputExcludesCache
|
|
2330
|
-
? (deltaInput || 0) + (deltaCachedRead || 0) + (deltaCacheWrite || 0)
|
|
2331
|
-
: (deltaInput || 0);
|
|
2332
|
-
session.lastContextTokensUpdatedAt = ts || Date.now();
|
|
2333
|
-
session.lastContextTokensStaleAfterCompact = false;
|
|
2334
|
-
}
|
|
2335
|
-
session.lastIterationIndex = iterationIndex;
|
|
2336
|
-
session.updatedAt = ts || Date.now();
|
|
2337
|
-
await saveSessionAsync(session, { expectedGeneration: session.generation });
|
|
2338
|
-
}
|
|
2339
|
-
|
|
2340
|
-
function standaloneStatusRouteInfo(session) {
|
|
2341
|
-
if (!session) return null;
|
|
2342
|
-
// autoCompactTokenLimit is an EXPLICIT sub-boundary auto-compact limit only.
|
|
2343
|
-
// Do NOT fall back to compactBoundaryTokens/contextWindow here — that
|
|
2344
|
-
// labels a derived full-window value as an explicit limit, which the
|
|
2345
|
-
// runtime would treat as the compaction trigger and collapse the buffer.
|
|
2346
|
-
// The boundary/window stays available via contextWindow/rawContextWindow.
|
|
2347
|
-
const _boundary = Number(session.compactBoundaryTokens || session.contextWindow || 0);
|
|
2348
|
-
const _limit = Number(session.autoCompactTokenLimit || 0);
|
|
2349
|
-
const explicitAutoCompactTokenLimit = _limit > 0 && (!_boundary || _limit < _boundary) ? _limit : null;
|
|
2350
|
-
return {
|
|
2351
|
-
provider: session.provider,
|
|
2352
|
-
model: session.model,
|
|
2353
|
-
modelDisplay: session.modelDisplay || session.displayName || session.model,
|
|
2354
|
-
effort: session.effort || '',
|
|
2355
|
-
fast: session.fast === true,
|
|
2356
|
-
contextWindow: session.contextWindow || null,
|
|
2357
|
-
rawContextWindow: session.rawContextWindow || session.contextWindow || null,
|
|
2358
|
-
effectiveContextWindowPercent: session.effectiveContextWindowPercent || null,
|
|
2359
|
-
autoCompactTokenLimit: explicitAutoCompactTokenLimit,
|
|
2360
|
-
presetId: session.presetId || null,
|
|
2361
|
-
presetName: session.presetName || null,
|
|
2362
|
-
};
|
|
2363
|
-
}
|
|
2364
|
-
|
|
2365
|
-
function recordStandaloneStatusTelemetry(session, result, durationMs) {
|
|
2366
|
-
if (!session || !result?.usage) return;
|
|
2367
|
-
const routeInfo = standaloneStatusRouteInfo(session);
|
|
2368
|
-
if (!routeInfo?.provider || !routeInfo?.model) return;
|
|
2369
|
-
const providerOut = {
|
|
2370
|
-
usage: result.usage,
|
|
2371
|
-
model: result.model,
|
|
2372
|
-
serviceTier: result.serviceTier,
|
|
2373
|
-
};
|
|
2374
|
-
// The transcript estimate is the SSOT for the displayed context footprint.
|
|
2375
|
-
// agentLoop()'s result has no `compact` field, so build a synthetic compact
|
|
2376
|
-
// arg carrying the live monotonic estimate (estimateMessagesTokens+reserve)
|
|
2377
|
-
// as afterTokens. This lights up summarizeGatewayUsage's estimate-based
|
|
2378
|
-
// contextUsedPct branch (provider input_tokens swing wildly / unbounded on
|
|
2379
|
-
// e.g. OpenAI gpt-5.5), and lets a genuine >100% pass through.
|
|
2380
|
-
const _estTokens = estimateTranscriptContextUsage(session.messages, session.tools || []);
|
|
2381
|
-
const _compactArg = { ...(result.compact && typeof result.compact === 'object' ? result.compact : {}), afterTokens: _estTokens };
|
|
2382
|
-
try {
|
|
2383
|
-
const summary = {
|
|
2384
|
-
...summarizeGatewayUsage(routeInfo, providerOut, _compactArg, durationMs),
|
|
2385
|
-
requestKind: 'chat',
|
|
2386
|
-
sessionId: session.id || null,
|
|
2387
|
-
toolCount: result.toolCallsTotal ?? null,
|
|
2388
|
-
messageCount: Array.isArray(session.messages) ? session.messages.length : null,
|
|
2389
|
-
cacheStrategy: session.providerCacheOpts?.cacheStrategy || null,
|
|
2390
|
-
};
|
|
2391
|
-
recordGatewayUsageEvent(summary);
|
|
2392
|
-
} catch {
|
|
2393
|
-
// Statusline telemetry must never affect the model turn.
|
|
2394
|
-
}
|
|
2395
|
-
|
|
2396
|
-
const provider = getProvider(routeInfo.provider);
|
|
2397
|
-
if (!provider) return;
|
|
2398
|
-
fetchOAuthUsageSnapshot(routeInfo, provider, (message) => {
|
|
2399
|
-
if (process.env.MIXDOG_STATUSLINE_TRACE) {
|
|
2400
|
-
process.stderr.write(`[statusline] ${message}\n`);
|
|
2401
|
-
}
|
|
2402
|
-
})
|
|
2403
|
-
.then((snapshot) => {
|
|
2404
|
-
try { buildGatewayLimits(routeInfo, providerOut, snapshot); } catch {}
|
|
2405
|
-
})
|
|
2406
|
-
.catch(() => {});
|
|
2407
|
-
}
|
|
930
|
+
// Usage-metrics accounting (idempotency Set, epoch/turn helpers, terminal +
|
|
931
|
+
// incremental persistence) moved to manager/usage-metrics.mjs. Its runtime
|
|
932
|
+
// accessor is now wired inside manager/runtime-liveness.mjs (which owns the
|
|
933
|
+
// _runtimeState Map), so persistIterationMetrics can read the live in-memory
|
|
934
|
+
// session and flag usageMetricsTurnIncremental.
|
|
935
|
+
//
|
|
936
|
+
// standaloneStatusRouteInfo + recordStandaloneStatusTelemetry moved to
|
|
937
|
+
// manager/status-telemetry.mjs (imported above).
|
|
2408
938
|
|
|
2409
939
|
/** Force-flush session metrics to disk. Used by watchdog terminal-reap (fix B). */
|
|
2410
940
|
export async function flushSessionMetrics(sessionId) {
|
|
@@ -2415,91 +945,6 @@ export async function flushSessionMetrics(sessionId) {
|
|
|
2415
945
|
await saveSessionAsync(session, { expectedGeneration: session.generation });
|
|
2416
946
|
}
|
|
2417
947
|
|
|
2418
|
-
/** Mark session hidden so listSessions() filters it out (runtime-only). */
|
|
2419
|
-
export function hideSessionFromList(sessionId) {
|
|
2420
|
-
if (!sessionId) return;
|
|
2421
|
-
const entry = _runtimeState.get(sessionId);
|
|
2422
|
-
if (entry) entry.listHidden = true;
|
|
2423
|
-
}
|
|
2424
|
-
|
|
2425
|
-
export function getSessionAbortSignal(sessionId) {
|
|
2426
|
-
return _runtimeState.get(sessionId)?.controller?.signal ?? null;
|
|
2427
|
-
}
|
|
2428
|
-
|
|
2429
|
-
/**
|
|
2430
|
-
* Return the most recent "session is making progress" timestamp.
|
|
2431
|
-
*
|
|
2432
|
-
* Combines three independent progress signals so an idle watchdog can stay
|
|
2433
|
-
* alive across both streaming and long tool calls:
|
|
2434
|
-
* - lastStreamDeltaAt: provider stream chunk landed
|
|
2435
|
-
* - toolStartedAt: a tool call just kicked off (nested tool work may
|
|
2436
|
-
* stall the outer stream for a while; this keeps the watchdog from
|
|
2437
|
-
* killing legitimate sub-agent runs)
|
|
2438
|
-
* - askStartedAt: ask just started; covers the pre-stream connect window
|
|
2439
|
-
*
|
|
2440
|
-
* Returns 0 when the runtime entry is unknown so callers can decide to
|
|
2441
|
-
* either skip the watchdog or treat 0 as "no progress yet".
|
|
2442
|
-
*/
|
|
2443
|
-
export function getSessionLastProgressAt(sessionId) {
|
|
2444
|
-
const entry = _runtimeState.get(sessionId);
|
|
2445
|
-
if (!entry) return 0;
|
|
2446
|
-
return Math.max(
|
|
2447
|
-
entry.lastProgressAt || 0,
|
|
2448
|
-
entry.lastStreamDeltaAt || 0,
|
|
2449
|
-
entry.toolStartedAt || 0,
|
|
2450
|
-
entry.askStartedAt || 0,
|
|
2451
|
-
);
|
|
2452
|
-
}
|
|
2453
|
-
|
|
2454
|
-
/**
|
|
2455
|
-
* Link a parent AbortSignal to a sub-session's controller so that aborting
|
|
2456
|
-
* the parent (fan-out deadline or caller ESC) tears down the agent role's
|
|
2457
|
-
* provider call promptly. Safe to call after prepareAgentSession but before
|
|
2458
|
-
* askSession completes. No-op if the session runtime isn't found.
|
|
2459
|
-
*
|
|
2460
|
-
* @param {string} sessionId — the sub-session to abort
|
|
2461
|
-
* @param {AbortSignal} parentSignal — upstream signal (from fan-out coordinator)
|
|
2462
|
-
*/
|
|
2463
|
-
export function linkParentSignalToSession(sessionId, parentSignal) {
|
|
2464
|
-
if (!(parentSignal instanceof AbortSignal)) return;
|
|
2465
|
-
const entry = _touchRuntime(sessionId);
|
|
2466
|
-
if (!entry.controller) entry.controller = createAbortController();
|
|
2467
|
-
const abortReason = () => {
|
|
2468
|
-
const reason = parentSignal.reason;
|
|
2469
|
-
if (reason instanceof Error) return reason;
|
|
2470
|
-
if (reason !== undefined && reason !== null && reason !== '') return new Error(String(reason));
|
|
2471
|
-
return new Error('parent signal aborted');
|
|
2472
|
-
};
|
|
2473
|
-
if (parentSignal.aborted) {
|
|
2474
|
-
_unlinkParentAbortListener(entry);
|
|
2475
|
-
try { entry.controller.abort(abortReason()); } catch { /* ignore */ }
|
|
2476
|
-
return;
|
|
2477
|
-
}
|
|
2478
|
-
_unlinkParentAbortListener(entry);
|
|
2479
|
-
const onParentAbort = () => {
|
|
2480
|
-
try { entry.controller?.abort(abortReason()); } catch { /* ignore */ }
|
|
2481
|
-
};
|
|
2482
|
-
parentSignal.addEventListener('abort', onParentAbort, { once: true });
|
|
2483
|
-
entry.parentAbortLink = { signal: parentSignal, listener: onParentAbort };
|
|
2484
|
-
}
|
|
2485
|
-
function _unlinkParentAbortListener(entry) {
|
|
2486
|
-
const link = entry?.parentAbortLink;
|
|
2487
|
-
if (!link) return;
|
|
2488
|
-
try { link.signal.removeEventListener('abort', link.listener); } catch { /* ignore */ }
|
|
2489
|
-
entry.parentAbortLink = null;
|
|
2490
|
-
}
|
|
2491
|
-
function _clearSessionRuntime(id) {
|
|
2492
|
-
if (id) {
|
|
2493
|
-
_stopToolActivityHeartbeat(id);
|
|
2494
|
-
_unlinkParentAbortListener(_runtimeState.get(id));
|
|
2495
|
-
_runtimeState.delete(id);
|
|
2496
|
-
// R15: also drop the per-session metric-idempotency Set; otherwise it
|
|
2497
|
-
// grows O(sessions x iterations) for the whole server lifetime since
|
|
2498
|
-
// nothing else deletes from _metricSeenIter on session close.
|
|
2499
|
-
_metricSeenIter.delete(id);
|
|
2500
|
-
}
|
|
2501
|
-
}
|
|
2502
|
-
|
|
2503
948
|
/**
|
|
2504
949
|
* Wrap an async call so that if the session's controller aborts mid-flight,
|
|
2505
950
|
* the wrapper settles with a SessionClosedError even if the underlying promise
|
|
@@ -2562,363 +1007,6 @@ const _sessionLocks = new Map();
|
|
|
2562
1007
|
// queue contract, two call sites. Rich content is kept in memory for the live
|
|
2563
1008
|
// relay path; the disk mirror stores only a text fallback so image bytes do not
|
|
2564
1009
|
// leak into the pending-message JSON.
|
|
2565
|
-
const _sessionPendingMessages = new Map();
|
|
2566
|
-
const PENDING_MESSAGES_FILE = 'session-pending-messages.json';
|
|
2567
|
-
const PENDING_MESSAGES_MODE = 0o600;
|
|
2568
|
-
const _pendingPersistBuffers = new Map();
|
|
2569
|
-
let _pendingPersistImmediate = null;
|
|
2570
|
-
|
|
2571
|
-
function pendingMessagesPath() {
|
|
2572
|
-
return join(resolvePluginData(), PENDING_MESSAGES_FILE);
|
|
2573
|
-
}
|
|
2574
|
-
|
|
2575
|
-
function isValidPendingSessionId(sessionId) {
|
|
2576
|
-
return typeof sessionId === 'string' && /^[A-Za-z0-9_-]+$/.test(sessionId);
|
|
2577
|
-
}
|
|
2578
|
-
|
|
2579
|
-
function normalizePendingStore(raw) {
|
|
2580
|
-
const sessions = raw && typeof raw === 'object' && raw.sessions && typeof raw.sessions === 'object'
|
|
2581
|
-
? raw.sessions
|
|
2582
|
-
: {};
|
|
2583
|
-
const out = { version: 1, updatedAt: Date.now(), sessions: {} };
|
|
2584
|
-
for (const [sid, value] of Object.entries(sessions)) {
|
|
2585
|
-
if (!isValidPendingSessionId(sid) || !Array.isArray(value)) continue;
|
|
2586
|
-
const q = value
|
|
2587
|
-
.map((entry) => {
|
|
2588
|
-
if (typeof entry === 'string') return entry;
|
|
2589
|
-
if (entry && typeof entry === 'object' && typeof entry.message === 'string') return entry.message;
|
|
2590
|
-
return '';
|
|
2591
|
-
})
|
|
2592
|
-
.filter(Boolean);
|
|
2593
|
-
if (q.length > 0) out.sessions[sid] = q;
|
|
2594
|
-
}
|
|
2595
|
-
return out;
|
|
2596
|
-
}
|
|
2597
|
-
|
|
2598
|
-
function normalizePendingMessageEntry(entry) {
|
|
2599
|
-
if (typeof entry === 'string') {
|
|
2600
|
-
const text = entry.trim();
|
|
2601
|
-
return text ? { content: text, text } : null;
|
|
2602
|
-
}
|
|
2603
|
-
if (Array.isArray(entry)) {
|
|
2604
|
-
if (entry.length === 0) return null;
|
|
2605
|
-
const text = promptContentText(entry).trim();
|
|
2606
|
-
return { content: entry, text };
|
|
2607
|
-
}
|
|
2608
|
-
if (!entry || typeof entry !== 'object') return null;
|
|
2609
|
-
const content = Object.prototype.hasOwnProperty.call(entry, 'content') ? entry.content : null;
|
|
2610
|
-
if (content == null) return null;
|
|
2611
|
-
const text = typeof entry.text === 'string' ? entry.text.trim() : promptContentText(content).trim();
|
|
2612
|
-
if (Array.isArray(content)) return content.length > 0 ? { content, text } : null;
|
|
2613
|
-
if (typeof content === 'string') {
|
|
2614
|
-
const value = content.trim();
|
|
2615
|
-
return value ? { content: value, text: text || value } : null;
|
|
2616
|
-
}
|
|
2617
|
-
const fallback = promptContentText(content).trim();
|
|
2618
|
-
return fallback ? { content: fallback, text: text || fallback } : null;
|
|
2619
|
-
}
|
|
2620
|
-
|
|
2621
|
-
function pendingMessageText(entry) {
|
|
2622
|
-
const normalized = normalizePendingMessageEntry(entry);
|
|
2623
|
-
return normalized ? String(normalized.text || promptContentText(normalized.content) || '').trim() : '';
|
|
2624
|
-
}
|
|
2625
|
-
|
|
2626
|
-
function pendingMessageQueueEntry(entry) {
|
|
2627
|
-
const normalized = normalizePendingMessageEntry(entry);
|
|
2628
|
-
if (!normalized) return null;
|
|
2629
|
-
if (typeof normalized.content === 'string' && normalized.content === normalized.text) return normalized.content;
|
|
2630
|
-
return { content: normalized.content, text: normalized.text || promptContentText(normalized.content).trim() };
|
|
2631
|
-
}
|
|
2632
|
-
|
|
2633
|
-
function persistPendingMessages(sessionId, messages) {
|
|
2634
|
-
if (!isValidPendingSessionId(sessionId)) return 0;
|
|
2635
|
-
const persistedMessages = (Array.isArray(messages) ? messages : [messages])
|
|
2636
|
-
.map(pendingMessageText)
|
|
2637
|
-
.filter(Boolean);
|
|
2638
|
-
if (persistedMessages.length === 0) return 0;
|
|
2639
|
-
let depth = 0;
|
|
2640
|
-
try {
|
|
2641
|
-
updateJsonAtomicSync(pendingMessagesPath(), (raw) => {
|
|
2642
|
-
const next = normalizePendingStore(raw);
|
|
2643
|
-
const q = Array.isArray(next.sessions[sessionId]) ? next.sessions[sessionId] : [];
|
|
2644
|
-
q.push(...persistedMessages);
|
|
2645
|
-
next.sessions[sessionId] = q;
|
|
2646
|
-
next.updatedAt = Date.now();
|
|
2647
|
-
depth = q.length;
|
|
2648
|
-
return next;
|
|
2649
|
-
}, { compact: true, lock: true, mode: PENDING_MESSAGES_MODE, fsync: false });
|
|
2650
|
-
} catch (err) {
|
|
2651
|
-
try { process.stderr.write(`[session] pending-message persist failed sessionId=${sessionId}: ${err?.message || err}\n`); } catch {}
|
|
2652
|
-
}
|
|
2653
|
-
return depth;
|
|
2654
|
-
}
|
|
2655
|
-
|
|
2656
|
-
function flushPendingMessagePersistsSync() {
|
|
2657
|
-
if (_pendingPersistImmediate) {
|
|
2658
|
-
try { clearImmediate(_pendingPersistImmediate); } catch {}
|
|
2659
|
-
_pendingPersistImmediate = null;
|
|
2660
|
-
}
|
|
2661
|
-
if (_pendingPersistBuffers.size === 0) return;
|
|
2662
|
-
const batches = [..._pendingPersistBuffers.entries()];
|
|
2663
|
-
_pendingPersistBuffers.clear();
|
|
2664
|
-
for (const [sid, messages] of batches) {
|
|
2665
|
-
persistPendingMessages(sid, messages);
|
|
2666
|
-
}
|
|
2667
|
-
}
|
|
2668
|
-
|
|
2669
|
-
function schedulePendingMessagePersist(sessionId, message) {
|
|
2670
|
-
if (!isValidPendingSessionId(sessionId)) return 0;
|
|
2671
|
-
const persistedMessage = pendingMessageText(message);
|
|
2672
|
-
if (!persistedMessage) return 0;
|
|
2673
|
-
const q = _pendingPersistBuffers.get(sessionId) || [];
|
|
2674
|
-
q.push(persistedMessage);
|
|
2675
|
-
_pendingPersistBuffers.set(sessionId, q);
|
|
2676
|
-
if (!_pendingPersistImmediate) {
|
|
2677
|
-
_pendingPersistImmediate = setImmediate(() => {
|
|
2678
|
-
_pendingPersistImmediate = null;
|
|
2679
|
-
flushPendingMessagePersistsSync();
|
|
2680
|
-
});
|
|
2681
|
-
}
|
|
2682
|
-
return q.length;
|
|
2683
|
-
}
|
|
2684
|
-
|
|
2685
|
-
function takeBufferedPendingMessages(sessionId) {
|
|
2686
|
-
if (!isValidPendingSessionId(sessionId)) return [];
|
|
2687
|
-
const buffered = _pendingPersistBuffers.get(sessionId);
|
|
2688
|
-
if (!buffered || buffered.length === 0) return [];
|
|
2689
|
-
_pendingPersistBuffers.delete(sessionId);
|
|
2690
|
-
return buffered.slice();
|
|
2691
|
-
}
|
|
2692
|
-
|
|
2693
|
-
function drainPersistedPendingMessages(sessionId) {
|
|
2694
|
-
if (!isValidPendingSessionId(sessionId)) return [];
|
|
2695
|
-
let drained = [];
|
|
2696
|
-
try {
|
|
2697
|
-
updateJsonAtomicSync(pendingMessagesPath(), (raw) => {
|
|
2698
|
-
const next = normalizePendingStore(raw);
|
|
2699
|
-
const q = Array.isArray(next.sessions[sessionId]) ? next.sessions[sessionId] : [];
|
|
2700
|
-
drained = q.filter((m) => typeof m === 'string' && m.length > 0);
|
|
2701
|
-
if (drained.length === 0) return undefined;
|
|
2702
|
-
delete next.sessions[sessionId];
|
|
2703
|
-
next.updatedAt = Date.now();
|
|
2704
|
-
return next;
|
|
2705
|
-
}, { compact: true, lock: true, mode: PENDING_MESSAGES_MODE, fsync: false });
|
|
2706
|
-
} catch (err) {
|
|
2707
|
-
try { process.stderr.write(`[session] pending-message drain failed sessionId=${sessionId}: ${err?.message || err}\n`); } catch {}
|
|
2708
|
-
}
|
|
2709
|
-
return drained;
|
|
2710
|
-
}
|
|
2711
|
-
|
|
2712
|
-
export function enqueuePendingMessage(sessionId, message) {
|
|
2713
|
-
const entry = pendingMessageQueueEntry(message);
|
|
2714
|
-
if (!sessionId || !entry) return 0;
|
|
2715
|
-
let q = _sessionPendingMessages.get(sessionId);
|
|
2716
|
-
if (!q) { q = []; _sessionPendingMessages.set(sessionId, q); }
|
|
2717
|
-
q.push(entry);
|
|
2718
|
-
const bufferedDepth = schedulePendingMessagePersist(sessionId, entry);
|
|
2719
|
-
return Math.max(q.length, bufferedDepth || 0);
|
|
2720
|
-
}
|
|
2721
|
-
export function drainPendingMessages(sessionId) {
|
|
2722
|
-
const q = _sessionPendingMessages.get(sessionId);
|
|
2723
|
-
const memory = q && q.length > 0 ? q.slice() : [];
|
|
2724
|
-
_sessionPendingMessages.delete(sessionId);
|
|
2725
|
-
const persisted = [...takeBufferedPendingMessages(sessionId), ...drainPersistedPendingMessages(sessionId)];
|
|
2726
|
-
const memoryVisible = modelVisiblePendingMessages(memory);
|
|
2727
|
-
const persistedVisible = modelVisiblePendingMessages(persisted);
|
|
2728
|
-
if (memoryVisible.length === 0) return persistedVisible;
|
|
2729
|
-
if (persistedVisible.length === 0) return memoryVisible;
|
|
2730
|
-
const persistedTexts = persistedVisible.map(pendingMessageText);
|
|
2731
|
-
const prefixMatches = memoryVisible.every((m, i) => persistedTexts[i] === pendingMessageText(m));
|
|
2732
|
-
if (prefixMatches) return [...memoryVisible, ...persistedVisible.slice(memoryVisible.length)];
|
|
2733
|
-
const out = persistedVisible.slice();
|
|
2734
|
-
const seen = new Set(persistedTexts);
|
|
2735
|
-
for (const m of memoryVisible) {
|
|
2736
|
-
const text = pendingMessageText(m);
|
|
2737
|
-
if (!text || seen.has(text)) continue;
|
|
2738
|
-
out.push(m);
|
|
2739
|
-
seen.add(text);
|
|
2740
|
-
}
|
|
2741
|
-
return out;
|
|
2742
|
-
}
|
|
2743
|
-
|
|
2744
|
-
function promptContentText(content) {
|
|
2745
|
-
if (typeof content === 'string') return content;
|
|
2746
|
-
if (Array.isArray(content)) {
|
|
2747
|
-
return content.map((part) => {
|
|
2748
|
-
if (typeof part === 'string') return part;
|
|
2749
|
-
if (part?.type === 'text') return part.text || '';
|
|
2750
|
-
if (part?.type === 'image') return '[Image]';
|
|
2751
|
-
return part?.text || '';
|
|
2752
|
-
}).filter(Boolean).join('\n');
|
|
2753
|
-
}
|
|
2754
|
-
return String(content ?? '');
|
|
2755
|
-
}
|
|
2756
|
-
|
|
2757
|
-
function hasModelVisiblePromptContent(prompt) {
|
|
2758
|
-
return !!promptContentText(prompt).trim();
|
|
2759
|
-
}
|
|
2760
|
-
|
|
2761
|
-
function promptContentBytes(content) {
|
|
2762
|
-
try {
|
|
2763
|
-
if (typeof content === 'string') return Buffer.byteLength(content, 'utf8');
|
|
2764
|
-
return Buffer.byteLength(JSON.stringify(content), 'utf8');
|
|
2765
|
-
} catch {
|
|
2766
|
-
return Buffer.byteLength(promptContentText(content), 'utf8');
|
|
2767
|
-
}
|
|
2768
|
-
}
|
|
2769
|
-
|
|
2770
|
-
function prefixUserTurnContent(content, contextBlock) {
|
|
2771
|
-
if (!contextBlock) return content;
|
|
2772
|
-
if (Array.isArray(content)) {
|
|
2773
|
-
return [{ type: 'text', text: `${contextBlock}# Task\n` }, ...content];
|
|
2774
|
-
}
|
|
2775
|
-
return `${contextBlock}# Task\n${content}`;
|
|
2776
|
-
}
|
|
2777
|
-
|
|
2778
|
-
function prefixSessionStartContent(content, sessionBlock) {
|
|
2779
|
-
if (!sessionBlock) return content;
|
|
2780
|
-
if (Array.isArray(content)) {
|
|
2781
|
-
return [{ type: 'text', text: `${sessionBlock}\n\n` }, ...content];
|
|
2782
|
-
}
|
|
2783
|
-
return `${sessionBlock}\n\n${content}`;
|
|
2784
|
-
}
|
|
2785
|
-
|
|
2786
|
-
function localIsoDate(date = new Date()) {
|
|
2787
|
-
const year = date.getFullYear();
|
|
2788
|
-
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
2789
|
-
const day = String(date.getDate()).padStart(2, '0');
|
|
2790
|
-
return `${year}-${month}-${day}`;
|
|
2791
|
-
}
|
|
2792
|
-
|
|
2793
|
-
function localDateTimeWithZone(date = new Date()) {
|
|
2794
|
-
const datePart = localIsoDate(date);
|
|
2795
|
-
const hh = String(date.getHours()).padStart(2, '0');
|
|
2796
|
-
const mm = String(date.getMinutes()).padStart(2, '0');
|
|
2797
|
-
const ss = String(date.getSeconds()).padStart(2, '0');
|
|
2798
|
-
let zone = '';
|
|
2799
|
-
try { zone = Intl.DateTimeFormat().resolvedOptions().timeZone || ''; } catch {}
|
|
2800
|
-
return zone ? `${datePart} ${hh}:${mm}:${ss} ${zone}` : `${datePart} ${hh}:${mm}:${ss}`;
|
|
2801
|
-
}
|
|
2802
|
-
|
|
2803
|
-
function temporalPromptText(content) {
|
|
2804
|
-
const text = promptContentText(content)
|
|
2805
|
-
.replace(/\s+/g, ' ')
|
|
2806
|
-
.trim()
|
|
2807
|
-
.toLowerCase();
|
|
2808
|
-
return text;
|
|
2809
|
-
}
|
|
2810
|
-
|
|
2811
|
-
function promptNeedsDateReminder(content) {
|
|
2812
|
-
const text = temporalPromptText(content);
|
|
2813
|
-
if (!text) return false;
|
|
2814
|
-
return /(?:오늘|내일|어제|모레|그저께|요즘|최근|방금|아까|현재\s*(?:날짜|시간|시각)|지금\s*(?:몇\s*시|시간|날짜|요일)|몇\s*월\s*몇\s*일|몇\s*시|무슨\s*요일|요일|날짜|이번\s*(?:주|달|월|년)|지난\s*(?:주|달|월|년)|다음\s*(?:주|달|월|년)|올해|작년|내년|today|tomorrow|yesterday|recently|current\s+(?:date|time)|what\s+(?:date|time)|which\s+day|weekday|this\s+(?:week|month|year)|last\s+(?:week|month|year)|next\s+(?:week|month|year))/i.test(text);
|
|
2815
|
-
}
|
|
2816
|
-
|
|
2817
|
-
function promptNeedsTimeReminder(content) {
|
|
2818
|
-
const text = temporalPromptText(content);
|
|
2819
|
-
if (!text) return false;
|
|
2820
|
-
return /(?:현재\s*(?:시간|시각)|지금\s*(?:몇\s*시|시간)|몇\s*시|시각|시간|current\s+time|what\s+time|time\s+is\s+it)/i.test(text);
|
|
2821
|
-
}
|
|
2822
|
-
|
|
2823
|
-
function buildCurrentTimeBlock(content) {
|
|
2824
|
-
const needsTime = promptNeedsTimeReminder(content);
|
|
2825
|
-
if (!needsTime && !promptNeedsDateReminder(content)) return '';
|
|
2826
|
-
return localDateTimeWithZone(new Date());
|
|
2827
|
-
}
|
|
2828
|
-
|
|
2829
|
-
function sessionModelDisplay(model) {
|
|
2830
|
-
const text = String(model || '').trim();
|
|
2831
|
-
if (!text) return '';
|
|
2832
|
-
return text
|
|
2833
|
-
.replace(/-\d{4}-\d{2}-\d{2}$/, '')
|
|
2834
|
-
.replace(/^gpt-/i, 'GPT-')
|
|
2835
|
-
.replace(/(?:^|-)([a-z])/g, (m) => m.toUpperCase());
|
|
2836
|
-
}
|
|
2837
|
-
|
|
2838
|
-
function buildSessionStartBlock(session, cwd) {
|
|
2839
|
-
if (!session || session.owner === 'agent') return '';
|
|
2840
|
-
const lines = ['# Session'];
|
|
2841
|
-
const effectiveCwd = String(cwd || session.cwd || '').trim();
|
|
2842
|
-
if (effectiveCwd) lines.push(`Cwd: ${effectiveCwd}`);
|
|
2843
|
-
const modelBits = [
|
|
2844
|
-
sessionModelDisplay(session.model),
|
|
2845
|
-
session.effort ? String(session.effort).trim().toUpperCase() : '',
|
|
2846
|
-
session.fast === true ? 'FAST' : '',
|
|
2847
|
-
].filter(Boolean);
|
|
2848
|
-
if (modelBits.length) lines.push(`Model: ${modelBits.join(' · ')}`);
|
|
2849
|
-
const workflowName = String(session.workflow?.name || session.workflow?.id || '').trim();
|
|
2850
|
-
if (workflowName) lines.push(`Workflow: ${workflowName}`);
|
|
2851
|
-
return lines.length > 1 ? lines.join('\n') : '';
|
|
2852
|
-
}
|
|
2853
|
-
|
|
2854
|
-
function isReferenceFilesMessage(message) {
|
|
2855
|
-
return message?.role === 'user'
|
|
2856
|
-
&& typeof message.content === 'string'
|
|
2857
|
-
&& /^Reference files:\s*/i.test(message.content.trimStart());
|
|
2858
|
-
}
|
|
2859
|
-
|
|
2860
|
-
function isProtectedContextUserMessage(message) {
|
|
2861
|
-
return message?.role === 'user'
|
|
2862
|
-
&& typeof message.content === 'string'
|
|
2863
|
-
&& message.content.trimStart().startsWith('<system-reminder>');
|
|
2864
|
-
}
|
|
2865
|
-
|
|
2866
|
-
function hasUserConversationMessage(messages) {
|
|
2867
|
-
return (Array.isArray(messages) ? messages : []).some((message) => (
|
|
2868
|
-
message?.role === 'user'
|
|
2869
|
-
&& !isProtectedContextUserMessage(message)
|
|
2870
|
-
&& !isReferenceFilesMessage(message)
|
|
2871
|
-
));
|
|
2872
|
-
}
|
|
2873
|
-
|
|
2874
|
-
function modelVisiblePendingMessages(messages) {
|
|
2875
|
-
return (Array.isArray(messages) ? messages : [])
|
|
2876
|
-
.map(pendingMessageQueueEntry)
|
|
2877
|
-
.filter(Boolean)
|
|
2878
|
-
.filter((message) => !isInternalRuntimeNotificationText(
|
|
2879
|
-
message && typeof message === 'object' && Object.prototype.hasOwnProperty.call(message, 'content')
|
|
2880
|
-
? message.content
|
|
2881
|
-
: message,
|
|
2882
|
-
));
|
|
2883
|
-
}
|
|
2884
|
-
|
|
2885
|
-
export function _mergePendingMessageEntries(entries) {
|
|
2886
|
-
const normalized = (Array.isArray(entries) ? entries : [])
|
|
2887
|
-
.map(normalizePendingMessageEntry)
|
|
2888
|
-
.filter(Boolean);
|
|
2889
|
-
if (normalized.length === 0) return null;
|
|
2890
|
-
const displayText = normalized.map((entry) => entry.text || promptContentText(entry.content))
|
|
2891
|
-
.filter((text) => String(text || '').trim())
|
|
2892
|
-
.join('\n');
|
|
2893
|
-
if (normalized.every((entry) => typeof entry.content === 'string')) {
|
|
2894
|
-
return {
|
|
2895
|
-
content: normalized.map((entry) => entry.content).filter(Boolean).join('\n'),
|
|
2896
|
-
text: displayText,
|
|
2897
|
-
count: normalized.length,
|
|
2898
|
-
};
|
|
2899
|
-
}
|
|
2900
|
-
const parts = [];
|
|
2901
|
-
for (const entry of normalized) {
|
|
2902
|
-
if (typeof entry.content === 'string') {
|
|
2903
|
-
if (entry.content.trim()) parts.push({ type: 'text', text: entry.content });
|
|
2904
|
-
} else if (Array.isArray(entry.content)) {
|
|
2905
|
-
parts.push(...entry.content);
|
|
2906
|
-
} else {
|
|
2907
|
-
const text = promptContentText(entry.content);
|
|
2908
|
-
if (text.trim()) parts.push({ type: 'text', text });
|
|
2909
|
-
}
|
|
2910
|
-
parts.push({ type: 'text', text: '\n' });
|
|
2911
|
-
}
|
|
2912
|
-
while (parts.length && parts[parts.length - 1]?.type === 'text' && parts[parts.length - 1]?.text === '\n') parts.pop();
|
|
2913
|
-
return { content: parts, text: displayText || promptContentText(parts), count: normalized.length };
|
|
2914
|
-
}
|
|
2915
|
-
|
|
2916
|
-
function isInternalRuntimeNotificationText(content) {
|
|
2917
|
-
return contractIsInternalRuntimeNotificationText(promptContentText(content));
|
|
2918
|
-
}
|
|
2919
|
-
|
|
2920
|
-
export const _isInternalRuntimeNotificationText = isInternalRuntimeNotificationText;
|
|
2921
|
-
|
|
2922
1010
|
function isInternalCancelledAssistantMessage(message) {
|
|
2923
1011
|
if (!message || message.role !== 'assistant') return false;
|
|
2924
1012
|
if (message.cancelled === true) return true;
|
|
@@ -3052,7 +1140,7 @@ async function persistCompactedOutgoingAfterAskFailure({
|
|
|
3052
1140
|
}) {
|
|
3053
1141
|
if (!activeSession || activeSession.closed === true) return;
|
|
3054
1142
|
if (!Array.isArray(turnOutgoing) || turnOutgoing.length === 0) return;
|
|
3055
|
-
const currentRuntime =
|
|
1143
|
+
const currentRuntime = _getRuntimeEntry(sessionId);
|
|
3056
1144
|
if (currentRuntime?.closed || currentRuntime?.generation !== askGeneration) return;
|
|
3057
1145
|
const sanitized = sanitizeSessionMessagesForModel(turnOutgoing);
|
|
3058
1146
|
const priorSanitized = sanitizeSessionMessagesForModel(
|
|
@@ -3130,7 +1218,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
3130
1218
|
}
|
|
3131
1219
|
}
|
|
3132
1220
|
if (!hasModelVisiblePromptContent(prompt)) {
|
|
3133
|
-
_unlinkParentAbortListener(
|
|
1221
|
+
_unlinkParentAbortListener(_getRuntimeEntry(sessionId));
|
|
3134
1222
|
return _result;
|
|
3135
1223
|
}
|
|
3136
1224
|
// ── Synchronous pre-await setup (must happen before any await so
|
|
@@ -3360,7 +1448,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
3360
1448
|
}
|
|
3361
1449
|
// Post-loop validation: if closeSession() landed while we were awaiting,
|
|
3362
1450
|
// drop the save so the tombstone on disk isn't overwritten.
|
|
3363
|
-
const currentRuntime =
|
|
1451
|
+
const currentRuntime = _getRuntimeEntry(sessionId);
|
|
3364
1452
|
if (currentRuntime?.closed || currentRuntime?.generation !== askGeneration) {
|
|
3365
1453
|
const reason = currentRuntime?.closedReason;
|
|
3366
1454
|
throw new SessionClosedError(sessionId, `closed during call (reason=${reason || 'unknown'})`, reason || null);
|
|
@@ -3576,7 +1664,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
3576
1664
|
// stale in-flight array once the turn unwinds.
|
|
3577
1665
|
if (activeSession) activeSession.liveTurnMessages = null;
|
|
3578
1666
|
if (err instanceof SessionClosedError) {
|
|
3579
|
-
const currentRuntime =
|
|
1667
|
+
const currentRuntime = _getRuntimeEntry(sessionId);
|
|
3580
1668
|
if (!currentRuntime?.closed) {
|
|
3581
1669
|
if (activeSession) {
|
|
3582
1670
|
const originalMessages = Array.isArray(activeSession.messages) ? activeSession.messages : [];
|
|
@@ -3644,14 +1732,14 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
3644
1732
|
continue;
|
|
3645
1733
|
}
|
|
3646
1734
|
}
|
|
3647
|
-
_unlinkParentAbortListener(
|
|
1735
|
+
_unlinkParentAbortListener(_getRuntimeEntry(sessionId));
|
|
3648
1736
|
return _result;
|
|
3649
1737
|
}
|
|
3650
1738
|
} finally {
|
|
3651
1739
|
// Clear the controller only if it's still ours (closeSession may have
|
|
3652
1740
|
// swapped it). Leave the rest of the runtime entry intact so agent type=list
|
|
3653
1741
|
// can still surface the final stage (done/error/cancelling).
|
|
3654
|
-
const entry =
|
|
1742
|
+
const entry = _getRuntimeEntry(sessionId);
|
|
3655
1743
|
if (entry && entry.generation === askGeneration) {
|
|
3656
1744
|
_unlinkParentAbortListener(entry);
|
|
3657
1745
|
entry.controller = null;
|
|
@@ -3730,7 +1818,7 @@ export function getSession(id) {
|
|
|
3730
1818
|
export function listSessions(opts = {}) {
|
|
3731
1819
|
const includeClosed = opts.includeClosed === true;
|
|
3732
1820
|
const sessions = listStoredSessionSummaries();
|
|
3733
|
-
const hiddenIds = new Set([...
|
|
1821
|
+
const hiddenIds = new Set([..._runtimeEntries()].filter(([, e]) => e.listHidden).map(([id]) => id));
|
|
3734
1822
|
// Tombstoned sessions (closed===true) are excluded unless the caller opts in
|
|
3735
1823
|
// (e.g. agent list includeClosed:true).
|
|
3736
1824
|
return sessions.filter(s => !hiddenIds.has(s.id) && (includeClosed || s.closed !== true));
|
|
@@ -3817,6 +1905,7 @@ export async function clearSessionMessages(sessionId, options = {}) {
|
|
|
3817
1905
|
const afterTokens = estimateTranscriptContextUsage(keep, session.tools || []);
|
|
3818
1906
|
const now = Date.now();
|
|
3819
1907
|
session.messages = keep;
|
|
1908
|
+
session.sessionStartMetaInjected = false;
|
|
3820
1909
|
session.totalInputTokens = 0;
|
|
3821
1910
|
session.totalOutputTokens = 0;
|
|
3822
1911
|
session.totalCachedReadTokens = 0;
|
|
@@ -3906,12 +1995,21 @@ export async function updateSessionStatus(id, status) {
|
|
|
3906
1995
|
* Long-term cleanup: `sweepTombstones()` below unlinks tombstones older than
|
|
3907
1996
|
* TOMBSTONE_MAX_AGE_MS (24h — vastly longer than any realistic in-flight race).
|
|
3908
1997
|
*/
|
|
3909
|
-
export function closeSession(id, reason = 'manual') {
|
|
1998
|
+
export function closeSession(id, reason = 'manual', opts = {}) {
|
|
1999
|
+
// tombstone=false: detach runtime resources (heartbeat, bash shells,
|
|
2000
|
+
// controller abort, runtime-map clear) WITHOUT planting the disk
|
|
2001
|
+
// tombstone. Used for non-empty sessions on /resume-away, /new, and
|
|
2002
|
+
// TUI exit — previously every one of those paths unconditionally
|
|
2003
|
+
// tombstoned the outgoing session, which made it vanish from the
|
|
2004
|
+
// Resume list immediately and get hard-deleted by sweepTombstones()
|
|
2005
|
+
// after 24h even though it had real conversation content worth
|
|
2006
|
+
// resuming. Only truly-empty scratch sessions should still tombstone.
|
|
2007
|
+
const tombstone = opts.tombstone !== false;
|
|
3910
2008
|
if (!id) return false;
|
|
3911
2009
|
_stopToolActivityHeartbeat(id);
|
|
3912
2010
|
// Prefer in-memory runtime session — allBashSessionIds may not be persisted
|
|
3913
2011
|
// yet for shells opened in the current turn (BL-bash-disk-sync).
|
|
3914
|
-
const inMemory =
|
|
2012
|
+
const inMemory = _getRuntimeEntry(id)?.session;
|
|
3915
2013
|
const persisted = inMemory || loadSession(id);
|
|
3916
2014
|
const bashSessionId = persisted?.implicitBashSessionId || null;
|
|
3917
2015
|
// Collect all persistent bash shells created during this session.
|
|
@@ -3922,9 +2020,19 @@ export function closeSession(id, reason = 'manual') {
|
|
|
3922
2020
|
// against old session records that only have implicitBashSessionId.
|
|
3923
2021
|
if (bashSessionId && !allBashIds.includes(bashSessionId)) allBashIds.push(bashSessionId);
|
|
3924
2022
|
// 1. Tombstone first — this wins the race against saveSession().
|
|
3925
|
-
|
|
2023
|
+
// Skipped when tombstone=false: no closed:true marker is planted, so
|
|
2024
|
+
// the session file stays intact and resumeSession() will accept it.
|
|
2025
|
+
// We still bump the on-disk generation via bumpSessionGeneration() —
|
|
2026
|
+
// that alone is what protects the session from a late save race: any
|
|
2027
|
+
// saveSession() still in flight from this detached turn (e.g. the
|
|
2028
|
+
// cancel-cleanup save below) carries the OLD generation as its
|
|
2029
|
+
// expectedGeneration, so _shouldDrop()'s ownership-counter rule drops
|
|
2030
|
+
// it once disk generation moves past that. Without this bump the late
|
|
2031
|
+
// write could silently overwrite the session after the user resumes
|
|
2032
|
+
// it back (BL: burned-session late-save clobber).
|
|
2033
|
+
const newGen = tombstone ? markSessionClosed(id, reason) : bumpSessionGeneration(id, reason);
|
|
3926
2034
|
// 2. Mark runtime as closed so post-await validation in askSession fires.
|
|
3927
|
-
const entry =
|
|
2035
|
+
const entry = _getRuntimeEntry(id);
|
|
3928
2036
|
if (entry) {
|
|
3929
2037
|
entry.closed = true;
|
|
3930
2038
|
entry.closedReason = reason;
|
|
@@ -3943,7 +2051,7 @@ export function closeSession(id, reason = 'manual') {
|
|
|
3943
2051
|
try {
|
|
3944
2052
|
const askStartedAt = entry?.askStartedAt;
|
|
3945
2053
|
const durationMs = (typeof askStartedAt === 'number') ? (Date.now() - askStartedAt) : null;
|
|
3946
|
-
const parts = [`session=${id}`, `reason=${reason}`];
|
|
2054
|
+
const parts = [`session=${id}`, `reason=${reason}`, `tombstone=${tombstone}`];
|
|
3947
2055
|
if (durationMs != null) parts.push(`duration=${durationMs}ms`);
|
|
3948
2056
|
if (process.env.MIXDOG_DEBUG_SESSION_LOG) process.stderr.write(`[agent-close] ${parts.join(' ')}\n`);
|
|
3949
2057
|
} catch { /* best-effort */ }
|
|
@@ -3958,6 +2066,10 @@ export function closeSession(id, reason = 'manual') {
|
|
|
3958
2066
|
// or Map entries across session lifetime. Fire-and-forget — close path
|
|
3959
2067
|
// should not await disk IO; errors are swallowed inside.
|
|
3960
2068
|
try { clearOffloadSession(id); } catch { /* ignore */ }
|
|
2069
|
+
// Drop the in-memory pending-message queue and any buffered-persist entry
|
|
2070
|
+
// for this session — otherwise both Maps accumulate one entry per closed
|
|
2071
|
+
// session for the life of the mcp-server.
|
|
2072
|
+
_dropPendingMessageState(id);
|
|
3961
2073
|
// 4. Defer runtime map clear to next tick so any settling askSession can
|
|
3962
2074
|
// observe `closed=true` / bumped generation before we yank the entry.
|
|
3963
2075
|
// Disk tombstone remains — that's what blocks resurrection.
|
|
@@ -3969,7 +2081,7 @@ export function closeSession(id, reason = 'manual') {
|
|
|
3969
2081
|
export function abortSessionTurn(id, reason = 'turn-abort') {
|
|
3970
2082
|
if (!id) return false;
|
|
3971
2083
|
_stopToolActivityHeartbeat(id);
|
|
3972
|
-
const entry =
|
|
2084
|
+
const entry = _getRuntimeEntry(id);
|
|
3973
2085
|
if (!entry || entry.closed) return false;
|
|
3974
2086
|
entry.stage = 'cancelling';
|
|
3975
2087
|
entry.closedReason = reason;
|
|
@@ -4019,7 +2131,7 @@ function sweepIdleSessions({ includeTombstones = true } = {}) {
|
|
|
4019
2131
|
// Skip entries with an active in-flight controller — aborting
|
|
4020
2132
|
// them via closeSession() is the safe path; clearing the runtime
|
|
4021
2133
|
// without signalling the controller leaves orphan provider work.
|
|
4022
|
-
const rtEntry =
|
|
2134
|
+
const rtEntry = _getRuntimeEntry(d.id);
|
|
4023
2135
|
if (rtEntry && rtEntry.controller && !rtEntry.controller.signal?.aborted) {
|
|
4024
2136
|
try { closeSession(d.id, 'idle-sweep'); } catch { /* ignore */ }
|
|
4025
2137
|
} else {
|
|
@@ -4087,7 +2199,7 @@ export function sweepTombstones() {
|
|
|
4087
2199
|
}
|
|
4088
2200
|
|
|
4089
2201
|
function hasActiveRuntimeWork() {
|
|
4090
|
-
for (const [, entry] of
|
|
2202
|
+
for (const [, entry] of _runtimeEntries()) {
|
|
4091
2203
|
if (!entry || entry.closed === true) continue;
|
|
4092
2204
|
if (entry.controller && !entry.controller.signal?.aborted) return true;
|
|
4093
2205
|
if (['connecting', 'requesting', 'streaming', 'tool_running', 'cancelling'].includes(entry.stage)) return true;
|