mixdog 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -23
- package/package.json +33 -27
- package/scripts/_test-folder-dialog.mjs +30 -0
- package/scripts/agent-parallel-smoke.mjs +388 -0
- package/scripts/agent-tag-reuse-smoke.mjs +183 -0
- package/scripts/background-task-meta-smoke.mjs +38 -0
- package/scripts/boot-smoke.mjs +52 -9
- package/scripts/build-runtime-linux.sh +348 -0
- package/scripts/build-runtime-macos.sh +217 -0
- package/scripts/build-runtime-windows.ps1 +242 -0
- package/scripts/compact-active-turn-test.mjs +68 -0
- package/scripts/compact-smoke.mjs +859 -129
- package/scripts/compact-trigger-migration-smoke.mjs +187 -0
- package/scripts/fix-brief-fn.mjs +35 -0
- package/scripts/fix-format-tool-surface.mjs +24 -0
- package/scripts/fix-tool-exec-visible.mjs +42 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/hook-bus-test.mjs +330 -0
- package/scripts/lead-workflow-smoke.mjs +33 -39
- package/scripts/live-worker-smoke.mjs +43 -37
- package/scripts/llm-trace-summary.mjs +315 -0
- package/scripts/memory-meta-concurrency-test.mjs +20 -0
- package/scripts/output-style-smoke.mjs +56 -15
- package/scripts/parent-abort-link-test.mjs +44 -0
- package/scripts/patch-agent-brief.mjs +48 -0
- package/scripts/patch-app.mjs +21 -0
- package/scripts/patch-app2.mjs +18 -0
- package/scripts/patch-dist-brief.mjs +96 -0
- package/scripts/patch-tool-exec.mjs +70 -0
- package/scripts/pretool-ask-runtime-test.mjs +54 -0
- package/scripts/provider-toolcall-test.mjs +376 -0
- package/scripts/reactive-compact-persist-smoke.mjs +124 -0
- package/scripts/sanitize-tool-pairs-test.mjs +260 -0
- package/scripts/session-context-bench.mjs +344 -0
- package/scripts/session-ingest-smoke.mjs +177 -0
- package/scripts/set-effort-config-test.mjs +41 -0
- package/scripts/smoke-runtime-negative.ps1 +106 -0
- package/scripts/smoke-runtime-negative.sh +97 -0
- package/scripts/smoke.mjs +25 -0
- package/scripts/tool-result-hook-test.mjs +48 -0
- package/scripts/tool-smoke.mjs +1223 -95
- package/scripts/toolcall-args-test.mjs +150 -0
- package/scripts/tui-background-failure-smoke.mjs +73 -0
- package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
- package/src/agents/debugger/AGENT.md +8 -0
- package/src/agents/explore/AGENT.md +4 -0
- package/src/agents/heavy-worker/AGENT.md +9 -3
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +8 -0
- package/src/agents/scheduler-task/AGENT.md +12 -0
- package/src/agents/scheduler-task/agent.json +6 -0
- package/src/agents/webhook-handler/AGENT.md +12 -0
- package/src/agents/webhook-handler/agent.json +6 -0
- package/src/agents/worker/AGENT.md +9 -3
- package/src/app.mjs +77 -3
- package/src/defaults/hidden-roles.json +17 -12
- package/src/headless-role.mjs +117 -0
- package/src/help.mjs +30 -0
- package/src/hooks/lib/permission-evaluator.cjs +11 -475
- package/src/lib/keychain-cjs.cjs +9 -1
- package/src/lib/mixdog-debug.cjs +0 -7
- package/src/lib/rules-builder.cjs +240 -96
- package/src/lib/text-utils.cjs +1 -1
- package/src/mixdog-session-runtime.mjs +2325 -450
- package/src/output-styles/default.md +12 -28
- package/src/output-styles/extreme-simple.md +9 -6
- package/src/output-styles/simple.md +22 -9
- package/src/repl.mjs +118 -59
- package/src/rules/agent/00-common.md +15 -0
- package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
- package/src/rules/agent/30-explorer.md +22 -0
- package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
- package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
- package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
- package/src/rules/lead/01-general.md +9 -5
- package/src/rules/lead/04-workflow.md +51 -12
- package/src/rules/lead/lead-tool.md +6 -0
- package/src/rules/shared/01-tool.md +12 -1
- package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
- package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
- package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
- package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
- package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
- package/src/runtime/agent/orchestrator/config.mjs +174 -55
- package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
- package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
- package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
- package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
- package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
- package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
- package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
- package/src/runtime/agent/orchestrator/session/loop.mjs +851 -674
- package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
- package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
- package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
- package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
- package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
- package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
- package/src/runtime/channels/index.mjs +152 -24
- package/src/runtime/channels/lib/scheduler.mjs +18 -14
- package/src/runtime/channels/lib/session-discovery.mjs +3 -2
- package/src/runtime/channels/lib/tool-format.mjs +0 -2
- package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
- package/src/runtime/channels/lib/webhook.mjs +1 -1
- package/src/runtime/channels/tool-defs.mjs +29 -29
- package/src/runtime/memory/index.mjs +635 -107
- package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
- package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
- package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
- package/src/runtime/memory/lib/memory-cycle1.mjs +38 -17
- package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
- package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
- package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
- package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
- package/src/runtime/memory/lib/memory.mjs +88 -9
- package/src/runtime/memory/lib/model-profile.mjs +1 -1
- package/src/runtime/memory/lib/pg/adapter.mjs +15 -1
- package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
- package/src/runtime/memory/lib/session-ingest.mjs +194 -0
- package/src/runtime/memory/lib/trace-store.mjs +96 -51
- package/src/runtime/memory/tool-defs.mjs +46 -37
- package/src/runtime/search/index.mjs +102 -466
- package/src/runtime/search/lib/web-tools.mjs +45 -25
- package/src/runtime/search/tool-defs.mjs +16 -23
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +4 -3
- package/src/runtime/shared/background-tasks.mjs +122 -11
- package/src/runtime/shared/child-spawn-gate.mjs +145 -0
- package/src/runtime/shared/config.mjs +7 -4
- package/src/runtime/shared/err-text.mjs +131 -4
- package/src/runtime/shared/llm/cost.mjs +2 -2
- package/src/runtime/shared/llm/http-agent.mjs +23 -7
- package/src/runtime/shared/llm/index.mjs +34 -11
- package/src/runtime/shared/llm/usage-log.mjs +4 -4
- package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
- package/src/runtime/shared/singleton-owner.mjs +104 -0
- package/src/runtime/shared/tool-execution-contract.mjs +199 -20
- package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
- package/src/runtime/shared/tool-surface.mjs +624 -98
- package/src/runtime/shared/user-data-guard.mjs +0 -2
- package/src/standalone/agent-task-status.mjs +203 -0
- package/src/standalone/agent-task-status.test.mjs +76 -0
- package/src/standalone/agent-tool.mjs +1913 -0
- package/src/standalone/channel-worker.mjs +370 -14
- package/src/standalone/explore-tool.mjs +165 -70
- package/src/standalone/folder-dialog.mjs +314 -0
- package/src/standalone/hook-bus.mjs +898 -22
- package/src/standalone/memory-runtime-proxy.mjs +320 -0
- package/src/standalone/projects.mjs +226 -0
- package/src/standalone/provider-admin.mjs +41 -24
- package/src/standalone/seeds.mjs +2 -69
- package/src/standalone/usage-dashboard.mjs +96 -8
- package/src/tui/App.jsx +4798 -2153
- package/src/tui/components/AnsiText.jsx +39 -28
- package/src/tui/components/ContextPanel.jsx +87 -29
- package/src/tui/components/Markdown.jsx +43 -77
- package/src/tui/components/MarkdownTable.jsx +9 -184
- package/src/tui/components/Message.jsx +28 -11
- package/src/tui/components/Picker.jsx +95 -56
- package/src/tui/components/PromptInput.jsx +367 -239
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +27 -21
- package/src/tui/components/Spinner.jsx +67 -38
- package/src/tui/components/StatusLine.jsx +606 -38
- package/src/tui/components/TextEntryPanel.jsx +128 -9
- package/src/tui/components/ToolExecution.jsx +617 -368
- package/src/tui/components/TurnDone.jsx +3 -3
- package/src/tui/components/UsagePanel.jsx +3 -5
- package/src/tui/components/tool-output-format.mjs +365 -0
- package/src/tui/components/tool-output-format.test.mjs +220 -0
- package/src/tui/dist/index.mjs +8915 -2418
- package/src/tui/engine-runtime-notification.test.mjs +115 -0
- package/src/tui/engine-tool-result-text.test.mjs +75 -0
- package/src/tui/engine.mjs +1455 -279
- package/src/tui/figures.mjs +21 -40
- package/src/tui/index.jsx +75 -31
- package/src/tui/input-editing.mjs +25 -0
- package/src/tui/markdown/format-token.mjs +511 -68
- package/src/tui/markdown/format-token.test.mjs +216 -0
- package/src/tui/markdown/render-ansi.mjs +94 -0
- package/src/tui/markdown/render-ansi.test.mjs +108 -0
- package/src/tui/markdown/stream-fence.mjs +34 -0
- package/src/tui/markdown/stream-fence.test.mjs +26 -0
- package/src/tui/markdown/table-layout.mjs +250 -0
- package/src/tui/paste-attachments.mjs +0 -7
- package/src/tui/spinner-verbs.mjs +1 -2
- package/src/tui/statusline-ansi-bridge.mjs +172 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
- package/src/tui/theme.mjs +746 -24
- package/src/tui/time-format.mjs +1 -1
- package/src/tui/transcript-tool-failures.mjs +67 -0
- package/src/tui/transcript-tool-failures.test.mjs +111 -0
- package/src/ui/ansi.mjs +1 -2
- package/src/ui/markdown.mjs +85 -26
- package/src/ui/markdown.test.mjs +70 -0
- package/src/ui/model-display.mjs +121 -0
- package/src/ui/session-stats.mjs +44 -0
- package/src/ui/statusline-context-label.test.mjs +15 -0
- package/src/ui/statusline.mjs +386 -178
- package/src/ui/tool-card.mjs +3 -16
- package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
- package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
- package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
- package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
- package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
- package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
- package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
- package/src/workflows/default/WORKFLOW.md +12 -5
- package/src/workflows/default/workflow.json +0 -1
- package/src/workflows/solo/WORKFLOW.md +15 -0
- package/src/workflows/solo/workflow.json +7 -0
- package/vendor/ink/build/output.js +6 -1
- package/src/agents/scheduler-task.md +0 -3
- package/src/agents/web-researcher/AGENT.md +0 -3
- package/src/agents/web-researcher/agent.json +0 -6
- package/src/agents/webhook-handler.md +0 -3
- package/src/rules/bridge/00-common.md +0 -5
- package/src/rules/bridge/30-explorer.md +0 -4
- package/src/rules/lead/00-tool-lead.md +0 -5
- package/src/rules/shared/00-language.md +0 -3
- package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
- package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
- package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
- package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
- package/src/standalone/bridge-tool.mjs +0 -1414
- package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
_applyCompactFailurePersistToSession,
|
|
6
|
+
_sessionMessagesAdvancedBeyondCompactedOutgoing,
|
|
7
|
+
} from './manager.mjs';
|
|
8
|
+
|
|
9
|
+
const compacted = [
|
|
10
|
+
{ role: 'user', content: 'summary turn' },
|
|
11
|
+
{ role: 'user', content: 'current task' },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
test('sessionMessagesAdvancedBeyondCompactedOutgoing: true for compacted prefix plus final assistant', () => {
|
|
15
|
+
const current = [
|
|
16
|
+
...compacted,
|
|
17
|
+
{ role: 'assistant', content: 'done' },
|
|
18
|
+
];
|
|
19
|
+
assert.equal(_sessionMessagesAdvancedBeyondCompactedOutgoing(current, compacted), true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('sessionMessagesAdvancedBeyondCompactedOutgoing: true for compacted prefix plus empty-final assistant', () => {
|
|
23
|
+
const current = [
|
|
24
|
+
...compacted,
|
|
25
|
+
{ role: 'assistant', content: '', emptyFinal: true },
|
|
26
|
+
];
|
|
27
|
+
assert.equal(_sessionMessagesAdvancedBeyondCompactedOutgoing(current, compacted), true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('sessionMessagesAdvancedBeyondCompactedOutgoing: false when transcript has not advanced', () => {
|
|
31
|
+
assert.equal(_sessionMessagesAdvancedBeyondCompactedOutgoing(compacted, compacted), false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('sessionMessagesAdvancedBeyondCompactedOutgoing: false when extra message is not a final assistant', () => {
|
|
35
|
+
const current = [
|
|
36
|
+
...compacted,
|
|
37
|
+
{ role: 'user', content: 'steering follow-up' },
|
|
38
|
+
];
|
|
39
|
+
assert.equal(_sessionMessagesAdvancedBeyondCompactedOutgoing(current, compacted), false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('sessionMessagesAdvancedBeyondCompactedOutgoing: false when prefix diverges from compacted outgoing', () => {
|
|
43
|
+
const current = [
|
|
44
|
+
{ role: 'user', content: 'different summary' },
|
|
45
|
+
{ role: 'user', content: 'current task' },
|
|
46
|
+
{ role: 'assistant', content: 'done' },
|
|
47
|
+
];
|
|
48
|
+
assert.equal(_sessionMessagesAdvancedBeyondCompactedOutgoing(current, compacted), false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('applyCompactFailurePersistToSession: preserves providerState when transcript already advanced', () => {
|
|
52
|
+
const priorSanitized = [
|
|
53
|
+
...compacted,
|
|
54
|
+
{ role: 'assistant', content: 'done' },
|
|
55
|
+
];
|
|
56
|
+
const session = {
|
|
57
|
+
messages: priorSanitized,
|
|
58
|
+
providerState: { continuation: 'fresh' },
|
|
59
|
+
compaction: { lastStage: 'compacting' },
|
|
60
|
+
};
|
|
61
|
+
const applied = _applyCompactFailurePersistToSession(session, {
|
|
62
|
+
priorSanitized,
|
|
63
|
+
sanitized: compacted,
|
|
64
|
+
messagesAdvanced: true,
|
|
65
|
+
error: new Error('ask failed'),
|
|
66
|
+
});
|
|
67
|
+
assert.equal(applied, true);
|
|
68
|
+
assert.deepEqual(session.messages, priorSanitized);
|
|
69
|
+
assert.deepEqual(session.providerState, { continuation: 'fresh' });
|
|
70
|
+
assert.equal(session.compaction.lastStage, 'failed');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('applyCompactFailurePersistToSession: clears providerState when replacing messages with compacted outgoing', () => {
|
|
74
|
+
const priorSanitized = [
|
|
75
|
+
{ role: 'user', content: 'old long history' },
|
|
76
|
+
];
|
|
77
|
+
const session = {
|
|
78
|
+
messages: priorSanitized,
|
|
79
|
+
providerState: { stale: true },
|
|
80
|
+
compaction: { lastStage: 'compacting' },
|
|
81
|
+
};
|
|
82
|
+
const applied = _applyCompactFailurePersistToSession(session, {
|
|
83
|
+
priorSanitized,
|
|
84
|
+
sanitized: compacted,
|
|
85
|
+
messagesAdvanced: false,
|
|
86
|
+
error: { code: 'AGENT_CONTEXT_OVERFLOW' },
|
|
87
|
+
});
|
|
88
|
+
assert.equal(applied, true);
|
|
89
|
+
assert.deepEqual(session.messages, compacted);
|
|
90
|
+
assert.equal(session.providerState, undefined);
|
|
91
|
+
assert.equal(session.compaction.lastStage, 'overflow_failed');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('applyCompactFailurePersistToSession: no-op when messages unchanged and not advanced', () => {
|
|
95
|
+
const session = {
|
|
96
|
+
messages: compacted,
|
|
97
|
+
providerState: { keep: true },
|
|
98
|
+
};
|
|
99
|
+
const applied = _applyCompactFailurePersistToSession(session, {
|
|
100
|
+
priorSanitized: compacted,
|
|
101
|
+
sanitized: compacted,
|
|
102
|
+
messagesAdvanced: false,
|
|
103
|
+
});
|
|
104
|
+
assert.equal(applied, false);
|
|
105
|
+
assert.deepEqual(session.providerState, { keep: true });
|
|
106
|
+
});
|
|
107
|
+
|
|
@@ -9,9 +9,48 @@ import { randomBytes } from 'crypto';
|
|
|
9
9
|
import { join } from 'path';
|
|
10
10
|
import { Worker } from 'worker_threads';
|
|
11
11
|
import { getPluginData } from '../config.mjs';
|
|
12
|
-
import {
|
|
12
|
+
import { isAgentOwner } from '../agent-owner.mjs';
|
|
13
|
+
import { renameWithRetrySync, updateJsonAtomicSync, writeJsonAtomicSync } from '../../../shared/atomic-file.mjs';
|
|
14
|
+
import { sanitizeContentForStoredHistory } from '../providers/media-normalization.mjs';
|
|
13
15
|
|
|
14
16
|
const _lastSaveError = new Map(); // id -> { message, at }
|
|
17
|
+
const SESSION_SUMMARY_INDEX_VERSION = 1;
|
|
18
|
+
|
|
19
|
+
// The live in-memory session (and every model request)
|
|
20
|
+
// retains attached image bytes across turns so multi-turn recognition works.
|
|
21
|
+
// The persisted session JSON, however, replaces image content with a short
|
|
22
|
+
// text placeholder at serialization time — keeping session files small without
|
|
23
|
+
// starving the model of the image mid-conversation. Returns the same object
|
|
24
|
+
// reference when nothing changed (no-image sessions pay only a shallow scan).
|
|
25
|
+
function _sessionForDisk(session) {
|
|
26
|
+
// Strip the in-flight live-turn alias (askSession sets session.liveTurnMessages
|
|
27
|
+
// for the turn duration so contextStatus() can estimate live context growth).
|
|
28
|
+
// It is a transient duplicate of the working transcript and must never be
|
|
29
|
+
// serialized: mid-turn saves (persistIterationMetrics) would otherwise bloat
|
|
30
|
+
// the session file and persist a non-canonical message array.
|
|
31
|
+
const hasLiveTurn = session && typeof session === 'object'
|
|
32
|
+
&& Object.prototype.hasOwnProperty.call(session, 'liveTurnMessages');
|
|
33
|
+
const messages = Array.isArray(session?.messages) ? session.messages : null;
|
|
34
|
+
if (!messages || messages.length === 0) {
|
|
35
|
+
if (!hasLiveTurn) return session;
|
|
36
|
+
const { liveTurnMessages: _drop, ...rest } = session;
|
|
37
|
+
return rest;
|
|
38
|
+
}
|
|
39
|
+
let changed = false;
|
|
40
|
+
const out = messages.map((m) => {
|
|
41
|
+
if (!m || typeof m !== 'object') return m;
|
|
42
|
+
const content = sanitizeContentForStoredHistory(m.content);
|
|
43
|
+
if (content !== m.content) { changed = true; return { ...m, content }; }
|
|
44
|
+
return m;
|
|
45
|
+
});
|
|
46
|
+
if (!changed) {
|
|
47
|
+
if (!hasLiveTurn) return session;
|
|
48
|
+
const { liveTurnMessages: _drop, ...rest } = session;
|
|
49
|
+
return rest;
|
|
50
|
+
}
|
|
51
|
+
const { liveTurnMessages: _drop, ...rest } = session;
|
|
52
|
+
return { ...rest, messages: out };
|
|
53
|
+
}
|
|
15
54
|
|
|
16
55
|
function _renameWithRetrySync(tmp, target) {
|
|
17
56
|
return renameWithRetrySync(tmp, target);
|
|
@@ -32,6 +71,11 @@ function sessionPath(id) {
|
|
|
32
71
|
}
|
|
33
72
|
return join(getStoreDir(), `${id}.json`);
|
|
34
73
|
}
|
|
74
|
+
function summaryIndexPath() {
|
|
75
|
+
const dir = getPluginData();
|
|
76
|
+
mkdirSync(dir, { recursive: true });
|
|
77
|
+
return join(dir, 'session-summaries.json');
|
|
78
|
+
}
|
|
35
79
|
/**
|
|
36
80
|
* Ensure generation/closed defaults on every session object.
|
|
37
81
|
* Older persisted sessions predate these fields; we normalise at load and save.
|
|
@@ -44,6 +88,176 @@ function _ensureLifecycleFields(session) {
|
|
|
44
88
|
return session;
|
|
45
89
|
}
|
|
46
90
|
|
|
91
|
+
function _messageText(content) {
|
|
92
|
+
if (typeof content === 'string') return content;
|
|
93
|
+
if (Array.isArray(content)) {
|
|
94
|
+
return content.map((part) => {
|
|
95
|
+
if (typeof part === 'string') return part;
|
|
96
|
+
if (part?.type === 'text') return part.text || '';
|
|
97
|
+
if (typeof part?.text === 'string') return part.text;
|
|
98
|
+
try { return JSON.stringify(part); } catch { return ''; }
|
|
99
|
+
}).filter(Boolean).join('\n');
|
|
100
|
+
}
|
|
101
|
+
if (content && typeof content === 'object') {
|
|
102
|
+
if (typeof content.text === 'string') return content.text;
|
|
103
|
+
try { return JSON.stringify(content); } catch { return ''; }
|
|
104
|
+
}
|
|
105
|
+
return '';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function _cleanPreview(text, max = 240) {
|
|
109
|
+
const value = String(text || '').replace(/\s+/g, ' ').trim();
|
|
110
|
+
return value.length > max ? value.slice(0, max).replace(/\s+\S*$/, '').trim() : value;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function _isPreviewNoise(text) {
|
|
114
|
+
const value = String(text || '').trim();
|
|
115
|
+
if (!value) return true;
|
|
116
|
+
if (value.startsWith('<system-reminder>')) return true;
|
|
117
|
+
if (/^Reference files:/i.test(value)) return true;
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function _sessionPreview(session) {
|
|
122
|
+
const messages = Array.isArray(session?.messages) ? session.messages : [];
|
|
123
|
+
let first = '';
|
|
124
|
+
for (const m of messages) {
|
|
125
|
+
if (m?.role !== 'user') continue;
|
|
126
|
+
const text = _cleanPreview(_messageText(m.content));
|
|
127
|
+
if (_isPreviewNoise(text)) continue;
|
|
128
|
+
if (!first) first = text;
|
|
129
|
+
}
|
|
130
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
131
|
+
const m = messages[i];
|
|
132
|
+
if (m?.role !== 'user') continue;
|
|
133
|
+
const text = _cleanPreview(_messageText(m.content));
|
|
134
|
+
if (_isPreviewNoise(text)) continue;
|
|
135
|
+
return text || first;
|
|
136
|
+
}
|
|
137
|
+
return first;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function _sessionMessageCount(session) {
|
|
141
|
+
const messages = Array.isArray(session?.messages) ? session.messages : [];
|
|
142
|
+
let count = 0;
|
|
143
|
+
for (const m of messages) {
|
|
144
|
+
if (m && (m.role === 'user' || m.role === 'assistant')) count++;
|
|
145
|
+
}
|
|
146
|
+
return count;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function _positiveNumber(value, fallback = 0) {
|
|
150
|
+
const n = Number(value);
|
|
151
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function _sessionSummary(session) {
|
|
155
|
+
if (!session?.id) return null;
|
|
156
|
+
return {
|
|
157
|
+
id: String(session.id),
|
|
158
|
+
updatedAt: _positiveNumber(session.updatedAt, Date.now()),
|
|
159
|
+
createdAt: _positiveNumber(session.createdAt, 0),
|
|
160
|
+
lastHeartbeatAt: _positiveNumber(session.lastHeartbeatAt, 0),
|
|
161
|
+
closed: session.closed === true,
|
|
162
|
+
status: String(session.status || (session.closed === true ? 'closed' : 'idle')),
|
|
163
|
+
owner: session.owner || 'user',
|
|
164
|
+
role: session.role || null,
|
|
165
|
+
sourceType: session.sourceType || null,
|
|
166
|
+
sourceName: session.sourceName || null,
|
|
167
|
+
scopeKey: session.scopeKey || null,
|
|
168
|
+
ownerSessionId: session.ownerSessionId || null,
|
|
169
|
+
clientHostPid: _positiveNumber(session.clientHostPid, 0) || null,
|
|
170
|
+
cwd: session.cwd || '',
|
|
171
|
+
provider: session.provider || null,
|
|
172
|
+
model: session.model || null,
|
|
173
|
+
agentTag: session.agentTag || null,
|
|
174
|
+
task_id: session.task_id || session.taskId || null,
|
|
175
|
+
permission: session.permission || null,
|
|
176
|
+
toolPermission: session.toolPermission || null,
|
|
177
|
+
messageCount: _sessionMessageCount(session),
|
|
178
|
+
preview: _sessionPreview(session),
|
|
179
|
+
generation: typeof session.generation === 'number' ? session.generation : 0,
|
|
180
|
+
implicitBashSessionId: session.implicitBashSessionId || null,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function _normalizeSummaryRow(row) {
|
|
185
|
+
if (!row?.id || typeof row.id !== 'string' || !/^[A-Za-z0-9_-]+$/.test(row.id)) return null;
|
|
186
|
+
return {
|
|
187
|
+
id: row.id,
|
|
188
|
+
updatedAt: _positiveNumber(row.updatedAt, 0),
|
|
189
|
+
createdAt: _positiveNumber(row.createdAt, 0),
|
|
190
|
+
lastHeartbeatAt: _positiveNumber(row.lastHeartbeatAt, 0),
|
|
191
|
+
closed: row.closed === true,
|
|
192
|
+
status: String(row.status || (row.closed === true ? 'closed' : 'idle')),
|
|
193
|
+
owner: row.owner || 'user',
|
|
194
|
+
role: row.role || null,
|
|
195
|
+
sourceType: row.sourceType || null,
|
|
196
|
+
sourceName: row.sourceName || null,
|
|
197
|
+
scopeKey: row.scopeKey || null,
|
|
198
|
+
ownerSessionId: row.ownerSessionId || null,
|
|
199
|
+
clientHostPid: _positiveNumber(row.clientHostPid, 0) || null,
|
|
200
|
+
cwd: row.cwd || '',
|
|
201
|
+
provider: row.provider || null,
|
|
202
|
+
model: row.model || null,
|
|
203
|
+
agentTag: row.agentTag || null,
|
|
204
|
+
task_id: row.task_id || null,
|
|
205
|
+
permission: row.permission || null,
|
|
206
|
+
toolPermission: row.toolPermission || null,
|
|
207
|
+
messageCount: Math.max(0, Math.floor(Number(row.messageCount) || 0)),
|
|
208
|
+
preview: _cleanPreview(row.preview || ''),
|
|
209
|
+
generation: typeof row.generation === 'number' ? row.generation : 0,
|
|
210
|
+
implicitBashSessionId: row.implicitBashSessionId || null,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function _normalizeSummaryIndex(raw) {
|
|
215
|
+
const rows = Array.isArray(raw?.rows) ? raw.rows.map(_normalizeSummaryRow).filter(Boolean) : [];
|
|
216
|
+
rows.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
|
|
217
|
+
return { version: SESSION_SUMMARY_INDEX_VERSION, updatedAt: _positiveNumber(raw?.updatedAt, 0), rows };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function _writeSummaryIndex(rows) {
|
|
221
|
+
const cleanRows = (rows || []).map(_normalizeSummaryRow).filter(Boolean)
|
|
222
|
+
.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
|
|
223
|
+
writeJsonAtomicSync(summaryIndexPath(), {
|
|
224
|
+
version: SESSION_SUMMARY_INDEX_VERSION,
|
|
225
|
+
updatedAt: Date.now(),
|
|
226
|
+
rows: cleanRows,
|
|
227
|
+
}, { compact: true, lock: true });
|
|
228
|
+
return cleanRows;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function _upsertSessionSummary(session) {
|
|
232
|
+
const row = _sessionSummary(session);
|
|
233
|
+
if (!row) return;
|
|
234
|
+
try {
|
|
235
|
+
updateJsonAtomicSync(summaryIndexPath(), (cur) => {
|
|
236
|
+
const index = _normalizeSummaryIndex(cur);
|
|
237
|
+
const rows = index.rows.filter((r) => r.id !== row.id);
|
|
238
|
+
const cleanRow = _normalizeSummaryRow(row);
|
|
239
|
+
if (!cleanRow) return undefined;
|
|
240
|
+
const existing = index.rows.find((r) => r.id === row.id) || null;
|
|
241
|
+
if (existing && JSON.stringify(existing) === JSON.stringify(cleanRow)) return undefined;
|
|
242
|
+
rows.push(cleanRow);
|
|
243
|
+
rows.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
|
|
244
|
+
return { version: SESSION_SUMMARY_INDEX_VERSION, updatedAt: Date.now(), rows };
|
|
245
|
+
}, { compact: true, lock: true });
|
|
246
|
+
} catch { /* summary index is best-effort */ }
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function _removeSessionSummary(id) {
|
|
250
|
+
if (!id) return;
|
|
251
|
+
try {
|
|
252
|
+
updateJsonAtomicSync(summaryIndexPath(), (cur) => {
|
|
253
|
+
const index = _normalizeSummaryIndex(cur);
|
|
254
|
+
const rows = index.rows.filter((r) => r.id !== id);
|
|
255
|
+
if (rows.length === index.rows.length) return undefined;
|
|
256
|
+
return { version: SESSION_SUMMARY_INDEX_VERSION, updatedAt: Date.now(), rows };
|
|
257
|
+
}, { compact: true, lock: true });
|
|
258
|
+
} catch { /* summary index is best-effort */ }
|
|
259
|
+
}
|
|
260
|
+
|
|
47
261
|
/** Module-level map tracking in-flight saves per session ID to prevent concurrent write corruption. */
|
|
48
262
|
const _savePending = new Map();
|
|
49
263
|
|
|
@@ -101,19 +315,6 @@ function _clearDebounce(id) {
|
|
|
101
315
|
if (t) { clearTimeout(t); _debounceTimers.delete(id); }
|
|
102
316
|
}
|
|
103
317
|
|
|
104
|
-
// Flush all debounced sessions synchronously on process exit / SIGTERM.
|
|
105
|
-
// This prevents losing the last turn's tool-result writes.
|
|
106
|
-
function _drainAllDebounced() {
|
|
107
|
-
for (const [id, t] of _debounceTimers) {
|
|
108
|
-
clearTimeout(t);
|
|
109
|
-
_debounceTimers.delete(id);
|
|
110
|
-
const cur = _savePending.get(id);
|
|
111
|
-
if (cur && cur.debouncing && cur.payload) {
|
|
112
|
-
try { _doSaveSync(cur.payload); } catch { /* best-effort */ }
|
|
113
|
-
_savePending.delete(id);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
318
|
// Self-registered exit drain; bare 'exit' hook stays as idempotent backup. Use the more comprehensive
|
|
118
319
|
// drainSessionStore so debounce + scheduled + writing payloads all flush.
|
|
119
320
|
process.on('exit', drainSessionStore);
|
|
@@ -291,12 +492,13 @@ function _doSaveSync(payload) {
|
|
|
291
492
|
const target = sessionPath(id);
|
|
292
493
|
const tmp = target + '.' + randomBytes(6).toString('hex') + '.tmp';
|
|
293
494
|
try {
|
|
294
|
-
writeFileSync(tmp, JSON.stringify(session), 'utf-8');
|
|
495
|
+
writeFileSync(tmp, JSON.stringify(_sessionForDisk(session)), 'utf-8');
|
|
295
496
|
if (_shouldDrop(id, opts)) {
|
|
296
497
|
try { unlinkSync(tmp); } catch { /* ignore cleanup failure */ }
|
|
297
498
|
return;
|
|
298
499
|
}
|
|
299
500
|
_renameWithRetrySync(tmp, target);
|
|
501
|
+
_upsertSessionSummary(session);
|
|
300
502
|
} catch (err) {
|
|
301
503
|
try { unlinkSync(tmp); } catch { /* ignore cleanup failure */ }
|
|
302
504
|
throw err;
|
|
@@ -312,7 +514,21 @@ function _shouldDrop(id, opts) {
|
|
|
312
514
|
const target = sessionPath(id);
|
|
313
515
|
if (!existsSync(target)) return false;
|
|
314
516
|
try {
|
|
315
|
-
const
|
|
517
|
+
const raw = readFileSync(target, 'utf-8');
|
|
518
|
+
// Tombstone check only needs `closed` and `generation`. Avoid a full
|
|
519
|
+
// JSON.parse of the entire session (which can be MBs on long chats and
|
|
520
|
+
// ran on every save). Fast-path a regex scan; only fall back to a full
|
|
521
|
+
// parse if the lightweight scan is inconclusive.
|
|
522
|
+
const closedMatch = /"closed"\s*:\s*(true|false)/.exec(raw);
|
|
523
|
+
if (closedMatch && closedMatch[1] === 'false') return false;
|
|
524
|
+
if (closedMatch && closedMatch[1] === 'true') {
|
|
525
|
+
const genMatch = /"generation"\s*:\s*(-?\d+)/.exec(raw);
|
|
526
|
+
const diskGen = genMatch ? Number(genMatch[1]) : 0;
|
|
527
|
+
return diskGen >= expected;
|
|
528
|
+
}
|
|
529
|
+
// No `closed` key found by scan — not a tombstone, so nothing to drop.
|
|
530
|
+
if (!closedMatch && !/"closed"/.test(raw)) return false;
|
|
531
|
+
const onDisk = JSON.parse(raw);
|
|
316
532
|
const diskGen = typeof onDisk.generation === 'number' ? onDisk.generation : 0;
|
|
317
533
|
return onDisk.closed === true && diskGen >= expected;
|
|
318
534
|
} catch {
|
|
@@ -379,7 +595,7 @@ async function _doSave(payload) {
|
|
|
379
595
|
const target = sessionPath(id);
|
|
380
596
|
const tmp = target + '.' + randomBytes(6).toString('hex') + '.tmp';
|
|
381
597
|
try {
|
|
382
|
-
await fsp.writeFile(tmp, JSON.stringify(session), 'utf-8');
|
|
598
|
+
await fsp.writeFile(tmp, JSON.stringify(_sessionForDisk(session)), 'utf-8');
|
|
383
599
|
// Second check: between the temp write and the rename, closeSession()
|
|
384
600
|
// may have planted a tombstone. Re-check on disk; if a newer tombstone
|
|
385
601
|
// now exists, discard our temp file rather than let rename clobber it.
|
|
@@ -390,6 +606,7 @@ async function _doSave(payload) {
|
|
|
390
606
|
return;
|
|
391
607
|
}
|
|
392
608
|
_renameWithRetrySync(tmp, target);
|
|
609
|
+
_upsertSessionSummary(session);
|
|
393
610
|
} catch (err) {
|
|
394
611
|
try { unlinkSync(tmp); } catch { /* ignore cleanup failure */ }
|
|
395
612
|
_savePending.delete(id);
|
|
@@ -417,7 +634,7 @@ export function markSessionClosed(id, reason = 'manual') {
|
|
|
417
634
|
const target = sessionPath(id);
|
|
418
635
|
const tmp = target + '.' + randomBytes(6).toString('hex') + '.tmp';
|
|
419
636
|
try {
|
|
420
|
-
writeFileSync(tmp, JSON.stringify(tombstone), 'utf-8');
|
|
637
|
+
writeFileSync(tmp, JSON.stringify(_sessionForDisk(tombstone)), 'utf-8');
|
|
421
638
|
_renameWithRetrySync(tmp, target);
|
|
422
639
|
} catch {
|
|
423
640
|
try { unlinkSync(tmp); } catch { /* ignore */ }
|
|
@@ -426,6 +643,7 @@ export function markSessionClosed(id, reason = 'manual') {
|
|
|
426
643
|
_savePending.delete(id);
|
|
427
644
|
_clearLiveSession(id);
|
|
428
645
|
_deleteHeartbeat(id);
|
|
646
|
+
_upsertSessionSummary(tombstone);
|
|
429
647
|
// Structured close metric. Single emission point because every close
|
|
430
648
|
// path funnels through markSessionClosed. lifeMs = updatedAt-createdAt
|
|
431
649
|
// straddles the tombstone (updatedAt was just set to Date.now()), so
|
|
@@ -488,14 +706,15 @@ export function deleteSession(id) {
|
|
|
488
706
|
catch { /* fall through to .hb cleanup */ }
|
|
489
707
|
}
|
|
490
708
|
_deleteHeartbeat(id);
|
|
709
|
+
if (removed) _removeSessionSummary(id);
|
|
491
710
|
return removed;
|
|
492
711
|
}
|
|
493
712
|
const DEFAULT_SESSION_TTL_MS = 5 * 60 * 1000; // 5 minutes idle — aligned with Anthropic 5m messages tier and OpenAI in-memory cache window
|
|
494
|
-
// Completed
|
|
713
|
+
// Completed agents (idle/done/error) live until terminal reap - must
|
|
495
714
|
// match TERMINAL_REAP_MS / _scheduleBridgeReap (3_600_000) in index.mjs and
|
|
496
|
-
//
|
|
497
|
-
const
|
|
498
|
-
const
|
|
715
|
+
// agent stall watchdog, so the store sweep is the durable 1h reaper.
|
|
716
|
+
const AGENT_TERMINAL_TTL_MS = 60 * 60 * 1000;
|
|
717
|
+
const AGENT_TERMINAL_STATUSES = new Set(['idle', 'done', 'error']);
|
|
499
718
|
// Hard wall-clock ceiling for sessions stuck in status='running'. The
|
|
500
719
|
// stream-watchdog should abort stalled streams within ~120s, but if it misses
|
|
501
720
|
// one (process crash, watchdog not started, provider never returned), this
|
|
@@ -518,6 +737,28 @@ export function listStoredSessions() {
|
|
|
518
737
|
return sessions.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
519
738
|
}
|
|
520
739
|
|
|
740
|
+
export function rebuildSessionSummaryIndex() {
|
|
741
|
+
const rows = listStoredSessions().map(_sessionSummary).filter(Boolean);
|
|
742
|
+
return _writeSummaryIndex(rows);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
export function listStoredSessionSummaries(options = {}) {
|
|
746
|
+
const rebuildIfMissing = options.rebuildIfMissing !== false;
|
|
747
|
+
const p = summaryIndexPath();
|
|
748
|
+
if (!existsSync(p)) {
|
|
749
|
+
return rebuildIfMissing ? rebuildSessionSummaryIndex() : [];
|
|
750
|
+
}
|
|
751
|
+
try {
|
|
752
|
+
const index = _normalizeSummaryIndex(JSON.parse(readFileSync(p, 'utf-8')));
|
|
753
|
+
if (index.rows.length > 0) return index.rows;
|
|
754
|
+
const dir = getStoreDir();
|
|
755
|
+
const hasSessionFiles = existsSync(dir) && readdirSync(dir).some((f) => f.endsWith('.json'));
|
|
756
|
+
return hasSessionFiles && rebuildIfMissing ? rebuildSessionSummaryIndex() : index.rows;
|
|
757
|
+
} catch {
|
|
758
|
+
return rebuildIfMissing ? rebuildSessionSummaryIndex() : [];
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
521
762
|
/**
|
|
522
763
|
* Raw directory scan — returns every parseable session file without any
|
|
523
764
|
* TTL-based inline deletion. Callers (e.g. sweepTombstones) need to own the
|
|
@@ -552,7 +793,7 @@ export function sweepStaleSessions(ttlMs, options = {}) {
|
|
|
552
793
|
const dir = getStoreDir();
|
|
553
794
|
if (!existsSync(dir))
|
|
554
795
|
return { cleaned: 0, remaining: 0, details: [], tombstonesCleaned: 0, tombstoneDetails: [], tombstoneErrors: [] };
|
|
555
|
-
const
|
|
796
|
+
const summaries = listStoredSessionSummaries();
|
|
556
797
|
const now = Date.now();
|
|
557
798
|
let cleaned = 0;
|
|
558
799
|
let remaining = 0;
|
|
@@ -560,38 +801,42 @@ export function sweepStaleSessions(ttlMs, options = {}) {
|
|
|
560
801
|
const details = [];
|
|
561
802
|
const tombstoneDetails = [];
|
|
562
803
|
const tombstoneErrors = [];
|
|
563
|
-
for (const
|
|
804
|
+
for (const row of summaries) {
|
|
564
805
|
try {
|
|
565
|
-
|
|
806
|
+
if (!row?.id) continue;
|
|
807
|
+
const jsonPath = sessionPath(row.id);
|
|
808
|
+
if (!existsSync(jsonPath)) {
|
|
809
|
+
_removeSessionSummary(row.id);
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
566
812
|
let jsonMtime = 0;
|
|
567
813
|
let heartbeatMtime = 0;
|
|
568
814
|
try { jsonMtime = statSync(jsonPath).mtimeMs || 0; } catch {}
|
|
569
815
|
try {
|
|
570
|
-
const hbPath = join(dir,
|
|
816
|
+
const hbPath = join(dir, `${row.id}.hb`);
|
|
571
817
|
if (existsSync(hbPath)) heartbeatMtime = statSync(hbPath).mtimeMs || 0;
|
|
572
818
|
} catch { /* .hb unavailable — fall back to JSON fields */ }
|
|
573
819
|
const freshnessGateMs = sweepIdle ? maxAge : (sweepTombstones ? tombstoneMaxAgeMs : 0);
|
|
574
|
-
const
|
|
575
|
-
if (freshnessGateMs > 0 &&
|
|
820
|
+
const newestKnown = Math.max(row.updatedAt || 0, row.lastHeartbeatAt || 0, row.createdAt || 0, jsonMtime, heartbeatMtime);
|
|
821
|
+
if (freshnessGateMs > 0 && newestKnown > 0 && now - newestKnown <= freshnessGateMs) {
|
|
576
822
|
remaining++;
|
|
577
823
|
continue;
|
|
578
824
|
}
|
|
579
|
-
const session = JSON.parse(readFileSync(jsonPath, 'utf-8'));
|
|
580
825
|
// Already-closed tombstones are handled here before heartbeat
|
|
581
826
|
// sidecar checks; they do not need liveness probing.
|
|
582
|
-
if (
|
|
827
|
+
if (row.closed === true || row.status === 'closed') {
|
|
583
828
|
if (sweepTombstones) {
|
|
584
|
-
const updated = Number(
|
|
829
|
+
const updated = Number(row.updatedAt);
|
|
585
830
|
const age = now - updated;
|
|
586
831
|
if (Number.isFinite(updated) && age >= tombstoneMaxAgeMs) {
|
|
587
832
|
try {
|
|
588
|
-
if (deleteSession(
|
|
833
|
+
if (deleteSession(row.id)) {
|
|
589
834
|
tombstonesCleaned++;
|
|
590
|
-
tombstoneDetails.push({ id:
|
|
835
|
+
tombstoneDetails.push({ id: row.id, ageSeconds: Math.floor(age / 1000) });
|
|
591
836
|
continue;
|
|
592
837
|
}
|
|
593
838
|
} catch (err) {
|
|
594
|
-
tombstoneErrors.push({ id:
|
|
839
|
+
tombstoneErrors.push({ id: row.id, message: err?.message || String(err) });
|
|
595
840
|
remaining++;
|
|
596
841
|
continue;
|
|
597
842
|
}
|
|
@@ -600,9 +845,9 @@ export function sweepStaleSessions(ttlMs, options = {}) {
|
|
|
600
845
|
remaining++;
|
|
601
846
|
continue;
|
|
602
847
|
}
|
|
603
|
-
// Sweep
|
|
848
|
+
// Sweep agent-owned and ownerless (legacy) sessions; skip explicit
|
|
604
849
|
// user sessions before touching heartbeat sidecars.
|
|
605
|
-
if (typeof
|
|
850
|
+
if (typeof row.owner === 'string' && row.owner.length > 0 && !isAgentOwner(row)) {
|
|
606
851
|
remaining++;
|
|
607
852
|
continue;
|
|
608
853
|
}
|
|
@@ -613,30 +858,30 @@ export function sweepStaleSessions(ttlMs, options = {}) {
|
|
|
613
858
|
// Prefer .hb sidecar mtime — updated at tight cadence (≤5s) without
|
|
614
859
|
// serialising the full JSON, so it reflects true liveness more
|
|
615
860
|
// accurately than the JSON timestamp fields.
|
|
616
|
-
let lastActive =
|
|
861
|
+
let lastActive = row.lastHeartbeatAt || row.updatedAt || row.createdAt || 0;
|
|
617
862
|
if (heartbeatMtime) lastActive = Math.max(lastActive, heartbeatMtime);
|
|
618
863
|
// Running sessions are normally reaped by the stream-watchdog
|
|
619
864
|
// within ~120s. Skip them here unless they've been silent past
|
|
620
865
|
// RUNNING_STALL_MS, at which point they are treated as zombies.
|
|
621
|
-
if (
|
|
866
|
+
if (row.status === 'running' && now - lastActive <= RUNNING_STALL_MS) {
|
|
622
867
|
remaining++;
|
|
623
868
|
continue;
|
|
624
869
|
}
|
|
625
|
-
const
|
|
626
|
-
&&
|
|
627
|
-
const sessionMaxAge =
|
|
870
|
+
const isCompletedAgent = isAgentOwner(row)
|
|
871
|
+
&& AGENT_TERMINAL_STATUSES.has(row.status);
|
|
872
|
+
const sessionMaxAge = isCompletedAgent ? AGENT_TERMINAL_TTL_MS : maxAge;
|
|
628
873
|
if (now - lastActive > sessionMaxAge) {
|
|
629
|
-
try { markSessionClosed(
|
|
874
|
+
try { markSessionClosed(row.id, 'idle-sweep'); }
|
|
630
875
|
catch (err) {
|
|
631
|
-
process.stderr.write(`[session-store] idle-sweep close failed for ${
|
|
876
|
+
process.stderr.write(`[session-store] idle-sweep close failed for ${row.id}: ${err?.message}\n`);
|
|
632
877
|
continue;
|
|
633
878
|
}
|
|
634
879
|
cleaned++;
|
|
635
880
|
details.push({
|
|
636
|
-
id:
|
|
637
|
-
owner:
|
|
881
|
+
id: row.id,
|
|
882
|
+
owner: row.owner || 'unknown',
|
|
638
883
|
idleMinutes: Math.round((now - lastActive) / 60000),
|
|
639
|
-
bashSessionId:
|
|
884
|
+
bashSessionId: row.implicitBashSessionId || null,
|
|
640
885
|
});
|
|
641
886
|
} else {
|
|
642
887
|
remaining++;
|
|
@@ -12,7 +12,7 @@ const TOOL_RESULT_SEARCH_THRESHOLD_CHARS = 50_000;
|
|
|
12
12
|
const TOOL_RESULT_GREP_THRESHOLD_CHARS = 20_000;
|
|
13
13
|
export const TOOL_RESULT_OFFLOAD_PREFIX = '[tool output offloaded:';
|
|
14
14
|
|
|
15
|
-
// Per-tool persistence limits mirror
|
|
15
|
+
// Per-tool persistence limits mirror reference per-tool maxResultSizeChars
|
|
16
16
|
// rather than a single global value: Grep persists at 20k (CC GrepTool), Glob
|
|
17
17
|
// and list/find_* at the 50k system default (CC DEFAULT_MAX_RESULT_SIZE_CHARS;
|
|
18
18
|
// tighter than CC Glob's 100k by design), and shell/bash_session/task at 30k
|
|
@@ -127,7 +127,7 @@ export async function maybeOffloadToolResult(sessionId, toolCallId, toolName, re
|
|
|
127
127
|
// tool-results/<sessionId>/*.txt entry, rmdirs the directory, and drops
|
|
128
128
|
// the sessionId entry from the module-level counter Map so a long-running
|
|
129
129
|
// mcp-server process doesn't accumulate per-session state across the
|
|
130
|
-
// lifetime of the
|
|
130
|
+
// lifetime of the agent runtime.
|
|
131
131
|
export async function clearOffloadSession(sessionId) {
|
|
132
132
|
if (!sessionId) return;
|
|
133
133
|
_offloadCounters.delete(sessionId);
|