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.
Files changed (285) hide show
  1. package/README.md +47 -23
  2. package/package.json +33 -27
  3. package/scripts/_test-folder-dialog.mjs +30 -0
  4. package/scripts/agent-parallel-smoke.mjs +388 -0
  5. package/scripts/agent-tag-reuse-smoke.mjs +183 -0
  6. package/scripts/background-task-meta-smoke.mjs +38 -0
  7. package/scripts/boot-smoke.mjs +52 -9
  8. package/scripts/build-runtime-linux.sh +348 -0
  9. package/scripts/build-runtime-macos.sh +217 -0
  10. package/scripts/build-runtime-windows.ps1 +242 -0
  11. package/scripts/compact-active-turn-test.mjs +68 -0
  12. package/scripts/compact-smoke.mjs +859 -129
  13. package/scripts/compact-trigger-migration-smoke.mjs +187 -0
  14. package/scripts/fix-brief-fn.mjs +35 -0
  15. package/scripts/fix-format-tool-surface.mjs +24 -0
  16. package/scripts/fix-tool-exec-visible.mjs +42 -0
  17. package/scripts/generate-runtime-manifest.mjs +166 -0
  18. package/scripts/hook-bus-test.mjs +330 -0
  19. package/scripts/lead-workflow-smoke.mjs +33 -39
  20. package/scripts/live-worker-smoke.mjs +43 -37
  21. package/scripts/llm-trace-summary.mjs +315 -0
  22. package/scripts/memory-meta-concurrency-test.mjs +20 -0
  23. package/scripts/output-style-smoke.mjs +56 -15
  24. package/scripts/parent-abort-link-test.mjs +44 -0
  25. package/scripts/patch-agent-brief.mjs +48 -0
  26. package/scripts/patch-app.mjs +21 -0
  27. package/scripts/patch-app2.mjs +18 -0
  28. package/scripts/patch-dist-brief.mjs +96 -0
  29. package/scripts/patch-tool-exec.mjs +70 -0
  30. package/scripts/pretool-ask-runtime-test.mjs +54 -0
  31. package/scripts/provider-toolcall-test.mjs +376 -0
  32. package/scripts/reactive-compact-persist-smoke.mjs +124 -0
  33. package/scripts/sanitize-tool-pairs-test.mjs +260 -0
  34. package/scripts/session-context-bench.mjs +344 -0
  35. package/scripts/session-ingest-smoke.mjs +177 -0
  36. package/scripts/set-effort-config-test.mjs +41 -0
  37. package/scripts/smoke-runtime-negative.ps1 +106 -0
  38. package/scripts/smoke-runtime-negative.sh +97 -0
  39. package/scripts/smoke.mjs +25 -0
  40. package/scripts/tool-result-hook-test.mjs +48 -0
  41. package/scripts/tool-smoke.mjs +1223 -95
  42. package/scripts/toolcall-args-test.mjs +150 -0
  43. package/scripts/tui-background-failure-smoke.mjs +73 -0
  44. package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
  45. package/src/agents/debugger/AGENT.md +8 -0
  46. package/src/agents/explore/AGENT.md +4 -0
  47. package/src/agents/heavy-worker/AGENT.md +9 -3
  48. package/src/agents/maintainer/AGENT.md +4 -0
  49. package/src/agents/reviewer/AGENT.md +8 -0
  50. package/src/agents/scheduler-task/AGENT.md +12 -0
  51. package/src/agents/scheduler-task/agent.json +6 -0
  52. package/src/agents/webhook-handler/AGENT.md +12 -0
  53. package/src/agents/webhook-handler/agent.json +6 -0
  54. package/src/agents/worker/AGENT.md +9 -3
  55. package/src/app.mjs +77 -3
  56. package/src/defaults/hidden-roles.json +17 -12
  57. package/src/headless-role.mjs +117 -0
  58. package/src/help.mjs +30 -0
  59. package/src/hooks/lib/permission-evaluator.cjs +11 -475
  60. package/src/lib/keychain-cjs.cjs +9 -1
  61. package/src/lib/mixdog-debug.cjs +0 -7
  62. package/src/lib/rules-builder.cjs +240 -96
  63. package/src/lib/text-utils.cjs +1 -1
  64. package/src/mixdog-session-runtime.mjs +2325 -450
  65. package/src/output-styles/default.md +12 -28
  66. package/src/output-styles/extreme-simple.md +9 -6
  67. package/src/output-styles/simple.md +22 -9
  68. package/src/repl.mjs +118 -59
  69. package/src/rules/agent/00-common.md +15 -0
  70. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  71. package/src/rules/agent/30-explorer.md +22 -0
  72. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  73. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  75. package/src/rules/lead/01-general.md +9 -5
  76. package/src/rules/lead/04-workflow.md +51 -12
  77. package/src/rules/lead/lead-tool.md +6 -0
  78. package/src/rules/shared/01-tool.md +12 -1
  79. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  80. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  81. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  82. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  83. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  85. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  86. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  87. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  88. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  89. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
  91. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  92. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  93. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  94. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
  95. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
  96. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  97. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  98. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  99. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  100. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  101. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  102. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  103. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  104. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  106. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
  107. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
  109. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  110. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  111. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
  112. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  113. package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +4 -4
  114. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
  115. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  116. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  117. package/src/runtime/agent/orchestrator/session/loop.mjs +851 -674
  118. package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
  119. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  120. package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
  121. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  122. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  123. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  124. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
  125. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
  126. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
  127. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
  128. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  129. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  130. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  131. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  132. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  133. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  137. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  138. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  139. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  140. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  141. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  142. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  143. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  144. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +1 -1
  145. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  146. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  147. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  148. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  149. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  150. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  151. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  152. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  153. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  154. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  155. package/src/runtime/channels/index.mjs +152 -24
  156. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  157. package/src/runtime/channels/lib/session-discovery.mjs +3 -2
  158. package/src/runtime/channels/lib/tool-format.mjs +0 -2
  159. package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
  160. package/src/runtime/channels/lib/webhook.mjs +1 -1
  161. package/src/runtime/channels/tool-defs.mjs +29 -29
  162. package/src/runtime/memory/index.mjs +635 -107
  163. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  164. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  165. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  166. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  167. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  168. package/src/runtime/memory/lib/memory-cycle1.mjs +38 -17
  169. package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
  170. package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
  171. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
  172. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  173. package/src/runtime/memory/lib/memory.mjs +88 -9
  174. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  175. package/src/runtime/memory/lib/pg/adapter.mjs +15 -1
  176. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  177. package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
  178. package/src/runtime/memory/lib/session-ingest.mjs +194 -0
  179. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  180. package/src/runtime/memory/tool-defs.mjs +46 -37
  181. package/src/runtime/search/index.mjs +102 -466
  182. package/src/runtime/search/lib/web-tools.mjs +45 -25
  183. package/src/runtime/search/tool-defs.mjs +16 -23
  184. package/src/runtime/shared/abort-controller.mjs +1 -1
  185. package/src/runtime/shared/atomic-file.mjs +4 -3
  186. package/src/runtime/shared/background-tasks.mjs +122 -11
  187. package/src/runtime/shared/child-spawn-gate.mjs +145 -0
  188. package/src/runtime/shared/config.mjs +7 -4
  189. package/src/runtime/shared/err-text.mjs +131 -4
  190. package/src/runtime/shared/llm/cost.mjs +2 -2
  191. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  192. package/src/runtime/shared/llm/index.mjs +34 -11
  193. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  194. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  195. package/src/runtime/shared/singleton-owner.mjs +104 -0
  196. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  197. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  198. package/src/runtime/shared/tool-surface.mjs +624 -98
  199. package/src/runtime/shared/user-data-guard.mjs +0 -2
  200. package/src/standalone/agent-task-status.mjs +203 -0
  201. package/src/standalone/agent-task-status.test.mjs +76 -0
  202. package/src/standalone/agent-tool.mjs +1913 -0
  203. package/src/standalone/channel-worker.mjs +370 -14
  204. package/src/standalone/explore-tool.mjs +165 -70
  205. package/src/standalone/folder-dialog.mjs +314 -0
  206. package/src/standalone/hook-bus.mjs +898 -22
  207. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  208. package/src/standalone/projects.mjs +226 -0
  209. package/src/standalone/provider-admin.mjs +41 -24
  210. package/src/standalone/seeds.mjs +2 -69
  211. package/src/standalone/usage-dashboard.mjs +96 -8
  212. package/src/tui/App.jsx +4798 -2153
  213. package/src/tui/components/AnsiText.jsx +39 -28
  214. package/src/tui/components/ContextPanel.jsx +87 -29
  215. package/src/tui/components/Markdown.jsx +43 -77
  216. package/src/tui/components/MarkdownTable.jsx +9 -184
  217. package/src/tui/components/Message.jsx +28 -11
  218. package/src/tui/components/Picker.jsx +95 -56
  219. package/src/tui/components/PromptInput.jsx +367 -239
  220. package/src/tui/components/QueuedCommands.jsx +1 -1
  221. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  222. package/src/tui/components/Spinner.jsx +67 -38
  223. package/src/tui/components/StatusLine.jsx +606 -38
  224. package/src/tui/components/TextEntryPanel.jsx +128 -9
  225. package/src/tui/components/ToolExecution.jsx +617 -368
  226. package/src/tui/components/TurnDone.jsx +3 -3
  227. package/src/tui/components/UsagePanel.jsx +3 -5
  228. package/src/tui/components/tool-output-format.mjs +365 -0
  229. package/src/tui/components/tool-output-format.test.mjs +220 -0
  230. package/src/tui/dist/index.mjs +8915 -2418
  231. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  232. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  233. package/src/tui/engine.mjs +1455 -279
  234. package/src/tui/figures.mjs +21 -40
  235. package/src/tui/index.jsx +75 -31
  236. package/src/tui/input-editing.mjs +25 -0
  237. package/src/tui/markdown/format-token.mjs +511 -68
  238. package/src/tui/markdown/format-token.test.mjs +216 -0
  239. package/src/tui/markdown/render-ansi.mjs +94 -0
  240. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  241. package/src/tui/markdown/stream-fence.mjs +34 -0
  242. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  243. package/src/tui/markdown/table-layout.mjs +250 -0
  244. package/src/tui/paste-attachments.mjs +0 -7
  245. package/src/tui/spinner-verbs.mjs +1 -2
  246. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  247. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  248. package/src/tui/theme.mjs +746 -24
  249. package/src/tui/time-format.mjs +1 -1
  250. package/src/tui/transcript-tool-failures.mjs +67 -0
  251. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  252. package/src/ui/ansi.mjs +1 -2
  253. package/src/ui/markdown.mjs +85 -26
  254. package/src/ui/markdown.test.mjs +70 -0
  255. package/src/ui/model-display.mjs +121 -0
  256. package/src/ui/session-stats.mjs +44 -0
  257. package/src/ui/statusline-context-label.test.mjs +15 -0
  258. package/src/ui/statusline.mjs +386 -178
  259. package/src/ui/tool-card.mjs +3 -16
  260. package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
  261. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  262. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  263. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  264. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  265. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  266. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  267. package/src/workflows/default/WORKFLOW.md +12 -5
  268. package/src/workflows/default/workflow.json +0 -1
  269. package/src/workflows/solo/WORKFLOW.md +15 -0
  270. package/src/workflows/solo/workflow.json +7 -0
  271. package/vendor/ink/build/output.js +6 -1
  272. package/src/agents/scheduler-task.md +0 -3
  273. package/src/agents/web-researcher/AGENT.md +0 -3
  274. package/src/agents/web-researcher/agent.json +0 -6
  275. package/src/agents/webhook-handler.md +0 -3
  276. package/src/rules/bridge/00-common.md +0 -5
  277. package/src/rules/bridge/30-explorer.md +0 -4
  278. package/src/rules/lead/00-tool-lead.md +0 -5
  279. package/src/rules/shared/00-language.md +0 -3
  280. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  281. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  282. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  283. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  284. package/src/standalone/bridge-tool.mjs +0 -1414
  285. 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 { renameWithRetrySync } from '../../../shared/atomic-file.mjs';
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 onDisk = JSON.parse(readFileSync(target, 'utf-8'));
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 bridge agents (idle/done/error) live until terminal reap must
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
- // bridge-stall-watchdog.mjs so the store sweep is the durable 1h reaper.
497
- const BRIDGE_TERMINAL_TTL_MS = 60 * 60 * 1000;
498
- const BRIDGE_TERMINAL_STATUSES = new Set(['idle', 'done', 'error']);
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 files = readdirSync(dir).filter(f => f.endsWith('.json'));
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 f of files) {
804
+ for (const row of summaries) {
564
805
  try {
565
- const jsonPath = join(dir, f);
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, f.replace(/\.json$/, '.hb'));
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 newestMtime = Math.max(jsonMtime, heartbeatMtime);
575
- if (freshnessGateMs > 0 && newestMtime > 0 && now - newestMtime <= freshnessGateMs) {
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 (session.closed === true || session.status === 'closed') {
827
+ if (row.closed === true || row.status === 'closed') {
583
828
  if (sweepTombstones) {
584
- const updated = Number(session.updatedAt);
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(session.id)) {
833
+ if (deleteSession(row.id)) {
589
834
  tombstonesCleaned++;
590
- tombstoneDetails.push({ id: session.id, ageSeconds: Math.floor(age / 1000) });
835
+ tombstoneDetails.push({ id: row.id, ageSeconds: Math.floor(age / 1000) });
591
836
  continue;
592
837
  }
593
838
  } catch (err) {
594
- tombstoneErrors.push({ id: session.id, message: err?.message || String(err) });
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 bridge-owned and ownerless (legacy) sessions; skip explicit
848
+ // Sweep agent-owned and ownerless (legacy) sessions; skip explicit
604
849
  // user sessions before touching heartbeat sidecars.
605
- if (typeof session.owner === 'string' && session.owner.length > 0 && session.owner !== 'bridge') {
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 = session.lastHeartbeatAt || session.updatedAt || session.createdAt || 0;
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 (session.status === 'running' && now - lastActive <= RUNNING_STALL_MS) {
866
+ if (row.status === 'running' && now - lastActive <= RUNNING_STALL_MS) {
622
867
  remaining++;
623
868
  continue;
624
869
  }
625
- const isCompletedBridge = session.owner === 'bridge'
626
- && BRIDGE_TERMINAL_STATUSES.has(session.status);
627
- const sessionMaxAge = isCompletedBridge ? BRIDGE_TERMINAL_TTL_MS : maxAge;
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(session.id, 'idle-sweep'); }
874
+ try { markSessionClosed(row.id, 'idle-sweep'); }
630
875
  catch (err) {
631
- process.stderr.write(`[session-store] idle-sweep close failed for ${session.id}: ${err?.message}\n`);
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: session.id,
637
- owner: session.owner || 'unknown',
881
+ id: row.id,
882
+ owner: row.owner || 'unknown',
638
883
  idleMinutes: Math.round((now - lastActive) / 60000),
639
- bashSessionId: session.implicitBashSessionId || null,
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 Claude Code's per-tool maxResultSizeChars
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 bridge.
130
+ // lifetime of the agent runtime.
131
131
  export async function clearOffloadSession(sessionId) {
132
132
  if (!sessionId) return;
133
133
  _offloadCounters.delete(sessionId);