mixdog 0.9.3 → 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 +7 -3
- 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/freevar-smoke.mjs +95 -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/provider-toolcall-test.mjs +7 -3
- 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 +23 -63
- package/scripts/tui-render-smoke.mjs +90 -0
- package/scripts/webhook-smoke.mjs +208 -0
- package/src/agents/debugger/AGENT.md +4 -1
- package/src/agents/heavy-worker/AGENT.md +6 -5
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +2 -1
- package/src/agents/worker/AGENT.md +8 -4
- package/src/lib/rules-builder.cjs +4 -0
- package/src/mixdog-session-runtime.mjs +632 -2042
- 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/rules/agent/00-common.md +2 -0
- package/src/rules/lead/lead-brief.md +12 -0
- package/src/rules/lead/lead-tool.md +0 -11
- 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/providers/anthropic-effort.mjs +62 -20
- 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 +81 -1281
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -3
- 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 +43 -1013
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -3
- 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/openai-codex-model.mjs +108 -0
- 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 +40 -1143
- 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 +297 -2123
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +130 -1002
- 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/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-policy.mjs +14 -2
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -3
- 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/termination.mjs +58 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +251 -397
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +7 -4
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +12 -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/usage-metrics.mjs +210 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +166 -1087
- 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/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 +32 -41
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
- 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 -92
- 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 +28 -265
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +0 -6
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +57 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +10 -122
- package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
- 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 +0 -21
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -72
- package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
- 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 +229 -663
- package/src/runtime/channels/lib/backend-dispatch.mjs +44 -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/output-forwarder.mjs +1 -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/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/tool-defs.mjs +11 -130
- package/src/runtime/memory/index.mjs +201 -1948
- 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/embedding-warmup.mjs +58 -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 +24 -842
- 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-store.mjs +22 -2
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
- package/src/runtime/memory/lib/query-handlers.mjs +780 -0
- package/src/runtime/memory/lib/recall-format.mjs +55 -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 +5 -13
- 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/atomic-file.mjs +26 -1
- package/src/runtime/shared/launcher-control.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 +23 -0
- package/src/runtime/shared/update-checker.mjs +7 -4
- package/src/session-runtime/config-helpers.mjs +84 -2
- package/src/session-runtime/config-lifecycle.mjs +232 -0
- package/src/session-runtime/cwd-plugins.mjs +226 -0
- package/src/session-runtime/mcp-glue.mjs +177 -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 +11 -9
- 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/settings-api.mjs +319 -0
- package/src/session-runtime/tool-catalog.mjs +29 -29
- package/src/session-runtime/tool-defs.mjs +84 -0
- package/src/session-runtime/warmup-schedulers.mjs +201 -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 +110 -671
- package/src/standalone/channel-worker.mjs +4 -7
- package/src/standalone/explore-tool.mjs +30 -9
- 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 +77 -870
- package/src/standalone/memory-runtime-proxy.mjs +7 -0
- package/src/standalone/opencode-go-login.mjs +5 -1
- package/src/standalone/provider-admin.mjs +1 -16
- package/src/standalone/usage-dashboard.mjs +3 -1
- package/src/tui/App.jsx +945 -8094
- 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 +23 -101
- 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 +52 -594
- 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/display-width.mjs +20 -3
- package/src/tui/dist/index.mjs +19652 -18630
- package/src/tui/engine/agent-job-feed.mjs +133 -0
- package/src/tui/engine/notification-plan.mjs +76 -0
- package/src/tui/engine/render-timing.mjs +17 -0
- package/src/tui/engine/tool-approval.mjs +94 -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.mjs +122 -562
- package/src/tui/figures.mjs +5 -0
- package/src/tui/index.jsx +105 -0
- package/src/tui/input-editing.mjs +2 -2
- package/src/tui/markdown/format-token.mjs +4 -1
- package/src/tui/statusline-ansi-bridge.mjs +11 -3
- package/src/tui/theme.mjs +6 -0
- 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 +67 -501
- package/src/ui/tool-card.mjs +0 -1
- package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
- package/src/workflows/default/WORKFLOW.md +1 -1
- package/src/workflows/sequential/WORKFLOW.md +1 -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/external-tool-adapters.test.mjs +0 -162
- 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/task-notification-envelope.test.mjs +0 -107
- 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/input-editing.selection.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/paste-fix.test.mjs +0 -119
- 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,28 +1,20 @@
|
|
|
1
1
|
import { createRequire } from 'module';
|
|
2
2
|
import { fileURLToPath } from 'url';
|
|
3
3
|
import { randomBytes, createHash } from 'crypto';
|
|
4
|
-
import { getProvider
|
|
5
|
-
import { fetchOAuthUsageSnapshot } from '../providers/oauth-usage.mjs';
|
|
4
|
+
import { getProvider } from '../providers/registry.mjs';
|
|
6
5
|
// Image content is kept in-memory and in the model-visible history so multi-turn
|
|
7
6
|
// recognition works reliably (live transcript always retains images). The
|
|
8
7
|
// stored-history placeholder swap now happens only at disk-serialization time
|
|
9
8
|
// inside the session store, so it is no longer imported here.
|
|
10
9
|
import {
|
|
11
|
-
recallFastTrackCompactMessages,
|
|
12
|
-
semanticCompactMessages,
|
|
13
|
-
pruneToolOutputsUnanchored,
|
|
14
|
-
effectiveBudget as compactEffectiveBudget,
|
|
15
|
-
compactTypeIsRecallFastTrack,
|
|
16
|
-
compactTypeIsSemantic,
|
|
17
10
|
normalizeCompactType,
|
|
18
11
|
DEFAULT_COMPACT_TYPE,
|
|
19
12
|
SUMMARY_PREFIX,
|
|
20
|
-
drainSessionCycle1,
|
|
21
13
|
} from './compact.mjs';
|
|
22
|
-
import { estimateMessagesTokens,
|
|
14
|
+
import { estimateMessagesTokens, estimateTranscriptContextUsage } from './context-utils.mjs';
|
|
23
15
|
import { executeInternalTool } from '../internal-tools.mjs';
|
|
24
16
|
import { collectSkillsCached, buildSkillManifest, composeSystemPrompt } from '../context/collect.mjs';
|
|
25
|
-
import { saveSession, saveSessionAsync, loadSession, listStoredSessionSummaries, sweepStaleSessions, markSessionClosed,
|
|
17
|
+
import { saveSession, saveSessionAsync, loadSession, listStoredSessionSummaries, sweepStaleSessions, markSessionClosed, bumpSessionGeneration, setLiveSession } from './store.mjs';
|
|
26
18
|
import { clearReadDedupSession, tryPrefetchCached, setPrefetchCached } from './read-dedup.mjs';
|
|
27
19
|
import { clearOffloadSession } from './tool-result-offload.mjs';
|
|
28
20
|
import { classifyResultKind } from './result-classification.mjs';
|
|
@@ -31,12 +23,16 @@ import { logLlmCall } from '../../../shared/llm/usage-log.mjs';
|
|
|
31
23
|
import { appendAgentTrace } from '../agent-trace.mjs';
|
|
32
24
|
import { isAgentOwner } from '../agent-owner.mjs';
|
|
33
25
|
import { getHiddenAgent } from '../internal-agents.mjs';
|
|
34
|
-
import {
|
|
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';
|
|
35
29
|
import {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
30
|
+
recordStandaloneStatusTelemetry,
|
|
31
|
+
} from './manager/status-telemetry.mjs';
|
|
32
|
+
import {
|
|
33
|
+
normalizeStaleCompactingStage,
|
|
34
|
+
runSessionCompaction,
|
|
35
|
+
} from './manager/compaction-runner.mjs';
|
|
40
36
|
// Split modules — see manager/ directory. manager.mjs is now a thin facade that
|
|
41
37
|
// orchestrates these cohesive units while keeping the exact public surface.
|
|
42
38
|
import {
|
|
@@ -59,9 +55,6 @@ import {
|
|
|
59
55
|
preserveBufferConfigFields,
|
|
60
56
|
resolveSessionContextMeta,
|
|
61
57
|
compactTriggerForSession,
|
|
62
|
-
compactTargetBudget,
|
|
63
|
-
semanticCompactionEnabledForSession,
|
|
64
|
-
compactTypeForSession,
|
|
65
58
|
} from './manager/context-meta.mjs';
|
|
66
59
|
import {
|
|
67
60
|
promptContentText,
|
|
@@ -80,11 +73,89 @@ import {
|
|
|
80
73
|
drainPendingMessages,
|
|
81
74
|
_dropPendingMessageState,
|
|
82
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 });
|
|
83
142
|
// Re-export split-module public/test surface unchanged so every importer of the
|
|
84
143
|
// old symbol names keeps resolving through the facade.
|
|
85
144
|
export { previewSessionTools };
|
|
86
145
|
export { _mergePendingMessageEntries, enqueuePendingMessage, drainPendingMessages };
|
|
87
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
|
+
};
|
|
88
159
|
let _codeGraphRuntimePromise = null;
|
|
89
160
|
let _agentLoopPromise = null;
|
|
90
161
|
let _bashSessionRuntimePromise = null;
|
|
@@ -146,7 +217,6 @@ export class SessionClosedError extends Error {
|
|
|
146
217
|
this.reason = closeReason || null;
|
|
147
218
|
}
|
|
148
219
|
}
|
|
149
|
-
const HEARTBEAT_THROTTLE_MS = 60_000; // 60s
|
|
150
220
|
// Cap how long the terminal unwind blocks on the post-result session save.
|
|
151
221
|
// The result is already produced (and relayed for agent surfaces) before this
|
|
152
222
|
// save, so a stalled disk write must not hold askSession() open — otherwise the
|
|
@@ -186,442 +256,6 @@ let nextId = Date.now();
|
|
|
186
256
|
export const _resolveSessionContextMeta = resolveSessionContextMeta;
|
|
187
257
|
export const _compactTriggerForSession = compactTriggerForSession;
|
|
188
258
|
export const _preserveBufferConfigFields = preserveBufferConfigFields;
|
|
189
|
-
// 'compacting' is a transient in-flight stage written just before semantic /
|
|
190
|
-
// recall-fasttrack compaction runs. If the process crashes or only partially
|
|
191
|
-
// saves while it is set, a later load/resume reads a session that is NOT
|
|
192
|
-
// actually compacting but whose UI marker (App.jsx / ContextPanel) shows
|
|
193
|
-
// "Compacting conversation" permanently. Normalize that stale transient stage
|
|
194
|
-
// to 'interrupted' so the surface recovers. Terminal stages (post_turn /
|
|
195
|
-
// manual / auto_clear / *_failed / overflow_failed) are intentionally left as
|
|
196
|
-
// the durable record of the last real outcome.
|
|
197
|
-
function normalizeStaleCompactingStage(session) {
|
|
198
|
-
const c = session?.compaction;
|
|
199
|
-
if (!c || typeof c !== 'object') return false;
|
|
200
|
-
if (c.lastStage !== 'compacting' && c.inProgress !== true) return false;
|
|
201
|
-
c.lastStage = 'interrupted';
|
|
202
|
-
c.inProgress = false;
|
|
203
|
-
c.lastCheckedAt = Date.now();
|
|
204
|
-
return true;
|
|
205
|
-
}
|
|
206
|
-
function addCompactUsageToSession(session, usage) {
|
|
207
|
-
if (!session || !usage) return;
|
|
208
|
-
const inputTokens = usage.inputTokens || 0;
|
|
209
|
-
const outputTokens = usage.outputTokens || 0;
|
|
210
|
-
const cachedTokens = usage.cachedTokens || 0;
|
|
211
|
-
const cacheWriteTokens = usage.cacheWriteTokens || 0;
|
|
212
|
-
const uncachedInputTokens = uncachedInputTokensForProvider(session.provider, inputTokens, cachedTokens, cacheWriteTokens);
|
|
213
|
-
session.totalInputTokens = (session.totalInputTokens || 0) + inputTokens;
|
|
214
|
-
session.totalOutputTokens = (session.totalOutputTokens || 0) + outputTokens;
|
|
215
|
-
session.totalCachedReadTokens = (session.totalCachedReadTokens || 0) + cachedTokens;
|
|
216
|
-
session.totalCacheWriteTokens = (session.totalCacheWriteTokens || 0) + cacheWriteTokens;
|
|
217
|
-
session.totalUncachedInputTokens = (session.totalUncachedInputTokens || 0) + uncachedInputTokens;
|
|
218
|
-
session.tokensCumulative = (session.tokensCumulative || 0) + inputTokens + outputTokens;
|
|
219
|
-
}
|
|
220
|
-
async function runRecallFastTrackForSession(session, messages, opts = {}) {
|
|
221
|
-
const sessionId = opts.sessionId || session?.id || null;
|
|
222
|
-
if (!sessionId) throw new Error('recall-fasttrack requires a session id');
|
|
223
|
-
const query = `session:${sessionId}:all-chunks`;
|
|
224
|
-
const querySha = createHash('sha256').update(query).digest('hex').slice(0, 16);
|
|
225
|
-
const callerCtx = {
|
|
226
|
-
callerSessionId: sessionId,
|
|
227
|
-
callerCwd: session?.cwd || undefined,
|
|
228
|
-
routingSessionId: sessionId,
|
|
229
|
-
clientHostPid: session?.clientHostPid,
|
|
230
|
-
signal: opts.signal || null,
|
|
231
|
-
};
|
|
232
|
-
const hydrateLimit = positiveContextWindow(session?.compaction?.recallIngestLimit)
|
|
233
|
-
|| Math.max(500, Math.min(5000, messages.length || 0));
|
|
234
|
-
try {
|
|
235
|
-
await executeInternalTool('memory', {
|
|
236
|
-
action: 'ingest_session',
|
|
237
|
-
sessionId,
|
|
238
|
-
messages,
|
|
239
|
-
cwd: session?.cwd,
|
|
240
|
-
limit: hydrateLimit,
|
|
241
|
-
}, callerCtx);
|
|
242
|
-
} catch (err) {
|
|
243
|
-
try { process.stderr.write(`[session] recall-fasttrack ingest skipped (sess=${sessionId}): ${err?.message || err}\n`); } catch {}
|
|
244
|
-
}
|
|
245
|
-
const dumpArgs = {
|
|
246
|
-
action: 'dump_session_roots',
|
|
247
|
-
sessionId,
|
|
248
|
-
includeRaw: true,
|
|
249
|
-
limit: positiveContextWindow(session?.compaction?.recallChunkLimit ?? session?.compaction?.recallLimit) || hydrateLimit,
|
|
250
|
-
};
|
|
251
|
-
const runTool = (name, args) => executeInternalTool(name, args, callerCtx);
|
|
252
|
-
let recallText = await executeInternalTool('memory', dumpArgs, callerCtx);
|
|
253
|
-
let cycle1Text = '';
|
|
254
|
-
const hasRawRows = /(?:^|\n)# raw_pending\s+\d+\s+id=/i.test(String(recallText || ''));
|
|
255
|
-
if (hasRawRows) {
|
|
256
|
-
try {
|
|
257
|
-
// Drain this session's cycle1 in window×concurrency units until no
|
|
258
|
-
// raw rows remain, so the injected root is fully chunked rather than
|
|
259
|
-
// carrying the unprocessed transcript tail (single-pass left raw in).
|
|
260
|
-
const drained = await drainSessionCycle1(runTool, {
|
|
261
|
-
sessionId,
|
|
262
|
-
dumpArgs,
|
|
263
|
-
deadlineMs: positiveContextWindow(session?.compaction?.recallCycle1DeadlineMs) || 120_000,
|
|
264
|
-
maxPasses: positiveContextWindow(session?.compaction?.recallCycle1MaxPasses) || 0,
|
|
265
|
-
cycleArgs: {
|
|
266
|
-
min_batch: 1,
|
|
267
|
-
session_cap: 1,
|
|
268
|
-
batch_size: positiveContextWindow(session?.compaction?.recallCycle1BatchSize) || 100,
|
|
269
|
-
rows_per_session: positiveContextWindow(session?.compaction?.recallRowsPerSession) || 100,
|
|
270
|
-
window_size: positiveContextWindow(session?.compaction?.recallWindowSize) || 20,
|
|
271
|
-
concurrency: positiveContextWindow(session?.compaction?.recallConcurrency) || 5,
|
|
272
|
-
},
|
|
273
|
-
});
|
|
274
|
-
recallText = drained.recallText;
|
|
275
|
-
cycle1Text = drained.cycle1Text;
|
|
276
|
-
if (drained.rawRemaining > 0) {
|
|
277
|
-
try { process.stderr.write(`[session] recall-fasttrack drained passes=${drained.passes} rawRemaining=${drained.rawRemaining} (sess=${sessionId})\n`); } catch {}
|
|
278
|
-
}
|
|
279
|
-
} catch (err) {
|
|
280
|
-
try { process.stderr.write(`[session] recall-fasttrack cycle1 skipped (sess=${sessionId}): ${err?.message || err}\n`); } catch {}
|
|
281
|
-
}
|
|
282
|
-
} else {
|
|
283
|
-
cycle1Text = 'cycle1: skipped (session chunks already hydrated)';
|
|
284
|
-
}
|
|
285
|
-
return { query, querySha, recallText: [`session_id=${sessionId}`, cycle1Text, recallText].map(v => String(v || '').trim()).filter(Boolean).join('\n\n') };
|
|
286
|
-
}
|
|
287
|
-
// Element-identity change detection (same approach as loop.mjs messagesArrayChanged): two
|
|
288
|
-
// arrays are "unchanged" only when same length AND every slot is the same object
|
|
289
|
-
// reference. Used to reject a no-op prune (which returns a fresh array whose
|
|
290
|
-
// elements are the untouched originals) from being accepted as a recovery.
|
|
291
|
-
function messagesChanged(before, after) {
|
|
292
|
-
if (!Array.isArray(before) || !Array.isArray(after)) return before !== after;
|
|
293
|
-
if (before.length !== after.length) return true;
|
|
294
|
-
for (let i = 0; i < before.length; i += 1) {
|
|
295
|
-
if (before[i] !== after[i]) return true;
|
|
296
|
-
}
|
|
297
|
-
return false;
|
|
298
|
-
}
|
|
299
|
-
async function runSessionCompaction(session, opts = {}) {
|
|
300
|
-
if (!session || session.closed === true) return null;
|
|
301
|
-
const mode = opts.mode === 'auto' ? 'auto' : 'manual';
|
|
302
|
-
const force = opts.force === true || mode === 'manual';
|
|
303
|
-
if (mode === 'auto' && session.compaction?.auto === false) return null;
|
|
304
|
-
const messages = Array.isArray(session.messages) ? session.messages : [];
|
|
305
|
-
if (messages.length < 3 && !force) return null;
|
|
306
|
-
const boundary = positiveContextWindow(session.compactBoundaryTokens)
|
|
307
|
-
|| positiveContextWindow(session.autoCompactTokenLimit)
|
|
308
|
-
|| positiveContextWindow(session.contextWindow);
|
|
309
|
-
if (!boundary) {
|
|
310
|
-
if (force) throw new Error('compact: no context window is available for this session');
|
|
311
|
-
return null;
|
|
312
|
-
}
|
|
313
|
-
// Reserve must mirror loop.mjs (buildCompactPolicy): request reserve (tool
|
|
314
|
-
// schema) PLUS the configured reserve (session.compaction.reservedTokens or
|
|
315
|
-
// MIXDOG_AGENT_COMPACT_RESERVED_TOKENS env). The old request-only value left
|
|
316
|
-
// the manual / auto-clear compact budget without the configured headroom the
|
|
317
|
-
// loop path reserves, so a compacted transcript could overflow on next send.
|
|
318
|
-
const requestReserveTokens = estimateRequestReserveTokens(session.tools || []);
|
|
319
|
-
const configuredReserveTokens = positiveContextWindow(session.compaction?.reservedTokens)
|
|
320
|
-
|| positiveContextWindow(process.env.MIXDOG_AGENT_COMPACT_RESERVED_TOKENS)
|
|
321
|
-
|| 0;
|
|
322
|
-
const reserveTokens = requestReserveTokens + configuredReserveTokens;
|
|
323
|
-
const beforeMessageTokens = estimateMessagesTokens(messages);
|
|
324
|
-
const triggerTokens = compactTriggerForSession(session, boundary)
|
|
325
|
-
|| positiveContextWindow(session.compaction?.triggerTokens)
|
|
326
|
-
|| boundary;
|
|
327
|
-
const bufferTokens = Math.max(0, boundary - triggerTokens);
|
|
328
|
-
const bufferRatio = boundary ? (bufferTokens / boundary) : compactBufferRatioForSession(session);
|
|
329
|
-
const pressureTokens = estimateTranscriptContextUsage(messages, session.tools || []);
|
|
330
|
-
const beforeTokens = pressureTokens;
|
|
331
|
-
const compactType = compactTypeForSession(session);
|
|
332
|
-
if (!force && pressureTokens < triggerTokens) return {
|
|
333
|
-
changed: false,
|
|
334
|
-
reason: 'below threshold',
|
|
335
|
-
compactType,
|
|
336
|
-
beforeMessages: messages.length,
|
|
337
|
-
afterMessages: messages.length,
|
|
338
|
-
beforeTokens,
|
|
339
|
-
afterTokens: beforeTokens,
|
|
340
|
-
beforeMessageTokens,
|
|
341
|
-
afterMessageTokens: beforeMessageTokens,
|
|
342
|
-
pressureTokens,
|
|
343
|
-
triggerTokens,
|
|
344
|
-
bufferTokens,
|
|
345
|
-
bufferRatio,
|
|
346
|
-
boundaryTokens: boundary,
|
|
347
|
-
budgetTokens: boundary,
|
|
348
|
-
targetBudgetTokens: boundary,
|
|
349
|
-
reserveTokens,
|
|
350
|
-
semanticCompact: false,
|
|
351
|
-
};
|
|
352
|
-
const budgetSourceTokens = force ? Math.max(pressureTokens, triggerTokens) : pressureTokens;
|
|
353
|
-
const compactBudget = compactTargetBudget(boundary, reserveTokens, budgetSourceTokens);
|
|
354
|
-
const budget = compactBudget || boundary;
|
|
355
|
-
try { opts.onStageChange?.('compacting'); } catch { /* best-effort */ }
|
|
356
|
-
const provider = opts.provider || getProvider(session.provider) || null;
|
|
357
|
-
let compacted;
|
|
358
|
-
let compactError = null;
|
|
359
|
-
let semanticCompactResult = null;
|
|
360
|
-
let semanticCompactError = null;
|
|
361
|
-
let recallFastTrackResult = null;
|
|
362
|
-
let recallFastTrackError = null;
|
|
363
|
-
if (compactTypeIsRecallFastTrack(compactType)) {
|
|
364
|
-
try {
|
|
365
|
-
const recallPayload = await runRecallFastTrackForSession(session, messages, opts);
|
|
366
|
-
recallFastTrackResult = recallFastTrackCompactMessages(messages, budget, {
|
|
367
|
-
reserveTokens,
|
|
368
|
-
force: true,
|
|
369
|
-
recallText: recallPayload.recallText,
|
|
370
|
-
query: recallPayload.query,
|
|
371
|
-
querySha: recallPayload.querySha,
|
|
372
|
-
// Ingest just ran on the live transcript, so an empty recall dump
|
|
373
|
-
// means the memory pipeline is broken — do NOT erase history
|
|
374
|
-
// behind an empty summary shell. Empty recall now throws and is
|
|
375
|
-
// handled by the semantic fallback below (or recorded failure).
|
|
376
|
-
allowEmptyRecall: false,
|
|
377
|
-
tailTurns: positiveContextWindow(session.compaction?.tailTurns) || 2,
|
|
378
|
-
keepTokens: positiveContextWindow(session.compaction?.keepTokens ?? session.compaction?.keep?.tokens),
|
|
379
|
-
preserveRecentTokens: positiveContextWindow(session.compaction?.preserveRecentTokens),
|
|
380
|
-
});
|
|
381
|
-
if (Array.isArray(recallFastTrackResult?.messages)) {
|
|
382
|
-
compacted = recallFastTrackResult.messages;
|
|
383
|
-
}
|
|
384
|
-
} catch (err) {
|
|
385
|
-
recallFastTrackError = err;
|
|
386
|
-
compactError = err;
|
|
387
|
-
try {
|
|
388
|
-
process.stderr.write(`[session] recall-fasttrack ${mode} compact failed (sess=${session.id || 'unknown'}): ${err?.message || err}\n`);
|
|
389
|
-
} catch { /* best-effort */ }
|
|
390
|
-
// Degraded-compact fallback: recall-fasttrack failed (empty recall,
|
|
391
|
-
// ingest error, fit failure). Before recording a hard failure, try
|
|
392
|
-
// the semantic path once so auto-clear/manual compaction still makes
|
|
393
|
-
// progress WITHOUT shipping an empty-recall summary. History is only
|
|
394
|
-
// replaced when the semantic summary actually succeeds.
|
|
395
|
-
if (semanticCompactionEnabledForSession(session)
|
|
396
|
-
&& provider && typeof provider.send === 'function') {
|
|
397
|
-
try {
|
|
398
|
-
semanticCompactResult = await semanticCompactMessages(
|
|
399
|
-
provider,
|
|
400
|
-
messages,
|
|
401
|
-
opts.model || session.model,
|
|
402
|
-
budget,
|
|
403
|
-
{
|
|
404
|
-
reserveTokens,
|
|
405
|
-
providerName: session.provider || provider?.name || null,
|
|
406
|
-
sessionId: opts.sessionId || session.id || null,
|
|
407
|
-
signal: opts.signal || null,
|
|
408
|
-
promptCacheKey: session.promptCacheKey || null,
|
|
409
|
-
providerCacheKey: session.promptCacheKey || null,
|
|
410
|
-
timeoutMs: positiveContextWindow(session.compaction?.timeoutMs) || 30_000,
|
|
411
|
-
tailTurns: positiveContextWindow(session.compaction?.tailTurns) || 2,
|
|
412
|
-
keepTokens: positiveContextWindow(session.compaction?.keepTokens ?? session.compaction?.keep?.tokens),
|
|
413
|
-
preserveRecentTokens: positiveContextWindow(session.compaction?.preserveRecentTokens),
|
|
414
|
-
force: true,
|
|
415
|
-
},
|
|
416
|
-
);
|
|
417
|
-
if (Array.isArray(semanticCompactResult?.messages)) {
|
|
418
|
-
compacted = semanticCompactResult.messages;
|
|
419
|
-
compactError = null;
|
|
420
|
-
addCompactUsageToSession(session, semanticCompactResult.usage);
|
|
421
|
-
try {
|
|
422
|
-
process.stderr.write(`[session] degraded compact: recall-fasttrack failed, semantic fallback succeeded (sess=${session.id || 'unknown'}, mode=${mode})\n`);
|
|
423
|
-
} catch { /* best-effort */ }
|
|
424
|
-
}
|
|
425
|
-
} catch (fallbackErr) {
|
|
426
|
-
semanticCompactError = fallbackErr;
|
|
427
|
-
try {
|
|
428
|
-
process.stderr.write(`[session] degraded compact: semantic fallback also failed (sess=${session.id || 'unknown'}): ${fallbackErr?.message || fallbackErr}\n`);
|
|
429
|
-
} catch { /* best-effort */ }
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
} else if (compactTypeIsSemantic(compactType)) {
|
|
434
|
-
try {
|
|
435
|
-
if (!semanticCompactionEnabledForSession(session)) {
|
|
436
|
-
throw new Error('semantic compact is disabled for this session');
|
|
437
|
-
}
|
|
438
|
-
if (!provider || typeof provider.send !== 'function') {
|
|
439
|
-
throw new Error(`semantic compact provider unavailable: ${session.provider || 'unknown'}`);
|
|
440
|
-
}
|
|
441
|
-
semanticCompactResult = await semanticCompactMessages(
|
|
442
|
-
provider,
|
|
443
|
-
messages,
|
|
444
|
-
opts.model || session.model,
|
|
445
|
-
budget,
|
|
446
|
-
{
|
|
447
|
-
reserveTokens,
|
|
448
|
-
providerName: session.provider || provider?.name || null,
|
|
449
|
-
sessionId: opts.sessionId || session.id || null,
|
|
450
|
-
signal: opts.signal || null,
|
|
451
|
-
promptCacheKey: session.promptCacheKey || null,
|
|
452
|
-
providerCacheKey: session.promptCacheKey || null,
|
|
453
|
-
timeoutMs: positiveContextWindow(session.compaction?.timeoutMs) || 30_000,
|
|
454
|
-
tailTurns: positiveContextWindow(session.compaction?.tailTurns) || 2,
|
|
455
|
-
keepTokens: positiveContextWindow(session.compaction?.keepTokens ?? session.compaction?.keep?.tokens),
|
|
456
|
-
preserveRecentTokens: positiveContextWindow(session.compaction?.preserveRecentTokens),
|
|
457
|
-
force: true,
|
|
458
|
-
},
|
|
459
|
-
);
|
|
460
|
-
if (Array.isArray(semanticCompactResult?.messages)) {
|
|
461
|
-
compacted = semanticCompactResult.messages;
|
|
462
|
-
addCompactUsageToSession(session, semanticCompactResult.usage);
|
|
463
|
-
}
|
|
464
|
-
} catch (err) {
|
|
465
|
-
semanticCompactError = err;
|
|
466
|
-
compactError = err;
|
|
467
|
-
try {
|
|
468
|
-
process.stderr.write(`[session] semantic ${mode} compact failed (sess=${session.id || 'unknown'}): ${err?.message || err}\n`);
|
|
469
|
-
} catch { /* best-effort */ }
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
if (!compacted && !compactError) {
|
|
473
|
-
compactError = new Error(`${compactType} compact produced no messages`);
|
|
474
|
-
}
|
|
475
|
-
// Anchor-independent prune safety net (mirror loop.mjs compact catch): when a
|
|
476
|
-
// non-recall (semantic) compact failed, try one non-LLM prune that needs no
|
|
477
|
-
// user anchor before recording failure, so Lead manual / auto-clear paths
|
|
478
|
-
// recover the same transcripts the loop path does. Gated off the recall
|
|
479
|
-
// path — a recall failure keeps its original contract (no silent prune).
|
|
480
|
-
if (!compacted && !recallFastTrackError) {
|
|
481
|
-
try {
|
|
482
|
-
const acceptThreshold = compactEffectiveBudget(budget, { reserveTokens });
|
|
483
|
-
const salvaged = pruneToolOutputsUnanchored(messages, budget, { reserveTokens });
|
|
484
|
-
// pruneToolOutputsUnanchored ALWAYS returns a fresh reconciled array
|
|
485
|
-
// (never the input identity), so `salvaged !== messages` is always
|
|
486
|
-
// true and cannot detect a no-op. Compare by element identity so a
|
|
487
|
-
// transcript that already fit (nothing pruned) is NOT falsely accepted
|
|
488
|
-
// as a recovery — that would clear compactError and unconditionally
|
|
489
|
-
// invalidate providerState for an unchanged transcript.
|
|
490
|
-
if (Array.isArray(salvaged)
|
|
491
|
-
&& messagesChanged(messages, salvaged)
|
|
492
|
-
&& estimateMessagesTokens(salvaged) <= acceptThreshold) {
|
|
493
|
-
compacted = salvaged;
|
|
494
|
-
compactError = null;
|
|
495
|
-
try {
|
|
496
|
-
process.stderr.write(`[session] compact fallback prune recovered (sess=${session.id || 'unknown'}, mode=${mode})\n`);
|
|
497
|
-
} catch { /* best-effort */ }
|
|
498
|
-
}
|
|
499
|
-
} catch { /* fall through to failure record */ }
|
|
500
|
-
}
|
|
501
|
-
if (!compacted) {
|
|
502
|
-
const now = Date.now();
|
|
503
|
-
session.compaction = {
|
|
504
|
-
...(session.compaction || {}),
|
|
505
|
-
auto: mode === 'auto' ? true : session.compaction?.auto !== false,
|
|
506
|
-
boundaryTokens: boundary,
|
|
507
|
-
triggerTokens,
|
|
508
|
-
bufferTokens,
|
|
509
|
-
bufferRatio,
|
|
510
|
-
reserveTokens,
|
|
511
|
-
lastStage: mode === 'auto' ? 'post_turn_failed' : 'manual_failed',
|
|
512
|
-
lastBeforeTokens: beforeTokens,
|
|
513
|
-
lastAfterTokens: beforeTokens,
|
|
514
|
-
lastBeforeMessageTokens: beforeMessageTokens,
|
|
515
|
-
lastAfterMessageTokens: beforeMessageTokens,
|
|
516
|
-
lastPressureTokens: pressureTokens,
|
|
517
|
-
lastCheckedAt: now,
|
|
518
|
-
lastChanged: false,
|
|
519
|
-
type: compactType,
|
|
520
|
-
compactType,
|
|
521
|
-
lastCompactType: compactType,
|
|
522
|
-
lastSemantic: false,
|
|
523
|
-
lastSemanticError: semanticCompactError?.message || null,
|
|
524
|
-
lastRecallFastTrack: false,
|
|
525
|
-
lastRecallFastTrackError: recallFastTrackError?.message || null,
|
|
526
|
-
lastError: compactError?.message || semanticCompactError?.message || recallFastTrackError?.message || String(compactError || semanticCompactError || recallFastTrackError || 'compact failed'),
|
|
527
|
-
};
|
|
528
|
-
return {
|
|
529
|
-
changed: false,
|
|
530
|
-
error: session.compaction.lastError,
|
|
531
|
-
compactType,
|
|
532
|
-
beforeMessages: messages.length,
|
|
533
|
-
afterMessages: messages.length,
|
|
534
|
-
beforeTokens,
|
|
535
|
-
afterTokens: beforeTokens,
|
|
536
|
-
beforeMessageTokens,
|
|
537
|
-
afterMessageTokens: beforeMessageTokens,
|
|
538
|
-
pressureTokens,
|
|
539
|
-
triggerTokens,
|
|
540
|
-
bufferTokens,
|
|
541
|
-
bufferRatio,
|
|
542
|
-
boundaryTokens: boundary,
|
|
543
|
-
budgetTokens: boundary,
|
|
544
|
-
targetBudgetTokens: budget,
|
|
545
|
-
reserveTokens,
|
|
546
|
-
semanticCompact: false,
|
|
547
|
-
semanticError: semanticCompactError?.message || null,
|
|
548
|
-
recallFastTrack: false,
|
|
549
|
-
recallFastTrackError: recallFastTrackError?.message || null,
|
|
550
|
-
};
|
|
551
|
-
}
|
|
552
|
-
let beforeEncoded = '';
|
|
553
|
-
let afterEncoded = '';
|
|
554
|
-
try { beforeEncoded = JSON.stringify(messages); } catch { beforeEncoded = ''; }
|
|
555
|
-
try { afterEncoded = JSON.stringify(compacted); } catch { afterEncoded = ''; }
|
|
556
|
-
const afterMessageTokens = estimateMessagesTokens(compacted);
|
|
557
|
-
const afterTokens = afterMessageTokens + reserveTokens;
|
|
558
|
-
const changed = beforeEncoded && afterEncoded
|
|
559
|
-
? beforeEncoded !== afterEncoded
|
|
560
|
-
: (compacted.length !== messages.length || afterMessageTokens !== beforeMessageTokens);
|
|
561
|
-
const unchangedReason = changed ? null : (force ? 'nothing to compact' : 'below threshold');
|
|
562
|
-
const now = Date.now();
|
|
563
|
-
session.messages = compacted;
|
|
564
|
-
session.providerState = undefined;
|
|
565
|
-
session.compaction = {
|
|
566
|
-
...(session.compaction || {}),
|
|
567
|
-
auto: mode === 'auto' ? true : session.compaction?.auto !== false,
|
|
568
|
-
boundaryTokens: boundary,
|
|
569
|
-
triggerTokens,
|
|
570
|
-
bufferTokens,
|
|
571
|
-
bufferRatio,
|
|
572
|
-
reserveTokens,
|
|
573
|
-
type: compactType,
|
|
574
|
-
compactType,
|
|
575
|
-
lastCompactType: compactType,
|
|
576
|
-
lastStage: mode === 'auto' ? 'post_turn' : 'manual',
|
|
577
|
-
lastBeforeTokens: beforeTokens,
|
|
578
|
-
lastAfterTokens: afterTokens,
|
|
579
|
-
lastBeforeMessageTokens: beforeMessageTokens,
|
|
580
|
-
lastAfterMessageTokens: afterMessageTokens,
|
|
581
|
-
lastPressureTokens: pressureTokens,
|
|
582
|
-
lastCheckedAt: now,
|
|
583
|
-
lastChanged: changed,
|
|
584
|
-
lastChangedAt: changed ? now : session.compaction?.lastChangedAt || null,
|
|
585
|
-
lastCompactAt: changed ? now : session.compaction?.lastCompactAt || null,
|
|
586
|
-
lastSemantic: semanticCompactResult?.semantic === true,
|
|
587
|
-
lastSemanticError: semanticCompactError?.message || null,
|
|
588
|
-
lastRecallFastTrack: recallFastTrackResult?.recallFastTrack === true,
|
|
589
|
-
lastRecallFastTrackError: recallFastTrackError?.message || null,
|
|
590
|
-
lastRecallFastTrackQuerySha: recallFastTrackResult?.query ? createHash('sha256').update(recallFastTrackResult.query).digest('hex').slice(0, 16) : null,
|
|
591
|
-
lastSemanticUsage: semanticCompactResult?.usage ? {
|
|
592
|
-
inputTokens: semanticCompactResult.usage.inputTokens || 0,
|
|
593
|
-
outputTokens: semanticCompactResult.usage.outputTokens || 0,
|
|
594
|
-
cachedTokens: semanticCompactResult.usage.cachedTokens || 0,
|
|
595
|
-
cacheWriteTokens: semanticCompactResult.usage.cacheWriteTokens || 0,
|
|
596
|
-
} : null,
|
|
597
|
-
compactCount: (session.compaction?.compactCount || 0) + (changed ? 1 : 0),
|
|
598
|
-
};
|
|
599
|
-
if (changed && mode === 'auto') session.lastContextTokensStaleAfterCompact = true;
|
|
600
|
-
return {
|
|
601
|
-
changed,
|
|
602
|
-
reason: unchangedReason,
|
|
603
|
-
compactType,
|
|
604
|
-
beforeMessages: messages.length,
|
|
605
|
-
afterMessages: compacted.length,
|
|
606
|
-
beforeTokens,
|
|
607
|
-
afterTokens,
|
|
608
|
-
beforeMessageTokens,
|
|
609
|
-
afterMessageTokens,
|
|
610
|
-
pressureTokens,
|
|
611
|
-
triggerTokens,
|
|
612
|
-
bufferTokens,
|
|
613
|
-
bufferRatio,
|
|
614
|
-
boundaryTokens: boundary,
|
|
615
|
-
budgetTokens: boundary,
|
|
616
|
-
targetBudgetTokens: budget,
|
|
617
|
-
reserveTokens,
|
|
618
|
-
semanticCompact: semanticCompactResult?.semantic === true,
|
|
619
|
-
semanticError: semanticCompactError?.message || null,
|
|
620
|
-
recallFastTrack: recallFastTrackResult?.recallFastTrack === true,
|
|
621
|
-
recallFastTrackError: recallFastTrackError?.message || null,
|
|
622
|
-
usage: semanticCompactResult?.usage || null,
|
|
623
|
-
};
|
|
624
|
-
}
|
|
625
259
|
// Provider-scoped unified cache key. Goal: all orchestrator-internal
|
|
626
260
|
// dispatches (agent/maintenance/mcp/scheduler/webhook) targeting the
|
|
627
261
|
// same provider land in a single server-side cache shard, so the
|
|
@@ -965,6 +599,34 @@ export function createSession(opts) {
|
|
|
965
599
|
if (!provider)
|
|
966
600
|
throw new Error(`Provider "${providerName}" not found or not enabled`);
|
|
967
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
|
+
}
|
|
968
630
|
const messages = [];
|
|
969
631
|
const ownerIsAgent = isAgentOwner(opts.owner);
|
|
970
632
|
const resolvedAgent = opts.agent || opts.role || profile?.taskType || null;
|
|
@@ -1204,69 +866,11 @@ export function createSession(opts) {
|
|
|
1204
866
|
return session;
|
|
1205
867
|
}
|
|
1206
868
|
|
|
1207
|
-
//
|
|
1208
|
-
//
|
|
1209
|
-
//
|
|
1210
|
-
//
|
|
1211
|
-
//
|
|
1212
|
-
// stage, lastStreamDeltaAt, lastToolCall, lastError, updatedAt,
|
|
1213
|
-
// controller?: AbortController, // set while an ask is in flight
|
|
1214
|
-
// generation?: number, // snapshot taken at ask start
|
|
1215
|
-
// closed?: boolean, // flipped by closeSession()
|
|
1216
|
-
// }
|
|
1217
|
-
const _runtimeState = new Map();
|
|
1218
|
-
const _toolActivityHeartbeats = new Map();
|
|
1219
|
-
const VALID_STAGES = new Set([
|
|
1220
|
-
'connecting', 'requesting', 'streaming', 'tool_running', 'idle', 'error', 'done', 'cancelling',
|
|
1221
|
-
]);
|
|
1222
|
-
function _touchRuntime(id) {
|
|
1223
|
-
let entry = _runtimeState.get(id);
|
|
1224
|
-
if (!entry) {
|
|
1225
|
-
entry = { stage: 'idle', lastStreamDeltaAt: null, lastToolCall: null, lastError: null, updatedAt: Date.now() };
|
|
1226
|
-
_runtimeState.set(id, entry);
|
|
1227
|
-
}
|
|
1228
|
-
return entry;
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
function _stopToolActivityHeartbeat(id) {
|
|
1232
|
-
if (!id) return;
|
|
1233
|
-
const timer = _toolActivityHeartbeats.get(id);
|
|
1234
|
-
if (!timer) return;
|
|
1235
|
-
try { clearInterval(timer); } catch { /* ignore */ }
|
|
1236
|
-
_toolActivityHeartbeats.delete(id);
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
function _touchSessionActivityProgress(id) {
|
|
1240
|
-
const entry = _runtimeState.get(id);
|
|
1241
|
-
if (!entry || entry.closed || entry.controller?.signal?.aborted) return;
|
|
1242
|
-
if (entry.stage !== 'tool_running') return;
|
|
1243
|
-
const now = Date.now();
|
|
1244
|
-
entry.lastProgressAt = now;
|
|
1245
|
-
entry.updatedAt = now;
|
|
1246
|
-
publishHeartbeat(id, now);
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
function _startToolActivityHeartbeat(id) {
|
|
1250
|
-
_stopToolActivityHeartbeat(id);
|
|
1251
|
-
if (!(DEFAULT_ACTIVITY_HEARTBEAT_MS > 0)) return;
|
|
1252
|
-
const timer = setInterval(() => _touchSessionActivityProgress(id), DEFAULT_ACTIVITY_HEARTBEAT_MS);
|
|
1253
|
-
if (typeof timer.unref === 'function') timer.unref();
|
|
1254
|
-
_toolActivityHeartbeats.set(id, timer);
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
export function updateSessionStage(id, stage) {
|
|
1258
|
-
if (!id || !VALID_STAGES.has(stage)) return;
|
|
1259
|
-
const entry = _touchRuntime(id);
|
|
1260
|
-
const now = Date.now();
|
|
1261
|
-
entry.stage = stage;
|
|
1262
|
-
if (stage === 'connecting' || stage === 'requesting') {
|
|
1263
|
-
entry.modelRequestStartedAt = now;
|
|
1264
|
-
}
|
|
1265
|
-
entry.lastProgressAt = now;
|
|
1266
|
-
entry.updatedAt = now;
|
|
1267
|
-
if (stage !== 'tool_running') _stopToolActivityHeartbeat(id);
|
|
1268
|
-
}
|
|
1269
|
-
|
|
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).
|
|
1270
874
|
export function updateSessionRoute(id, route = {}) {
|
|
1271
875
|
if (!id) return null;
|
|
1272
876
|
const session = loadSession(id);
|
|
@@ -1322,475 +926,15 @@ export function updateSessionRoute(id, route = {}) {
|
|
|
1322
926
|
return session;
|
|
1323
927
|
}
|
|
1324
928
|
|
|
1325
|
-
/**
|
|
1326
|
-
* Reset heartbeat-visible fields for a new ask. Preserves controller/generation/
|
|
1327
|
-
* closed (lifecycle) but clears the previous run's streaming state so stale
|
|
1328
|
-
* lastToolCall / lastStreamDeltaAt from the previous ask don't leak into the
|
|
1329
|
-
* new one.
|
|
1330
|
-
*/
|
|
1331
|
-
export function markSessionAskStart(id) {
|
|
1332
|
-
if (!id) return;
|
|
1333
|
-
_stopToolActivityHeartbeat(id);
|
|
1334
|
-
const entry = _touchRuntime(id);
|
|
1335
|
-
entry.usageMetricsTurnIncremental = false;
|
|
1336
|
-
const sessionForTurn = entry.session ?? loadSession(id);
|
|
1337
|
-
if (sessionForTurn) bumpUsageMetricsTurnId(sessionForTurn);
|
|
1338
|
-
entry.stage = 'connecting';
|
|
1339
|
-
entry.lastStreamDeltaAt = null;
|
|
1340
|
-
entry.lastToolCall = null;
|
|
1341
|
-
entry.toolStartedAt = null;
|
|
1342
|
-
entry.lastError = null;
|
|
1343
|
-
// A new ask starts a fresh turn lifecycle — clear any stale empty-final
|
|
1344
|
-
// classification from the prior turn so inspectBridgeEntry doesn't keep
|
|
1345
|
-
// short-circuiting to 'empty-synthesis' (which would disable stall
|
|
1346
|
-
// detection for the entire new turn).
|
|
1347
|
-
entry.emptyFinal = false;
|
|
1348
|
-
entry.emptyFinalAt = null;
|
|
1349
|
-
// askStartedAt is the watchdog's fallback reference when a session
|
|
1350
|
-
// hangs before any stream delta arrives. Without it, a provider that
|
|
1351
|
-
// never returns a first token would stall forever because the watchdog
|
|
1352
|
-
// keys solely on lastStreamDeltaAt.
|
|
1353
|
-
const now = Date.now();
|
|
1354
|
-
entry.askStartedAt = now;
|
|
1355
|
-
entry.modelRequestStartedAt = now;
|
|
1356
|
-
entry.lastProgressAt = now;
|
|
1357
|
-
entry.updatedAt = now;
|
|
1358
|
-
// Publish heartbeat immediately so the status aggregator picks the
|
|
1359
|
-
// session up in the connecting / requesting window. Without this the
|
|
1360
|
-
// .hb file only landed on the first stream chunk — producing a 3–10s
|
|
1361
|
-
// (xhigh: 30s+) invisible gap where agent sessions ran but the CC
|
|
1362
|
-
// statusline showed no maintenance/agent badge. STREAM_FRESH_MS (5 min)
|
|
1363
|
-
// still drops a session whose provider truly never returns a chunk;
|
|
1364
|
-
// markSessionStreamDelta keeps refreshing once chunks arrive.
|
|
1365
|
-
publishHeartbeat(id, now);
|
|
1366
|
-
}
|
|
1367
|
-
export async function markSessionStreamDelta(id) {
|
|
1368
|
-
if (!id) return;
|
|
1369
|
-
// Non-creating lookup: a live ask ALWAYS has a runtime entry (markSessionAskStart
|
|
1370
|
-
// creates it before streaming begins). _touchRuntime would instead resurrect a
|
|
1371
|
-
// blank entry — and closeSession()/idle-sweep clear _runtimeState on a deferred
|
|
1372
|
-
// tick while a detached provider stream may still be trickling deltas. A delta
|
|
1373
|
-
// arriving after that clear must NOT re-create an entry or it would republish the
|
|
1374
|
-
// .hb heartbeat that markSessionClosed deleted, orphaning a dead session's
|
|
1375
|
-
// heartbeat indefinitely (the disk tombstone blocks ask resumption but not this
|
|
1376
|
-
// path). Skip a missing, tombstoned, or aborted entry — never refresh liveness.
|
|
1377
|
-
const entry = _runtimeState.get(id);
|
|
1378
|
-
if (!entry || entry.closed || entry.controller?.signal?.aborted) return;
|
|
1379
|
-
_stopToolActivityHeartbeat(id);
|
|
1380
|
-
const now = Date.now();
|
|
1381
|
-
entry.lastStreamDeltaAt = now;
|
|
1382
|
-
entry.lastProgressAt = now;
|
|
1383
|
-
// Only promote to 'streaming' if we were in a pre-stream stage; never downgrade
|
|
1384
|
-
// mid-tool (tool_running has its own delta source if the tool streams back).
|
|
1385
|
-
if (entry.stage === 'connecting' || entry.stage === 'requesting') {
|
|
1386
|
-
entry.stage = 'streaming';
|
|
1387
|
-
}
|
|
1388
|
-
// Lightweight heartbeat (≤5s self-throttled) for the status aggregator.
|
|
1389
|
-
// Disk-side session.lastHeartbeatAt below is the heavy 60s zombie-reaper
|
|
1390
|
-
// signal; the .hb file is the fast fresh-session signal consumed by the
|
|
1391
|
-
// status line.
|
|
1392
|
-
publishHeartbeat(id, now);
|
|
1393
|
-
const session = entry.session;
|
|
1394
|
-
if (session && now - (session.lastHeartbeatAt || 0) > HEARTBEAT_THROTTLE_MS) {
|
|
1395
|
-
session.lastHeartbeatAt = now;
|
|
1396
|
-
await saveSessionAsync(session, { expectedGeneration: session.generation });
|
|
1397
|
-
}
|
|
1398
|
-
entry.updatedAt = now;
|
|
1399
|
-
}
|
|
1400
|
-
export function markSessionToolCall(id, toolName) {
|
|
1401
|
-
if (!id) return;
|
|
1402
|
-
const entry = _touchRuntime(id);
|
|
1403
|
-
entry.stage = 'tool_running';
|
|
1404
|
-
entry.lastToolCall = toolName || null;
|
|
1405
|
-
entry.toolStartedAt = Date.now();
|
|
1406
|
-
entry.lastProgressAt = entry.toolStartedAt;
|
|
1407
|
-
entry.updatedAt = entry.toolStartedAt;
|
|
1408
|
-
publishHeartbeat(id, entry.toolStartedAt);
|
|
1409
|
-
_startToolActivityHeartbeat(id);
|
|
1410
|
-
}
|
|
1411
|
-
// Parent AbortSignal listeners are dropped on askSession unwind (finally /
|
|
1412
|
-
// terminal return) and on error/cancel/close — not in markSessionDone, which
|
|
1413
|
-
// also runs between queued follow-up turns within one ask.
|
|
1414
|
-
export function markSessionDone(id, { empty = false } = {}) {
|
|
1415
|
-
if (!id) return;
|
|
1416
|
-
_stopToolActivityHeartbeat(id);
|
|
1417
|
-
const entry = _touchRuntime(id);
|
|
1418
|
-
entry.stage = 'done';
|
|
1419
|
-
entry.lastError = null;
|
|
1420
|
-
entry.askStartedAt = null;
|
|
1421
|
-
entry.toolStartedAt = null;
|
|
1422
|
-
// Non-empty completion: drop any stale empty-final flag so a subsequent
|
|
1423
|
-
// ask on the same reusable runtime entry starts clean. Empty-final
|
|
1424
|
-
// completions preserve the flag (set by markSessionEmptyFinal just prior).
|
|
1425
|
-
if (!empty) {
|
|
1426
|
-
entry.emptyFinal = false;
|
|
1427
|
-
entry.emptyFinalAt = null;
|
|
1428
|
-
}
|
|
1429
|
-
const doneTs = Date.now();
|
|
1430
|
-
entry.doneAt = doneTs;
|
|
1431
|
-
entry.lastProgressAt = doneTs;
|
|
1432
|
-
entry.updatedAt = doneTs;
|
|
1433
|
-
// Terminal stage — drop the heartbeat so the status badge releases
|
|
1434
|
-
// immediately. A subsequent ask on the same session re-publishes via
|
|
1435
|
-
// markSessionStreamDelta on the first chunk.
|
|
1436
|
-
deleteHeartbeat(id);
|
|
1437
|
-
}
|
|
1438
|
-
// Tag a session as having completed with empty final synthesis (no
|
|
1439
|
-
// content/reasoning). Distinct from `markSessionDone`: still a success
|
|
1440
|
-
// (no abort), but the stall watchdog and post-mortem tools can
|
|
1441
|
-
// distinguish "finished empty" from "finished with content" without
|
|
1442
|
-
// mistaking the silence for a stall.
|
|
1443
|
-
export function markSessionEmptyFinal(id) {
|
|
1444
|
-
if (!id) return;
|
|
1445
|
-
const entry = _touchRuntime(id);
|
|
1446
|
-
entry.emptyFinal = true;
|
|
1447
|
-
entry.emptyFinalAt = Date.now();
|
|
1448
|
-
}
|
|
1449
|
-
export function markSessionError(id, msg) {
|
|
1450
|
-
if (!id) return;
|
|
1451
|
-
_stopToolActivityHeartbeat(id);
|
|
1452
|
-
const entry = _touchRuntime(id);
|
|
1453
|
-
entry.stage = 'error';
|
|
1454
|
-
entry.lastError = msg ? String(msg).slice(0, 200) : null;
|
|
1455
|
-
entry.askStartedAt = null;
|
|
1456
|
-
entry.toolStartedAt = null;
|
|
1457
|
-
// Error path is a non-empty completion (we have an error message, not a
|
|
1458
|
-
// silent empty final). Clear the flag so the next ask starts clean.
|
|
1459
|
-
entry.emptyFinal = false;
|
|
1460
|
-
entry.emptyFinalAt = null;
|
|
1461
|
-
const errTs = Date.now();
|
|
1462
|
-
entry.doneAt = errTs;
|
|
1463
|
-
entry.lastProgressAt = errTs;
|
|
1464
|
-
entry.updatedAt = errTs;
|
|
1465
|
-
deleteHeartbeat(id);
|
|
1466
|
-
_unlinkParentAbortListener(entry);
|
|
1467
|
-
}
|
|
1468
|
-
export function markSessionCancelled(id) {
|
|
1469
|
-
if (!id) return;
|
|
1470
|
-
_stopToolActivityHeartbeat(id);
|
|
1471
|
-
const entry = _touchRuntime(id);
|
|
1472
|
-
entry.stage = 'done';
|
|
1473
|
-
entry.lastError = null;
|
|
1474
|
-
entry.askStartedAt = null;
|
|
1475
|
-
entry.toolStartedAt = null;
|
|
1476
|
-
entry.emptyFinal = false;
|
|
1477
|
-
entry.emptyFinalAt = null;
|
|
1478
|
-
const doneTs = Date.now();
|
|
1479
|
-
entry.doneAt = doneTs;
|
|
1480
|
-
entry.lastProgressAt = doneTs;
|
|
1481
|
-
entry.updatedAt = doneTs;
|
|
1482
|
-
deleteHeartbeat(id);
|
|
1483
|
-
_unlinkParentAbortListener(entry);
|
|
1484
|
-
}
|
|
1485
|
-
export function getSessionRuntime(id) {
|
|
1486
|
-
return id ? (_runtimeState.get(id) || null) : null;
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
const _COMPACTION_BLOCKED_STAGES = new Set([
|
|
1490
|
-
'connecting', 'requesting', 'streaming', 'tool_running', 'cancelling',
|
|
1491
|
-
]);
|
|
1492
|
-
|
|
1493
|
-
export function isSessionCompactionBlocked(sessionId) {
|
|
1494
|
-
if (!sessionId) return false;
|
|
1495
|
-
const entry = _runtimeState.get(sessionId);
|
|
1496
|
-
if (!entry || entry.closed === true) return false;
|
|
1497
|
-
if (entry.controller && !entry.controller.signal?.aborted) return true;
|
|
1498
|
-
return _COMPACTION_BLOCKED_STAGES.has(entry.stage);
|
|
1499
|
-
}
|
|
1500
|
-
|
|
1501
|
-
export function getSessionProgressSnapshot(sessionId) {
|
|
1502
|
-
const entry = _runtimeState.get(sessionId);
|
|
1503
|
-
if (!entry) return null;
|
|
1504
|
-
const askStartedAt = entry.askStartedAt || 0;
|
|
1505
|
-
const modelRequestStartedAt = entry.modelRequestStartedAt || askStartedAt;
|
|
1506
|
-
const firstActivityAt = Math.max(
|
|
1507
|
-
entry.lastStreamDeltaAt || 0,
|
|
1508
|
-
entry.toolStartedAt || 0,
|
|
1509
|
-
);
|
|
1510
|
-
const stage = entry.stage || 'idle';
|
|
1511
|
-
const waitingForFirstActivity = Boolean(
|
|
1512
|
-
modelRequestStartedAt
|
|
1513
|
-
&& (stage === 'connecting' || stage === 'requesting')
|
|
1514
|
-
&& firstActivityAt <= modelRequestStartedAt
|
|
1515
|
-
);
|
|
1516
|
-
return {
|
|
1517
|
-
stage,
|
|
1518
|
-
askStartedAt,
|
|
1519
|
-
modelRequestStartedAt,
|
|
1520
|
-
firstActivityAt,
|
|
1521
|
-
lastStreamDeltaAt: entry.lastStreamDeltaAt || 0,
|
|
1522
|
-
toolStartedAt: entry.toolStartedAt || 0,
|
|
1523
|
-
lastProgressAt: entry.lastProgressAt || 0,
|
|
1524
|
-
updatedAt: entry.updatedAt || 0,
|
|
1525
|
-
hasFirstActivity: Boolean(firstActivityAt && (!askStartedAt || firstActivityAt >= askStartedAt)),
|
|
1526
|
-
waitingForFirstActivity,
|
|
1527
|
-
};
|
|
1528
|
-
}
|
|
1529
|
-
|
|
1530
|
-
/**
|
|
1531
|
-
* Iterate all active session runtimes. Used by the stream watchdog.
|
|
1532
|
-
* Returns an iterable of [sessionId, entry] pairs; consumers should
|
|
1533
|
-
* treat entries as read-only snapshots and avoid mutating them.
|
|
1534
|
-
*/
|
|
1535
|
-
export function forEachSessionRuntime() {
|
|
1536
|
-
return _runtimeState.entries();
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
929
|
// --- Incremental metric persistence (fix A) ---
|
|
1540
|
-
//
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
return next;
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
export function resolveUsageMetricsTurnId(session, delta = {}) {
|
|
1552
|
-
if (delta.usageMetricsTurnId != null && Number.isFinite(Number(delta.usageMetricsTurnId))) {
|
|
1553
|
-
return Number(delta.usageMetricsTurnId);
|
|
1554
|
-
}
|
|
1555
|
-
return Number(session?.usageMetricsTurnId) || 0;
|
|
1556
|
-
}
|
|
1557
|
-
|
|
1558
|
-
/** Advance loop metrics epoch when agentLoop resets its iteration counter (post-compact). */
|
|
1559
|
-
export function bumpUsageMetricsEpoch(session) {
|
|
1560
|
-
if (!session || typeof session !== 'object') return 0;
|
|
1561
|
-
const next = (Number(session.usageMetricsEpoch) || 0) + 1;
|
|
1562
|
-
session.usageMetricsEpoch = next;
|
|
1563
|
-
return next;
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
/**
|
|
1567
|
-
* Resolve usage-metrics epoch for idempotency (exported for regression smoke).
|
|
1568
|
-
* Prefers session.usageMetricsEpoch (bumped in loop on compact reset) and optional
|
|
1569
|
-
* delta.usageMetricsEpoch; falls back to iteration regression when loop did not bump.
|
|
1570
|
-
*/
|
|
1571
|
-
export function resolveUsageMetricsEpoch(session, delta = {}) {
|
|
1572
|
-
if (!session) return 0;
|
|
1573
|
-
let epoch = Number(session.usageMetricsEpoch) || 0;
|
|
1574
|
-
if (delta.usageMetricsEpoch != null && Number.isFinite(Number(delta.usageMetricsEpoch))) {
|
|
1575
|
-
epoch = Math.max(epoch, Number(delta.usageMetricsEpoch));
|
|
1576
|
-
}
|
|
1577
|
-
const idx = Number(delta.iterationIndex);
|
|
1578
|
-
const prevLastIdx = typeof session.lastIterationIndex === 'number'
|
|
1579
|
-
? session.lastIterationIndex
|
|
1580
|
-
: null;
|
|
1581
|
-
if (
|
|
1582
|
-
(delta.usageMetricsEpoch == null || !Number.isFinite(Number(delta.usageMetricsEpoch)))
|
|
1583
|
-
&& prevLastIdx !== null
|
|
1584
|
-
&& Number.isFinite(idx)
|
|
1585
|
-
&& idx < prevLastIdx
|
|
1586
|
-
) {
|
|
1587
|
-
epoch += 1;
|
|
1588
|
-
}
|
|
1589
|
-
return epoch;
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
|
-
export function usageMetricsSourceKey(delta = {}) {
|
|
1593
|
-
const raw = delta.source ?? delta.usageSource;
|
|
1594
|
-
if (raw == null || raw === '') return 'provider_send';
|
|
1595
|
-
return String(raw);
|
|
1596
|
-
}
|
|
1597
|
-
|
|
1598
|
-
/** Idempotency key for incremental usage persistence (exported for regression smoke). */
|
|
1599
|
-
export function usageMetricsIdempotencyKey(sessionId, session, delta = {}) {
|
|
1600
|
-
const turnId = resolveUsageMetricsTurnId(session, delta);
|
|
1601
|
-
const epoch = resolveUsageMetricsEpoch(session, delta);
|
|
1602
|
-
const source = usageMetricsSourceKey(delta);
|
|
1603
|
-
return `${sessionId}:${turnId}:${epoch}:${delta.iterationIndex}:${source}`;
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
function uncachedInputTokensForProvider(provider, inputTokens, cachedReadTokens = 0, cacheWriteTokens = 0) {
|
|
1607
|
-
const input = Number(inputTokens) || 0;
|
|
1608
|
-
if (input <= 0) return 0;
|
|
1609
|
-
// Anthropic-style providers report input_tokens excluding cache reads; OpenAI
|
|
1610
|
-
// Responses/Gemini-style providers report input_tokens inclusive of cached
|
|
1611
|
-
// prefix tokens. Keep both views so UI can show the real context footprint
|
|
1612
|
-
// and the fresh/new token portion without mistaking cache hits for a cache
|
|
1613
|
-
// break.
|
|
1614
|
-
if (providerInputExcludesCache(provider)) return input + (Number(cacheWriteTokens) || 0);
|
|
1615
|
-
return Math.max(input - (Number(cachedReadTokens) || 0) - (Number(cacheWriteTokens) || 0), 0);
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1618
|
-
/**
|
|
1619
|
-
* Apply terminal ask usage to session totals. Skips lifetime totals when incremental
|
|
1620
|
-
* per-iteration persistence already counted this turn (askSession path).
|
|
1621
|
-
*/
|
|
1622
|
-
export function applyAskTerminalUsageTotals(session, result, options = {}) {
|
|
1623
|
-
if (!session || !result?.usage) return;
|
|
1624
|
-
const skipTotals = options.skipTotalsIfIncremental === true;
|
|
1625
|
-
if (!skipTotals) {
|
|
1626
|
-
const inputTokens = result.usage.inputTokens || 0;
|
|
1627
|
-
const outputTokens = result.usage.outputTokens || 0;
|
|
1628
|
-
const cachedTokens = result.usage.cachedTokens || 0;
|
|
1629
|
-
const cacheWriteTokens = result.usage.cacheWriteTokens || 0;
|
|
1630
|
-
const uncachedInputTokens = uncachedInputTokensForProvider(session.provider, inputTokens, cachedTokens, cacheWriteTokens);
|
|
1631
|
-
session.totalInputTokens = (session.totalInputTokens || 0) + inputTokens;
|
|
1632
|
-
session.totalOutputTokens = (session.totalOutputTokens || 0) + outputTokens;
|
|
1633
|
-
session.tokensCumulative = (session.tokensCumulative || 0)
|
|
1634
|
-
+ inputTokens
|
|
1635
|
-
+ outputTokens;
|
|
1636
|
-
session.totalCachedReadTokens = (session.totalCachedReadTokens || 0) + cachedTokens;
|
|
1637
|
-
session.totalCacheWriteTokens = (session.totalCacheWriteTokens || 0) + cacheWriteTokens;
|
|
1638
|
-
session.totalUncachedInputTokens = (session.totalUncachedInputTokens || 0) + uncachedInputTokens;
|
|
1639
|
-
}
|
|
1640
|
-
const _lastTurn = result.lastTurnUsage || result.usage || {};
|
|
1641
|
-
const _lastInputTokens = _lastTurn.inputTokens || 0;
|
|
1642
|
-
const _lastCachedReadTokens = _lastTurn.cachedTokens || 0;
|
|
1643
|
-
const _lastCacheWriteTokens = _lastTurn.cacheWriteTokens || 0;
|
|
1644
|
-
session.lastInputTokens = _lastInputTokens;
|
|
1645
|
-
session.lastOutputTokens = _lastTurn.outputTokens || 0;
|
|
1646
|
-
session.lastCachedReadTokens = _lastCachedReadTokens;
|
|
1647
|
-
session.lastCacheWriteTokens = _lastCacheWriteTokens;
|
|
1648
|
-
session.lastUncachedInputTokens = uncachedInputTokensForProvider(
|
|
1649
|
-
session.provider,
|
|
1650
|
-
_lastInputTokens,
|
|
1651
|
-
_lastCachedReadTokens,
|
|
1652
|
-
_lastCacheWriteTokens,
|
|
1653
|
-
);
|
|
1654
|
-
const _inputExcludesCache = providerInputExcludesCache(session.provider);
|
|
1655
|
-
session.lastContextTokens = _inputExcludesCache
|
|
1656
|
-
? _lastInputTokens + _lastCachedReadTokens + _lastCacheWriteTokens
|
|
1657
|
-
: _lastInputTokens;
|
|
1658
|
-
session.lastContextTokensUpdatedAt = Date.now();
|
|
1659
|
-
session.lastContextTokensStaleAfterCompact = false;
|
|
1660
|
-
}
|
|
1661
|
-
|
|
1662
|
-
/**
|
|
1663
|
-
* Persist incremental usage delta immediately after each provider.send iteration.
|
|
1664
|
-
* Idempotency key `sessionId:turnId:epoch:iterationIndex:source` scopes retries
|
|
1665
|
-
* per ask, compaction epoch, iteration, and usage source.
|
|
1666
|
-
*/
|
|
1667
|
-
export async function persistIterationMetrics(delta) {
|
|
1668
|
-
if (!delta || !delta.sessionId) return;
|
|
1669
|
-
const { sessionId, iterationIndex, deltaInput, deltaOutput, deltaCachedRead, deltaCacheWrite, ts } = delta;
|
|
1670
|
-
const runtimeEntry = _runtimeState.get(sessionId);
|
|
1671
|
-
const session = runtimeEntry?.session ?? loadSession(sessionId);
|
|
1672
|
-
if (!session || session.closed) return;
|
|
1673
|
-
const epoch = resolveUsageMetricsEpoch(session, delta);
|
|
1674
|
-
if (epoch !== (Number(session.usageMetricsEpoch) || 0)) {
|
|
1675
|
-
session.usageMetricsEpoch = epoch;
|
|
1676
|
-
}
|
|
1677
|
-
let seen = _metricSeenIter.get(sessionId);
|
|
1678
|
-
if (!seen) {
|
|
1679
|
-
seen = new Set();
|
|
1680
|
-
_metricSeenIter.set(sessionId, seen);
|
|
1681
|
-
}
|
|
1682
|
-
const ikey = usageMetricsIdempotencyKey(sessionId, session, delta);
|
|
1683
|
-
const isReplay = seen.has(ikey);
|
|
1684
|
-
seen.add(ikey);
|
|
1685
|
-
if (!isReplay) {
|
|
1686
|
-
if (runtimeEntry) runtimeEntry.usageMetricsTurnIncremental = true;
|
|
1687
|
-
const deltaUncachedInput = delta.deltaUncachedInput != null
|
|
1688
|
-
? Number(delta.deltaUncachedInput) || 0
|
|
1689
|
-
: uncachedInputTokensForProvider(session.provider, deltaInput, deltaCachedRead, deltaCacheWrite);
|
|
1690
|
-
session.totalInputTokens = (session.totalInputTokens || 0) + (deltaInput || 0);
|
|
1691
|
-
session.totalOutputTokens = (session.totalOutputTokens || 0) + (deltaOutput || 0);
|
|
1692
|
-
session.tokensCumulative = (session.tokensCumulative || 0) + (deltaInput || 0) + (deltaOutput || 0);
|
|
1693
|
-
// Cache totals — additive fields, default 0 on legacy sessions; both
|
|
1694
|
-
// are undefined-safe so the schema migrates lazily as new iterations
|
|
1695
|
-
// land. Keeps live + terminal aggregates in lock-step (loop.mjs already
|
|
1696
|
-
// includes cached_read / cache_write in its terminal usage rollup).
|
|
1697
|
-
session.totalCachedReadTokens = (session.totalCachedReadTokens || 0) + (deltaCachedRead || 0);
|
|
1698
|
-
session.totalCacheWriteTokens = (session.totalCacheWriteTokens || 0) + (deltaCacheWrite || 0);
|
|
1699
|
-
session.totalUncachedInputTokens = (session.totalUncachedInputTokens || 0) + deltaUncachedInput;
|
|
1700
|
-
// Window snapshot updated per iteration so agent type=list reflects the
|
|
1701
|
-
// most-recent provider-reported input size even for short dispatches
|
|
1702
|
-
// that finish before askSession's terminal save lands.
|
|
1703
|
-
session.lastInputTokens = deltaInput || 0;
|
|
1704
|
-
session.lastOutputTokens = deltaOutput || 0;
|
|
1705
|
-
session.lastCachedReadTokens = deltaCachedRead || 0;
|
|
1706
|
-
session.lastCacheWriteTokens = deltaCacheWrite || 0;
|
|
1707
|
-
session.lastUncachedInputTokens = deltaUncachedInput;
|
|
1708
|
-
// Normalized last-call context footprint: how many prompt tokens the
|
|
1709
|
-
// model actually saw on the most-recent send, comparable ACROSS
|
|
1710
|
-
// providers. Anthropic reports input_tokens EXCLUDING cache (cache_read
|
|
1711
|
-
// is a separate field), so the cached portion must be added back to
|
|
1712
|
-
// reflect real context size; openai/grok/gemini already fold cached
|
|
1713
|
-
// tokens INTO the input count, so input alone is the footprint.
|
|
1714
|
-
const _inputExcludesCache = providerInputExcludesCache(session.provider);
|
|
1715
|
-
session.lastContextTokens = _inputExcludesCache
|
|
1716
|
-
? (deltaInput || 0) + (deltaCachedRead || 0) + (deltaCacheWrite || 0)
|
|
1717
|
-
: (deltaInput || 0);
|
|
1718
|
-
session.lastContextTokensUpdatedAt = ts || Date.now();
|
|
1719
|
-
session.lastContextTokensStaleAfterCompact = false;
|
|
1720
|
-
}
|
|
1721
|
-
session.lastIterationIndex = iterationIndex;
|
|
1722
|
-
session.updatedAt = ts || Date.now();
|
|
1723
|
-
await saveSessionAsync(session, { expectedGeneration: session.generation });
|
|
1724
|
-
}
|
|
1725
|
-
|
|
1726
|
-
function standaloneStatusRouteInfo(session) {
|
|
1727
|
-
if (!session) return null;
|
|
1728
|
-
// autoCompactTokenLimit is an EXPLICIT sub-boundary auto-compact limit only.
|
|
1729
|
-
// Do NOT fall back to compactBoundaryTokens/contextWindow here — that
|
|
1730
|
-
// labels a derived full-window value as an explicit limit, which the
|
|
1731
|
-
// runtime would treat as the compaction trigger and collapse the buffer.
|
|
1732
|
-
// The boundary/window stays available via contextWindow/rawContextWindow.
|
|
1733
|
-
const _boundary = Number(session.compactBoundaryTokens || session.contextWindow || 0);
|
|
1734
|
-
const _limit = Number(session.autoCompactTokenLimit || 0);
|
|
1735
|
-
const explicitAutoCompactTokenLimit = _limit > 0 && (!_boundary || _limit < _boundary) ? _limit : null;
|
|
1736
|
-
return {
|
|
1737
|
-
provider: session.provider,
|
|
1738
|
-
model: session.model,
|
|
1739
|
-
modelDisplay: session.modelDisplay || session.displayName || session.model,
|
|
1740
|
-
effort: session.effort || '',
|
|
1741
|
-
fast: session.fast === true,
|
|
1742
|
-
contextWindow: session.contextWindow || null,
|
|
1743
|
-
rawContextWindow: session.rawContextWindow || session.contextWindow || null,
|
|
1744
|
-
effectiveContextWindowPercent: session.effectiveContextWindowPercent || null,
|
|
1745
|
-
autoCompactTokenLimit: explicitAutoCompactTokenLimit,
|
|
1746
|
-
presetId: session.presetId || null,
|
|
1747
|
-
presetName: session.presetName || null,
|
|
1748
|
-
};
|
|
1749
|
-
}
|
|
1750
|
-
|
|
1751
|
-
function recordStandaloneStatusTelemetry(session, result, durationMs) {
|
|
1752
|
-
if (!session || !result?.usage) return;
|
|
1753
|
-
const routeInfo = standaloneStatusRouteInfo(session);
|
|
1754
|
-
if (!routeInfo?.provider || !routeInfo?.model) return;
|
|
1755
|
-
const providerOut = {
|
|
1756
|
-
usage: result.usage,
|
|
1757
|
-
model: result.model,
|
|
1758
|
-
serviceTier: result.serviceTier,
|
|
1759
|
-
};
|
|
1760
|
-
// The transcript estimate is the SSOT for the displayed context footprint.
|
|
1761
|
-
// agentLoop()'s result has no `compact` field, so build a synthetic compact
|
|
1762
|
-
// arg carrying the live monotonic estimate (estimateMessagesTokens+reserve)
|
|
1763
|
-
// as afterTokens. This lights up summarizeGatewayUsage's estimate-based
|
|
1764
|
-
// contextUsedPct branch (provider input_tokens swing wildly / unbounded on
|
|
1765
|
-
// e.g. OpenAI gpt-5.5), and lets a genuine >100% pass through.
|
|
1766
|
-
const _estTokens = estimateTranscriptContextUsage(session.messages, session.tools || []);
|
|
1767
|
-
const _compactArg = { ...(result.compact && typeof result.compact === 'object' ? result.compact : {}), afterTokens: _estTokens };
|
|
1768
|
-
try {
|
|
1769
|
-
const summary = {
|
|
1770
|
-
...summarizeGatewayUsage(routeInfo, providerOut, _compactArg, durationMs),
|
|
1771
|
-
requestKind: 'chat',
|
|
1772
|
-
sessionId: session.id || null,
|
|
1773
|
-
toolCount: result.toolCallsTotal ?? null,
|
|
1774
|
-
messageCount: Array.isArray(session.messages) ? session.messages.length : null,
|
|
1775
|
-
cacheStrategy: session.providerCacheOpts?.cacheStrategy || null,
|
|
1776
|
-
};
|
|
1777
|
-
recordGatewayUsageEvent(summary);
|
|
1778
|
-
} catch {
|
|
1779
|
-
// Statusline telemetry must never affect the model turn.
|
|
1780
|
-
}
|
|
1781
|
-
|
|
1782
|
-
const provider = getProvider(routeInfo.provider);
|
|
1783
|
-
if (!provider) return;
|
|
1784
|
-
fetchOAuthUsageSnapshot(routeInfo, provider, (message) => {
|
|
1785
|
-
if (process.env.MIXDOG_STATUSLINE_TRACE) {
|
|
1786
|
-
process.stderr.write(`[statusline] ${message}\n`);
|
|
1787
|
-
}
|
|
1788
|
-
})
|
|
1789
|
-
.then((snapshot) => {
|
|
1790
|
-
try { buildGatewayLimits(routeInfo, providerOut, snapshot); } catch {}
|
|
1791
|
-
})
|
|
1792
|
-
.catch(() => {});
|
|
1793
|
-
}
|
|
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).
|
|
1794
938
|
|
|
1795
939
|
/** Force-flush session metrics to disk. Used by watchdog terminal-reap (fix B). */
|
|
1796
940
|
export async function flushSessionMetrics(sessionId) {
|
|
@@ -1801,91 +945,6 @@ export async function flushSessionMetrics(sessionId) {
|
|
|
1801
945
|
await saveSessionAsync(session, { expectedGeneration: session.generation });
|
|
1802
946
|
}
|
|
1803
947
|
|
|
1804
|
-
/** Mark session hidden so listSessions() filters it out (runtime-only). */
|
|
1805
|
-
export function hideSessionFromList(sessionId) {
|
|
1806
|
-
if (!sessionId) return;
|
|
1807
|
-
const entry = _runtimeState.get(sessionId);
|
|
1808
|
-
if (entry) entry.listHidden = true;
|
|
1809
|
-
}
|
|
1810
|
-
|
|
1811
|
-
export function getSessionAbortSignal(sessionId) {
|
|
1812
|
-
return _runtimeState.get(sessionId)?.controller?.signal ?? null;
|
|
1813
|
-
}
|
|
1814
|
-
|
|
1815
|
-
/**
|
|
1816
|
-
* Return the most recent "session is making progress" timestamp.
|
|
1817
|
-
*
|
|
1818
|
-
* Combines three independent progress signals so an idle watchdog can stay
|
|
1819
|
-
* alive across both streaming and long tool calls:
|
|
1820
|
-
* - lastStreamDeltaAt: provider stream chunk landed
|
|
1821
|
-
* - toolStartedAt: a tool call just kicked off (nested tool work may
|
|
1822
|
-
* stall the outer stream for a while; this keeps the watchdog from
|
|
1823
|
-
* killing legitimate sub-agent runs)
|
|
1824
|
-
* - askStartedAt: ask just started; covers the pre-stream connect window
|
|
1825
|
-
*
|
|
1826
|
-
* Returns 0 when the runtime entry is unknown so callers can decide to
|
|
1827
|
-
* either skip the watchdog or treat 0 as "no progress yet".
|
|
1828
|
-
*/
|
|
1829
|
-
export function getSessionLastProgressAt(sessionId) {
|
|
1830
|
-
const entry = _runtimeState.get(sessionId);
|
|
1831
|
-
if (!entry) return 0;
|
|
1832
|
-
return Math.max(
|
|
1833
|
-
entry.lastProgressAt || 0,
|
|
1834
|
-
entry.lastStreamDeltaAt || 0,
|
|
1835
|
-
entry.toolStartedAt || 0,
|
|
1836
|
-
entry.askStartedAt || 0,
|
|
1837
|
-
);
|
|
1838
|
-
}
|
|
1839
|
-
|
|
1840
|
-
/**
|
|
1841
|
-
* Link a parent AbortSignal to a sub-session's controller so that aborting
|
|
1842
|
-
* the parent (fan-out deadline or caller ESC) tears down the agent role's
|
|
1843
|
-
* provider call promptly. Safe to call after prepareAgentSession but before
|
|
1844
|
-
* askSession completes. No-op if the session runtime isn't found.
|
|
1845
|
-
*
|
|
1846
|
-
* @param {string} sessionId — the sub-session to abort
|
|
1847
|
-
* @param {AbortSignal} parentSignal — upstream signal (from fan-out coordinator)
|
|
1848
|
-
*/
|
|
1849
|
-
export function linkParentSignalToSession(sessionId, parentSignal) {
|
|
1850
|
-
if (!(parentSignal instanceof AbortSignal)) return;
|
|
1851
|
-
const entry = _touchRuntime(sessionId);
|
|
1852
|
-
if (!entry.controller) entry.controller = createAbortController();
|
|
1853
|
-
const abortReason = () => {
|
|
1854
|
-
const reason = parentSignal.reason;
|
|
1855
|
-
if (reason instanceof Error) return reason;
|
|
1856
|
-
if (reason !== undefined && reason !== null && reason !== '') return new Error(String(reason));
|
|
1857
|
-
return new Error('parent signal aborted');
|
|
1858
|
-
};
|
|
1859
|
-
if (parentSignal.aborted) {
|
|
1860
|
-
_unlinkParentAbortListener(entry);
|
|
1861
|
-
try { entry.controller.abort(abortReason()); } catch { /* ignore */ }
|
|
1862
|
-
return;
|
|
1863
|
-
}
|
|
1864
|
-
_unlinkParentAbortListener(entry);
|
|
1865
|
-
const onParentAbort = () => {
|
|
1866
|
-
try { entry.controller?.abort(abortReason()); } catch { /* ignore */ }
|
|
1867
|
-
};
|
|
1868
|
-
parentSignal.addEventListener('abort', onParentAbort, { once: true });
|
|
1869
|
-
entry.parentAbortLink = { signal: parentSignal, listener: onParentAbort };
|
|
1870
|
-
}
|
|
1871
|
-
function _unlinkParentAbortListener(entry) {
|
|
1872
|
-
const link = entry?.parentAbortLink;
|
|
1873
|
-
if (!link) return;
|
|
1874
|
-
try { link.signal.removeEventListener('abort', link.listener); } catch { /* ignore */ }
|
|
1875
|
-
entry.parentAbortLink = null;
|
|
1876
|
-
}
|
|
1877
|
-
function _clearSessionRuntime(id) {
|
|
1878
|
-
if (id) {
|
|
1879
|
-
_stopToolActivityHeartbeat(id);
|
|
1880
|
-
_unlinkParentAbortListener(_runtimeState.get(id));
|
|
1881
|
-
_runtimeState.delete(id);
|
|
1882
|
-
// R15: also drop the per-session metric-idempotency Set; otherwise it
|
|
1883
|
-
// grows O(sessions x iterations) for the whole server lifetime since
|
|
1884
|
-
// nothing else deletes from _metricSeenIter on session close.
|
|
1885
|
-
_metricSeenIter.delete(id);
|
|
1886
|
-
}
|
|
1887
|
-
}
|
|
1888
|
-
|
|
1889
948
|
/**
|
|
1890
949
|
* Wrap an async call so that if the session's controller aborts mid-flight,
|
|
1891
950
|
* the wrapper settles with a SessionClosedError even if the underlying promise
|
|
@@ -2081,7 +1140,7 @@ async function persistCompactedOutgoingAfterAskFailure({
|
|
|
2081
1140
|
}) {
|
|
2082
1141
|
if (!activeSession || activeSession.closed === true) return;
|
|
2083
1142
|
if (!Array.isArray(turnOutgoing) || turnOutgoing.length === 0) return;
|
|
2084
|
-
const currentRuntime =
|
|
1143
|
+
const currentRuntime = _getRuntimeEntry(sessionId);
|
|
2085
1144
|
if (currentRuntime?.closed || currentRuntime?.generation !== askGeneration) return;
|
|
2086
1145
|
const sanitized = sanitizeSessionMessagesForModel(turnOutgoing);
|
|
2087
1146
|
const priorSanitized = sanitizeSessionMessagesForModel(
|
|
@@ -2159,7 +1218,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
2159
1218
|
}
|
|
2160
1219
|
}
|
|
2161
1220
|
if (!hasModelVisiblePromptContent(prompt)) {
|
|
2162
|
-
_unlinkParentAbortListener(
|
|
1221
|
+
_unlinkParentAbortListener(_getRuntimeEntry(sessionId));
|
|
2163
1222
|
return _result;
|
|
2164
1223
|
}
|
|
2165
1224
|
// ── Synchronous pre-await setup (must happen before any await so
|
|
@@ -2389,7 +1448,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
2389
1448
|
}
|
|
2390
1449
|
// Post-loop validation: if closeSession() landed while we were awaiting,
|
|
2391
1450
|
// drop the save so the tombstone on disk isn't overwritten.
|
|
2392
|
-
const currentRuntime =
|
|
1451
|
+
const currentRuntime = _getRuntimeEntry(sessionId);
|
|
2393
1452
|
if (currentRuntime?.closed || currentRuntime?.generation !== askGeneration) {
|
|
2394
1453
|
const reason = currentRuntime?.closedReason;
|
|
2395
1454
|
throw new SessionClosedError(sessionId, `closed during call (reason=${reason || 'unknown'})`, reason || null);
|
|
@@ -2605,7 +1664,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
2605
1664
|
// stale in-flight array once the turn unwinds.
|
|
2606
1665
|
if (activeSession) activeSession.liveTurnMessages = null;
|
|
2607
1666
|
if (err instanceof SessionClosedError) {
|
|
2608
|
-
const currentRuntime =
|
|
1667
|
+
const currentRuntime = _getRuntimeEntry(sessionId);
|
|
2609
1668
|
if (!currentRuntime?.closed) {
|
|
2610
1669
|
if (activeSession) {
|
|
2611
1670
|
const originalMessages = Array.isArray(activeSession.messages) ? activeSession.messages : [];
|
|
@@ -2673,14 +1732,14 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
2673
1732
|
continue;
|
|
2674
1733
|
}
|
|
2675
1734
|
}
|
|
2676
|
-
_unlinkParentAbortListener(
|
|
1735
|
+
_unlinkParentAbortListener(_getRuntimeEntry(sessionId));
|
|
2677
1736
|
return _result;
|
|
2678
1737
|
}
|
|
2679
1738
|
} finally {
|
|
2680
1739
|
// Clear the controller only if it's still ours (closeSession may have
|
|
2681
1740
|
// swapped it). Leave the rest of the runtime entry intact so agent type=list
|
|
2682
1741
|
// can still surface the final stage (done/error/cancelling).
|
|
2683
|
-
const entry =
|
|
1742
|
+
const entry = _getRuntimeEntry(sessionId);
|
|
2684
1743
|
if (entry && entry.generation === askGeneration) {
|
|
2685
1744
|
_unlinkParentAbortListener(entry);
|
|
2686
1745
|
entry.controller = null;
|
|
@@ -2759,7 +1818,7 @@ export function getSession(id) {
|
|
|
2759
1818
|
export function listSessions(opts = {}) {
|
|
2760
1819
|
const includeClosed = opts.includeClosed === true;
|
|
2761
1820
|
const sessions = listStoredSessionSummaries();
|
|
2762
|
-
const hiddenIds = new Set([...
|
|
1821
|
+
const hiddenIds = new Set([..._runtimeEntries()].filter(([, e]) => e.listHidden).map(([id]) => id));
|
|
2763
1822
|
// Tombstoned sessions (closed===true) are excluded unless the caller opts in
|
|
2764
1823
|
// (e.g. agent list includeClosed:true).
|
|
2765
1824
|
return sessions.filter(s => !hiddenIds.has(s.id) && (includeClosed || s.closed !== true));
|
|
@@ -2846,6 +1905,7 @@ export async function clearSessionMessages(sessionId, options = {}) {
|
|
|
2846
1905
|
const afterTokens = estimateTranscriptContextUsage(keep, session.tools || []);
|
|
2847
1906
|
const now = Date.now();
|
|
2848
1907
|
session.messages = keep;
|
|
1908
|
+
session.sessionStartMetaInjected = false;
|
|
2849
1909
|
session.totalInputTokens = 0;
|
|
2850
1910
|
session.totalOutputTokens = 0;
|
|
2851
1911
|
session.totalCachedReadTokens = 0;
|
|
@@ -2935,12 +1995,21 @@ export async function updateSessionStatus(id, status) {
|
|
|
2935
1995
|
* Long-term cleanup: `sweepTombstones()` below unlinks tombstones older than
|
|
2936
1996
|
* TOMBSTONE_MAX_AGE_MS (24h — vastly longer than any realistic in-flight race).
|
|
2937
1997
|
*/
|
|
2938
|
-
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;
|
|
2939
2008
|
if (!id) return false;
|
|
2940
2009
|
_stopToolActivityHeartbeat(id);
|
|
2941
2010
|
// Prefer in-memory runtime session — allBashSessionIds may not be persisted
|
|
2942
2011
|
// yet for shells opened in the current turn (BL-bash-disk-sync).
|
|
2943
|
-
const inMemory =
|
|
2012
|
+
const inMemory = _getRuntimeEntry(id)?.session;
|
|
2944
2013
|
const persisted = inMemory || loadSession(id);
|
|
2945
2014
|
const bashSessionId = persisted?.implicitBashSessionId || null;
|
|
2946
2015
|
// Collect all persistent bash shells created during this session.
|
|
@@ -2951,9 +2020,19 @@ export function closeSession(id, reason = 'manual') {
|
|
|
2951
2020
|
// against old session records that only have implicitBashSessionId.
|
|
2952
2021
|
if (bashSessionId && !allBashIds.includes(bashSessionId)) allBashIds.push(bashSessionId);
|
|
2953
2022
|
// 1. Tombstone first — this wins the race against saveSession().
|
|
2954
|
-
|
|
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);
|
|
2955
2034
|
// 2. Mark runtime as closed so post-await validation in askSession fires.
|
|
2956
|
-
const entry =
|
|
2035
|
+
const entry = _getRuntimeEntry(id);
|
|
2957
2036
|
if (entry) {
|
|
2958
2037
|
entry.closed = true;
|
|
2959
2038
|
entry.closedReason = reason;
|
|
@@ -2972,7 +2051,7 @@ export function closeSession(id, reason = 'manual') {
|
|
|
2972
2051
|
try {
|
|
2973
2052
|
const askStartedAt = entry?.askStartedAt;
|
|
2974
2053
|
const durationMs = (typeof askStartedAt === 'number') ? (Date.now() - askStartedAt) : null;
|
|
2975
|
-
const parts = [`session=${id}`, `reason=${reason}`];
|
|
2054
|
+
const parts = [`session=${id}`, `reason=${reason}`, `tombstone=${tombstone}`];
|
|
2976
2055
|
if (durationMs != null) parts.push(`duration=${durationMs}ms`);
|
|
2977
2056
|
if (process.env.MIXDOG_DEBUG_SESSION_LOG) process.stderr.write(`[agent-close] ${parts.join(' ')}\n`);
|
|
2978
2057
|
} catch { /* best-effort */ }
|
|
@@ -3002,7 +2081,7 @@ export function closeSession(id, reason = 'manual') {
|
|
|
3002
2081
|
export function abortSessionTurn(id, reason = 'turn-abort') {
|
|
3003
2082
|
if (!id) return false;
|
|
3004
2083
|
_stopToolActivityHeartbeat(id);
|
|
3005
|
-
const entry =
|
|
2084
|
+
const entry = _getRuntimeEntry(id);
|
|
3006
2085
|
if (!entry || entry.closed) return false;
|
|
3007
2086
|
entry.stage = 'cancelling';
|
|
3008
2087
|
entry.closedReason = reason;
|
|
@@ -3052,7 +2131,7 @@ function sweepIdleSessions({ includeTombstones = true } = {}) {
|
|
|
3052
2131
|
// Skip entries with an active in-flight controller — aborting
|
|
3053
2132
|
// them via closeSession() is the safe path; clearing the runtime
|
|
3054
2133
|
// without signalling the controller leaves orphan provider work.
|
|
3055
|
-
const rtEntry =
|
|
2134
|
+
const rtEntry = _getRuntimeEntry(d.id);
|
|
3056
2135
|
if (rtEntry && rtEntry.controller && !rtEntry.controller.signal?.aborted) {
|
|
3057
2136
|
try { closeSession(d.id, 'idle-sweep'); } catch { /* ignore */ }
|
|
3058
2137
|
} else {
|
|
@@ -3120,7 +2199,7 @@ export function sweepTombstones() {
|
|
|
3120
2199
|
}
|
|
3121
2200
|
|
|
3122
2201
|
function hasActiveRuntimeWork() {
|
|
3123
|
-
for (const [, entry] of
|
|
2202
|
+
for (const [, entry] of _runtimeEntries()) {
|
|
3124
2203
|
if (!entry || entry.closed === true) continue;
|
|
3125
2204
|
if (entry.controller && !entry.controller.signal?.aborted) return true;
|
|
3126
2205
|
if (['connecting', 'requesting', 'streaming', 'tool_running', 'cancelling'].includes(entry.stage)) return true;
|