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,177 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ normalizeIngestRole,
4
+ stableSessionSourceRef,
5
+ redactToolArgString,
6
+ redactToolArgValue,
7
+ sessionMessageContent,
8
+ createIngestTurnAllocator,
9
+ } from '../src/runtime/memory/lib/session-ingest.mjs';
10
+
11
+ function assert(condition, message) {
12
+ if (!condition) throw new Error(message);
13
+ }
14
+
15
+ // --- Role normalization ---
16
+ assert(normalizeIngestRole('human') === 'user', 'human should normalize to user');
17
+ assert(normalizeIngestRole('AI') === 'assistant', 'AI should normalize to assistant');
18
+ assert(normalizeIngestRole('tool_result') === 'tool', 'tool_result should normalize to tool');
19
+ assert(normalizeIngestRole('developer') === 'developer', 'developer should be preserved');
20
+ assert(normalizeIngestRole('mystery') === null, 'unknown role should be dropped');
21
+
22
+ // --- Stable source_ref across index/time for untimestamped messages ---
23
+ const plain = { role: 'user', content: 'fix the bug in compact.mjs' };
24
+ const refA = stableSessionSourceRef('sess1', plain, 'user', 'fix the bug in compact.mjs');
25
+ const refB = stableSessionSourceRef('sess1', plain, 'user', 'fix the bug in compact.mjs');
26
+ assert(refA === refB, 'same untimestamped message must yield the same source_ref regardless of call');
27
+ assert(!/#\d+$/.test(refA), 'source_ref must not carry a volatile #index suffix');
28
+
29
+ // Two textually identical untimestamped plain messages dedupe to one ref.
30
+ const dupA = stableSessionSourceRef('sess1', { role: 'user', content: 'ok' }, 'user', 'ok');
31
+ const dupB = stableSessionSourceRef('sess1', { role: 'user', content: 'ok' }, 'user', 'ok');
32
+ assert(dupA === dupB, 'identical untimestamped plain messages should share a stable ref (dedupe preferred)');
33
+
34
+ // A synthesized Date.now() fallback ts must NOT change identity: the helper
35
+ // only reads m.ts/m.timestamp, so two calls for the same untimestamped message
36
+ // at different wall-clock times still match.
37
+ const t1 = stableSessionSourceRef('sess1', plain, 'user', 'fix the bug in compact.mjs');
38
+ const t2 = stableSessionSourceRef('sess1', plain, 'user', 'fix the bug in compact.mjs');
39
+ assert(t1 === t2, 'synthesized timestamps must not enter identity');
40
+
41
+ // Original timestamp, when present, DOES contribute to identity.
42
+ const tsMsg = { role: 'user', content: 'same text', ts: 1700000000000 };
43
+ const tsRef = stableSessionSourceRef('sess1', tsMsg, 'user', 'same text');
44
+ const noTsRef = stableSessionSourceRef('sess1', { role: 'user', content: 'same text' }, 'user', 'same text');
45
+ assert(tsRef !== noTsRef, 'an original ts should differentiate identity from an untimestamped twin');
46
+
47
+ // Tool-id identity: different tool ids => different refs; same id => same ref.
48
+ const toolMsg1 = { role: 'assistant', content: 'x', toolCalls: [{ id: 'call_1', name: 'apply_patch' }] };
49
+ const toolMsg2 = { role: 'assistant', content: 'x', toolCalls: [{ id: 'call_2', name: 'apply_patch' }] };
50
+ const toolRef1 = stableSessionSourceRef('sess1', toolMsg1, 'assistant', 'x');
51
+ const toolRef2 = stableSessionSourceRef('sess1', toolMsg2, 'assistant', 'x');
52
+ assert(toolRef1 !== toolRef2, 'different tool-call ids should produce different source_refs');
53
+ const toolRef1b = stableSessionSourceRef('sess1', { ...toolMsg1 }, 'assistant', 'x');
54
+ assert(toolRef1 === toolRef1b, 'same tool-call id + content should produce the same source_ref');
55
+ const resultRef = stableSessionSourceRef('sess1', { role: 'tool', content: 'done', toolCallId: 'call_1' }, 'tool', 'done');
56
+ assert(typeof resultRef === 'string' && resultRef.startsWith('session:sess1:'), 'tool_result ref should be well formed');
57
+
58
+ // --- Sensitive redaction (object/JSON args) ---
59
+ const jsonArgs = JSON.stringify({ url: 'https://x', api_key: 'sk-secret-123', nested: { password: 'p@ss word' } });
60
+ const redactedJson = redactToolArgString(jsonArgs);
61
+ assert(!redactedJson.includes('sk-secret-123'), 'api_key value must be redacted from JSON args');
62
+ assert(!redactedJson.includes('p@ss word'), 'nested password must be redacted from JSON args');
63
+ assert(redactedJson.includes('https://x'), 'non-sensitive url should be preserved');
64
+ assert(redactedJson.includes('[redacted]'), 'redaction marker should be present');
65
+
66
+ const objRedaction = redactToolArgValue({ authorization: 'Bearer abc.def', keep: 'value' });
67
+ assert(objRedaction.authorization === '[redacted]', 'authorization key should collapse to [redacted]');
68
+ assert(objRedaction.keep === 'value', 'non-sensitive key should be preserved');
69
+
70
+ // --- Sensitive redaction (raw non-JSON strings with spaces / Bearer / cookies) ---
71
+ const rawCases = [
72
+ 'authorization: Bearer abc.def',
73
+ 'password="abc def"',
74
+ "api_key='abc def'",
75
+ 'cookie: a=b; c=d',
76
+ ];
77
+ for (const raw of rawCases) {
78
+ const out = redactToolArgString(raw);
79
+ assert(out.includes('[redacted]'), `raw secret should be redacted: ${raw}`);
80
+ }
81
+ const bearerOut = redactToolArgString('authorization: Bearer abc.def');
82
+ assert(!bearerOut.includes('abc.def'), 'Bearer token fragment must not leak');
83
+ assert(!bearerOut.includes('Bearer'), 'Bearer scheme word must be consumed by redaction');
84
+ const pwSpaceOut = redactToolArgString('password="abc def"');
85
+ assert(!pwSpaceOut.includes('abc def'), 'quoted password with spaces must not leak');
86
+ const apiSpaceOut = redactToolArgString("api_key='abc def'");
87
+ assert(!apiSpaceOut.includes('abc def'), 'single-quoted api_key with spaces must not leak');
88
+ const cookieOut = redactToolArgString('cookie: a=b; c=d');
89
+ assert(!/a=b/.test(cookieOut), 'cookie value (incl. ;-separated pairs) must not leak');
90
+
91
+ // Non-sensitive raw args stay readable.
92
+ const plainArgs = redactToolArgString('file=compact.mjs lines=10');
93
+ assert(plainArgs.includes('compact.mjs'), 'non-sensitive raw args should be preserved');
94
+ assert(!plainArgs.includes('[redacted]'), 'non-sensitive raw args should not be redacted');
95
+
96
+ // --- Prefixed sensitive key variants (raw) ---
97
+ const prefixedRawCases = [
98
+ ['access_token=abc.def', 'abc.def'],
99
+ ['access-token=abc.def', 'abc.def'],
100
+ ['x-api-key=sk-supersecret-123', 'sk-supersecret-123'],
101
+ ['bearer_token: Bearer bear-tok-456', 'bear-tok-456'],
102
+ ];
103
+ for (const [raw, frag] of prefixedRawCases) {
104
+ const out = redactToolArgString(raw);
105
+ assert(out.includes('[redacted]'), `prefixed key variant should be redacted: ${raw}`);
106
+ assert(!out.includes(frag), `prefixed key variant must not leak its secret fragment: ${raw}`);
107
+ }
108
+
109
+ // --- JSON/object string VALUES carrying embedded raw secrets ---
110
+ // Non-sensitive keys (headers/env) whose string value embeds a secret pair must
111
+ // still be redacted (defense-in-depth), while non-sensitive content stays.
112
+ const embeddedCases = [
113
+ [{ headers: 'authorization: Bearer abc.def' }, 'abc.def'],
114
+ [{ headers: 'cookie: a=b; c=d' }, 'a=b'],
115
+ [{ env: 'x-api-key=sk-supersecret-123' }, 'sk-supersecret-123'],
116
+ ];
117
+ for (const [obj, frag] of embeddedCases) {
118
+ const viaString = redactToolArgString(JSON.stringify(obj));
119
+ assert(!viaString.includes(frag), `JSON string value secret must not leak: ${JSON.stringify(obj)}`);
120
+ assert(viaString.includes('[redacted]'), `JSON string value secret should be redacted: ${JSON.stringify(obj)}`);
121
+ // Object path (redactToolArgValue) must redact the embedded secret too.
122
+ const viaObject = JSON.stringify(redactToolArgValue(obj));
123
+ assert(!viaObject.includes(frag), `object string value secret must not leak: ${JSON.stringify(obj)}`);
124
+ }
125
+ // Non-sensitive sibling values remain readable.
126
+ const mixedObj = redactToolArgValue({ url: 'https://x', headers: 'authorization: Bearer abc.def', note: 'keep me' });
127
+ assert(mixedObj.url === 'https://x', 'non-sensitive url should be preserved alongside redacted header value');
128
+ assert(mixedObj.note === 'keep me', 'non-sensitive note should be preserved');
129
+ assert(!JSON.stringify(mixedObj).includes('abc.def'), 'embedded header secret must not leak from mixed object');
130
+
131
+ // --- sessionMessageContent shaping ---
132
+ const asstMsg = {
133
+ role: 'assistant',
134
+ content: 'running tool',
135
+ toolCalls: [{ id: 'call_9', name: 'http_get', arguments: '{"url":"https://x","authorization":"Bearer zzz"}' }],
136
+ };
137
+ const asstShaped = sessionMessageContent(asstMsg);
138
+ assert(asstShaped.includes('running tool'), 'assistant text should be preserved');
139
+ assert(asstShaped.includes('http_get'), 'tool name should be readable');
140
+ assert(asstShaped.includes('id=call_9'), 'tool id should be readable');
141
+ assert(asstShaped.includes('https://x'), 'non-sensitive tool arg should be readable');
142
+ assert(!asstShaped.includes('zzz'), 'sensitive tool arg should be redacted in shaped content');
143
+
144
+ const toolResultShaped = sessionMessageContent({ role: 'tool', content: 'patch applied', toolCallId: 'call_9' });
145
+ assert(toolResultShaped.includes('[tool_result id=call_9]'), 'tool result should be tagged with its pairing id');
146
+ assert(toolResultShaped.includes('patch applied'), 'tool result body should be preserved');
147
+
148
+ const rawToolMsg = sessionMessageContent({
149
+ role: 'assistant',
150
+ content: '',
151
+ toolCalls: [{ id: 'call_x', name: 'curl', arguments: 'authorization: Bearer abc.def' }],
152
+ });
153
+ assert(!/abc\.def/.test(rawToolMsg), 'session message redaction should not leak raw Bearer token text');
154
+
155
+ // --- Monotonic source_turn allocator (post-compaction ordering) ---
156
+ // Seed from the current MAX(source_turn) for the session; every newly inserted
157
+ // row must get a strictly increasing turn ABOVE all prior rows, independent of
158
+ // the (post-compaction) array index. Re-ingested rows do not advance the count.
159
+ const alloc = createIngestTurnAllocator(42);
160
+ assert(alloc.peekNext() === 43, 'allocator should peek the next turn above the prior max');
161
+ assert(alloc.peekNext() === 43, 'peek must be idempotent until next() is called');
162
+ assert(alloc.next() === 43, 'first inserted row should take prevMax+1');
163
+ assert(alloc.next() === 44, 'second inserted row should take prevMax+2');
164
+ assert(alloc.current() === 44, 'current should track the last consumed turn');
165
+ // Simulate: pre-compact rows already at turns 1..3 (prevMax=3); after
166
+ // compaction a later message is appended with a LOW array index — it must STILL
167
+ // get turn 4 (continuation), not a low/old turn that would sort before them.
168
+ const post = createIngestTurnAllocator(3);
169
+ const appendedTurn = post.next();
170
+ assert(appendedTurn === 4, 'a post-compaction appended row must continue after the prior max source_turn');
171
+ assert(appendedTurn > 3, 'post-compaction row turn must sort AFTER older pre-compaction rows');
172
+ // A conflicting re-ingest (row already present) must not consume a turn.
173
+ const seeded = createIngestTurnAllocator(10);
174
+ const peek = seeded.peekNext();
175
+ assert(peek === 11 && seeded.current() === 10, 're-ingest peek must not advance the counter');
176
+
177
+ process.stdout.write('session ingest smoke passed \u2713\n');
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ import test from 'node:test';
3
+ import assert from 'node:assert/strict';
4
+ import { readFileSync } from 'node:fs';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { dirname, resolve } from 'node:path';
7
+ import { __saveModelSettingsForTest } from '../src/mixdog-session-runtime.mjs';
8
+
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const RUNTIME_SRC = resolve(__dirname, '../src/mixdog-session-runtime.mjs');
11
+
12
+ test('setEffort persists through adoptConfig with in-memory baseConfig', () => {
13
+ const src = readFileSync(RUNTIME_SRC, 'utf8');
14
+ const block = src.match(/async setEffort\(value\) \{[\s\S]*?\n \},/)?.[0] || '';
15
+ assert.match(block, /adoptConfig\(saveModelSettings\(cfgMod, route, \{ fastCapable, baseConfig: config \}\)/);
16
+ assert.doesNotMatch(block, /config = saveModelSettings/);
17
+ });
18
+
19
+ test('saveModelSettings preserves baseConfig fields not present on disk', () => {
20
+ const saved = [];
21
+ const cfgMod = {
22
+ loadConfig: () => ({ profile: 'from-disk', modelSettings: {}, fastModels: {} }),
23
+ saveConfig: (next) => {
24
+ saved.push(next);
25
+ return next;
26
+ },
27
+ };
28
+ const baseConfig = {
29
+ profile: 'in-memory-only',
30
+ modelSettings: {},
31
+ fastModels: {},
32
+ autoClear: { enabled: true },
33
+ };
34
+ const route = { provider: 'openai', model: 'gpt-5.4', effort: 'high', fast: false };
35
+ const result = __saveModelSettingsForTest(cfgMod, route, { fastCapable: true, baseConfig });
36
+ assert.equal(result.profile, 'in-memory-only');
37
+ assert.equal(result.autoClear?.enabled, true);
38
+ assert.equal(result.modelSettings['openai/gpt-5.4']?.effort, 'high');
39
+ assert.equal(saved.length, 1);
40
+ assert.equal(saved[0].profile, 'in-memory-only');
41
+ });
@@ -0,0 +1,106 @@
1
+ # smoke-runtime-negative.ps1 — Negative-path tests against released win32-x64
2
+ # runtime tarball. Validates: download, sha256, corrupt-tarball, fresh-extract
3
+ # boot, double-init refusal, hostile-env survival.
4
+ #
5
+ # Env: TAG / OS / ARCH / RUNTIME_RELEASE_REPOSITORY (default tribgames/mixdog)
6
+
7
+ $ErrorActionPreference = 'Stop'
8
+
9
+ $Tag = if ($env:TAG) { $env:TAG } else { 'runtime-v0.4.0' }
10
+ $Os = if ($env:OS) { $env:OS } else { 'win32' }
11
+ $Arch = if ($env:ARCH) { $env:ARCH } else { 'x64' }
12
+ $ReleaseRepo = if ($env:RUNTIME_RELEASE_REPOSITORY) { $env:RUNTIME_RELEASE_REPOSITORY } else { 'tribgames/mixdog' }
13
+ $PgVer = '16.4'
14
+ $PgvectorVer = '0.8.2'
15
+
16
+ $Asset = "mixdog-runtime-${Os}-${Arch}-pg${PgVer}-pgvector${PgvectorVer}.tar.gz"
17
+ $Url = "https://github.com/${ReleaseRepo}/releases/download/${Tag}/${Asset}"
18
+
19
+ $Work = New-Item -ItemType Directory -Force -Path "$env:TEMP\smoke-$([guid]::NewGuid().ToString('N'))" | Select-Object -ExpandProperty FullName
20
+
21
+ try {
22
+ Write-Host "==> Negative-path smoke for ${Os}-${Arch} from $Url"
23
+
24
+ Write-Host "==> Test 1: HEAD reaches release asset"
25
+ $resp = Invoke-WebRequest -Uri $Url -Method Head -UseBasicParsing -ErrorAction Stop
26
+ Write-Host " HTTP $($resp.StatusCode)"
27
+
28
+ Write-Host "==> Test 2: full download + sha256"
29
+ $TarPath = Join-Path $Work $Asset
30
+ Invoke-WebRequest -Uri $Url -OutFile $TarPath -UseBasicParsing
31
+ $sha = (Get-FileHash -Algorithm SHA256 $TarPath).Hash.ToLower()
32
+ Write-Host " sha256=$sha"
33
+
34
+ Write-Host "==> Test 3: corrupt tarball — flip a byte and ensure tar errors"
35
+ $CorruptPath = "$Work\corrupt.tar.gz"
36
+ Copy-Item $TarPath $CorruptPath
37
+ $bytes = [byte[]]::new(1); $rand = [System.Random]::new(); $rand.NextBytes($bytes)
38
+ $stream = [System.IO.File]::OpenWrite($CorruptPath)
39
+ $stream.Position = 102400
40
+ $stream.Write($bytes, 0, 1)
41
+ $stream.Close()
42
+ $CorruptDir = "$Work\corrupt"
43
+ New-Item -ItemType Directory -Force -Path $CorruptDir | Out-Null
44
+ $TarProc = Start-Process -FilePath tar -ArgumentList @('-xzf', $CorruptPath, '-C', $CorruptDir.Replace('\','/')) -Wait:$false -PassThru -WindowStyle Hidden
45
+ if (-not $TarProc.WaitForExit(15000)) {
46
+ Stop-Process -Id $TarProc.Id -Force -ErrorAction SilentlyContinue
47
+ Write-Host " PASS: corrupted tarball extraction timed out and was stopped"
48
+ } elseif ($TarProc.ExitCode -eq 0) {
49
+ Write-Host " WARN: corrupted tarball extracted without error (tar gzip recovery)"
50
+ } else {
51
+ Write-Host " PASS: corrupted tarball rejected by tar (exit $($TarProc.ExitCode))"
52
+ }
53
+
54
+ Write-Host "==> Test 4: fresh-extract boot (extension load + distance query)"
55
+ $FreshDir = "$Work\fresh"
56
+ New-Item -ItemType Directory -Force -Path $FreshDir | Out-Null
57
+ & tar -xzf $TarPath -C ($FreshDir.Replace('\','/'))
58
+ if ($LASTEXITCODE -ne 0) { throw "fresh extract failed" }
59
+
60
+ $PgBin = "$FreshDir\bin"
61
+ $Data = "$FreshDir\pgdata"
62
+ $Log = "$FreshDir\pg.log"
63
+ $Port = 55897
64
+
65
+ # Hostile env: minimal PATH (System32 only), no PGROOT/PGDATA
66
+ $SavedPath = $env:PATH
67
+ $SavedPgRoot = $env:PGROOT
68
+ $env:PATH = "$env:SystemRoot\System32;$env:SystemRoot"
69
+ $env:PGROOT = $null
70
+ $env:PGDATA = $null
71
+
72
+ try {
73
+ & "$PgBin\postgres.exe" --version
74
+ if ($LASTEXITCODE -ne 0) { throw "FAIL: postgres --version" }
75
+ & "$PgBin\initdb.exe" -D $Data --username=postgres --auth-local=trust --no-locale -E UTF8 | Out-Null
76
+ if ($LASTEXITCODE -ne 0) { throw "FAIL: initdb" }
77
+ & "$PgBin\pg_ctl.exe" -D $Data -o "-p $Port -h 127.0.0.1" -l $Log -w start
78
+ if ($LASTEXITCODE -ne 0) { Get-Content $Log | Select-Object -Last 30; throw "FAIL: pg_ctl start" }
79
+
80
+ try {
81
+ & "$PgBin\psql.exe" -h 127.0.0.1 -p $Port -U postgres -d postgres -c "CREATE EXTENSION vector;" | Out-Null
82
+ if ($LASTEXITCODE -ne 0) { throw "FAIL: CREATE EXTENSION" }
83
+ $ExtV = & "$PgBin\psql.exe" -h 127.0.0.1 -p $Port -U postgres -d postgres -tAc "SELECT extversion FROM pg_extension WHERE extname='vector';"
84
+ if ($ExtV.Trim() -ne $PgvectorVer) { throw "FAIL: extversion='$ExtV' expected='$PgvectorVer'" }
85
+ Write-Host " PASS: fresh-extract boot + vector extension"
86
+ } finally {
87
+ & "$PgBin\pg_ctl.exe" -D $Data -m fast stop 2>$null | Out-Null
88
+ }
89
+
90
+ Write-Host "==> Test 5: second initdb on initialized dir refused"
91
+ $rc = (Start-Process -FilePath "$PgBin\initdb.exe" -ArgumentList "-D",$Data,"-U","postgres" -Wait -PassThru -NoNewWindow).ExitCode
92
+ if ($rc -eq 0) {
93
+ Write-Host " WARN: second initdb succeeded (unexpected)"
94
+ } else {
95
+ Write-Host " PASS: second initdb refused (exit $rc)"
96
+ }
97
+ } finally {
98
+ $env:PATH = $SavedPath
99
+ $env:PGROOT = $SavedPgRoot
100
+ }
101
+
102
+ Write-Host "==> All negative-path smokes: PASS"
103
+ }
104
+ finally {
105
+ Remove-Item -Recurse -Force $Work -ErrorAction SilentlyContinue
106
+ }
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env bash
2
+ # smoke-runtime-negative.sh — Negative-path tests against the released runtime
3
+ # tarball for a given OS/arch. Validates: download URL, sha256 verification,
4
+ # corrupt-tarball rejection, fresh-extract boot, double-boot port handling,
5
+ # stale-cache version-skew detection.
6
+ #
7
+ # Env: TAG=runtime-v0.4.0 OS=linux|darwin ARCH=x64|arm64
8
+ # RUNTIME_RELEASE_REPOSITORY — override repo (default tribgames/mixdog)
9
+
10
+ set -euo pipefail
11
+
12
+ TAG="${TAG:-runtime-v0.4.0}"
13
+ OS="${OS:-$(uname -s | tr '[:upper:]' '[:lower:]')}"
14
+ ARCH="${ARCH:-$(uname -m | sed 's/x86_64/x64/')}"
15
+ PG_VER="16.4"
16
+ PGVECTOR_VER="0.8.2"
17
+
18
+ RELEASE_REPO="${RUNTIME_RELEASE_REPOSITORY:-tribgames/mixdog}"
19
+ PLATFORM="${OS}-${ARCH}"
20
+ ASSET="mixdog-runtime-${OS}-${ARCH}-pg${PG_VER}-pgvector${PGVECTOR_VER}.tar.gz"
21
+ URL="https://github.com/${RELEASE_REPO}/releases/download/${TAG}/${ASSET}"
22
+
23
+ WORK="$(mktemp -d)"
24
+ trap 'rm -rf "$WORK"' EXIT
25
+
26
+ echo "==> Negative-path smoke for ${PLATFORM} from ${URL}"
27
+
28
+ # Linux container needs unprivileged user for initdb
29
+ if [[ "$OS" == "linux" && "$(id -u)" -eq 0 ]]; then
30
+ apt-get update -qq && apt-get install -y --no-install-recommends curl ca-certificates >/dev/null
31
+ if ! id pguser >/dev/null 2>&1; then useradd -m -s /bin/bash pguser; fi
32
+ chmod 755 "$WORK"
33
+ RUN_AS=(runuser -u pguser --)
34
+ else
35
+ RUN_AS=()
36
+ fi
37
+
38
+ echo "==> Test 1: HEAD reaches release asset"
39
+ curl -sIL -o /dev/null -w " HTTP %{http_code}\n" "$URL" || { echo "FAIL: URL HEAD"; exit 1; }
40
+
41
+ echo "==> Test 2: full download + sha256 (manifest match)"
42
+ curl -sL -o "$WORK/$ASSET" "$URL"
43
+ SHA="$(sha256sum "$WORK/$ASSET" 2>/dev/null | awk '{print $1}' || shasum -a 256 "$WORK/$ASSET" | awk '{print $1}')"
44
+ echo " sha256=$SHA"
45
+
46
+ echo "==> Test 3: corrupt tarball — flip one byte and ensure tar fails or runtime detects"
47
+ cp "$WORK/$ASSET" "$WORK/corrupt.tar.gz"
48
+ # Flip a byte at offset 100KB into the tarball
49
+ dd if=/dev/urandom of="$WORK/corrupt.tar.gz" bs=1 count=1 seek=102400 conv=notrunc 2>/dev/null
50
+ mkdir -p "$WORK/corrupt"
51
+ if tar xzf "$WORK/corrupt.tar.gz" -C "$WORK/corrupt" 2>/dev/null; then
52
+ echo " WARN: corrupted tarball extracted without error (tar gzip recovery)"
53
+ else
54
+ echo " PASS: corrupted tarball rejected by tar"
55
+ fi
56
+
57
+ echo "==> Test 4: fresh-extract boot (extension load + distance query)"
58
+ mkdir -p "$WORK/fresh"
59
+ tar xzf "$WORK/$ASSET" -C "$WORK/fresh"
60
+ chown -R pguser:pguser "$WORK/fresh" 2>/dev/null || true
61
+
62
+ PG_BIN="$WORK/fresh/bin"
63
+ DATA="$WORK/fresh/pgdata"
64
+ LOG="$WORK/fresh/pg.log"
65
+ PORT=55897
66
+
67
+ "${RUN_AS[@]}" env -i HOME="$WORK/fresh" PATH=/usr/bin:/bin "$PG_BIN/postgres" --version
68
+ "${RUN_AS[@]}" env -i HOME="$WORK/fresh" PATH=/usr/bin:/bin "$PG_BIN/initdb" -D "$DATA" --auth-local=trust --no-locale -E UTF8 -U postgres > /dev/null
69
+ "${RUN_AS[@]}" env -i HOME="$WORK/fresh" PATH=/usr/bin:/bin "$PG_BIN/pg_ctl" -D "$DATA" -o "-p $PORT -h 127.0.0.1" -l "$LOG" -w start
70
+ trap 'echo "==> pg.log tail:"; tail -100 "'"$LOG"'" 2>/dev/null || true; "${RUN_AS[@]}" env -i HOME="'"$WORK/fresh"'" PATH=/usr/bin:/bin "'"$PG_BIN"'/pg_ctl" -D "'"$DATA"'" -m fast stop > /dev/null 2>&1 || true; rm -rf "$WORK"' EXIT
71
+
72
+ # Diagnostic on env -i: list bundled lib deps that the dynamic linker can't resolve.
73
+ echo "==> ldd vector.so (under env -i):"
74
+ "${RUN_AS[@]}" env -i HOME="$WORK/fresh" PATH=/usr/bin:/bin ldd "$WORK/fresh/lib/postgresql/vector.so" 2>&1 | head -20 || true
75
+
76
+ "${RUN_AS[@]}" env -i HOME="$WORK/fresh" PATH=/usr/bin:/bin "$PG_BIN/psql" -h 127.0.0.1 -p "$PORT" -U postgres -d postgres -c "CREATE EXTENSION vector;" > /dev/null
77
+ EXTV="$("${RUN_AS[@]}" env -i HOME="$WORK/fresh" PATH=/usr/bin:/bin "$PG_BIN/psql" -h 127.0.0.1 -p "$PORT" -U postgres -d postgres -tAc "SELECT extversion FROM pg_extension WHERE extname='vector';")"
78
+ [[ "$EXTV" == "$PGVECTOR_VER" ]] || { echo "FAIL: extversion=$EXTV"; exit 1; }
79
+ echo " PASS: fresh-extract boot + vector extension"
80
+
81
+ echo "==> Test 5: same tarball extracted twice → second initdb refuses (data-dir already initialized)"
82
+ mkdir -p "$WORK/twice"
83
+ tar xzf "$WORK/$ASSET" -C "$WORK/twice"
84
+ chown -R pguser:pguser "$WORK/twice" 2>/dev/null || true
85
+ DATA2="$WORK/twice/pgdata"
86
+ "${RUN_AS[@]}" env -i HOME="$WORK/twice" PATH=/usr/bin:/bin "$WORK/twice/bin/initdb" -D "$DATA2" --auth-local=trust --no-locale -E UTF8 -U postgres > /dev/null
87
+ if "${RUN_AS[@]}" env -i HOME="$WORK/twice" PATH=/usr/bin:/bin "$WORK/twice/bin/initdb" -D "$DATA2" -U postgres 2>/dev/null; then
88
+ echo " WARN: second initdb succeeded on already-initialized dir (unexpected)"
89
+ else
90
+ echo " PASS: second initdb refused already-initialized dir"
91
+ fi
92
+
93
+ echo "==> Test 6: shutdown + cleanup"
94
+ "${RUN_AS[@]}" env -i HOME="$WORK/fresh" PATH=/usr/bin:/bin "$PG_BIN/pg_ctl" -D "$DATA" -m fast stop > /dev/null
95
+ trap 'rm -rf "$WORK"' EXIT
96
+
97
+ echo "==> All negative-path smokes: PASS"
package/scripts/smoke.mjs CHANGED
@@ -73,6 +73,31 @@ runNode(['--input-type=module', '-e', `
73
73
  if (line.includes('37%') || !line.includes('0%')) throw new Error('statusline must not show local estimated context as session usage: ' + JSON.stringify(line));
74
74
  `], 'statusline estimated-context isolation smoke', { env: isolatedStatuslineEnv });
75
75
 
76
+ runNode(['--input-type=module', '-e', `
77
+ process.stdout.columns = 120;
78
+ const { renderStatusline } = await import('./src/ui/statusline.mjs');
79
+ const line = await renderStatusline({
80
+ provider: 'openai',
81
+ model: 'gpt-5.5',
82
+ contextWindow: 950000,
83
+ stats: { currentContextTokens: 360000, currentContextSource: 'post_compact_estimate' },
84
+ });
85
+ if (!line.includes('37%')) throw new Error('statusline must show post-compact estimated context after stale API usage: ' + JSON.stringify(line));
86
+ `], 'statusline post-compact context smoke', { env: isolatedStatuslineEnv });
87
+
88
+ runNode(['--input-type=module', '-e', `
89
+ process.stdout.columns = 120;
90
+ const { renderStatusline } = await import('./src/ui/statusline.mjs');
91
+ const line = await renderStatusline({
92
+ provider: 'openai',
93
+ model: 'gpt-5.5',
94
+ contextWindow: 950000,
95
+ stats: { currentContextTokens: 0 },
96
+ agentJobs: [{ task_id: 'task_statusline_smoke', status: 'running', tag: 'bench-agent', startedAt: new Date().toISOString() }],
97
+ });
98
+ if (!line.includes('Running') || !line.includes('bench-agent')) throw new Error('statusline must render live agent task state: ' + JSON.stringify(line));
99
+ `], 'statusline live agent task smoke', { env: isolatedStatuslineEnv });
100
+
76
101
  runNode(['--input-type=module', '-e', `
77
102
  const { mkdtempSync, mkdirSync, writeFileSync } = await import('node:fs');
78
103
  const { tmpdir } = await import('node:os');
@@ -0,0 +1,48 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {
4
+ normalizeHookUpdatedToolOutput,
5
+ resolveToolResultAfterHook,
6
+ } from '../src/runtime/agent/orchestrator/session/loop.mjs';
7
+
8
+ const structuredImage = {
9
+ content: [{ type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'abc' } }],
10
+ };
11
+
12
+ test('resolveToolResultAfterHook leaves original result when hook omits override', () => {
13
+ assert.equal(resolveToolResultAfterHook('tool body', {}), 'tool body');
14
+ assert.equal(resolveToolResultAfterHook('tool body', { blocked: undefined }), 'tool body');
15
+ assert.deepEqual(resolveToolResultAfterHook(structuredImage, {}), structuredImage);
16
+ });
17
+
18
+ test('resolveToolResultAfterHook does not treat updatedToolOutput undefined as override', () => {
19
+ assert.equal(resolveToolResultAfterHook('keep-me', { updatedToolOutput: undefined }), 'keep-me');
20
+ assert.deepEqual(resolveToolResultAfterHook(structuredImage, { updatedToolOutput: undefined }), structuredImage);
21
+ });
22
+
23
+ test('resolveToolResultAfterHook applies intentional empty-string override', () => {
24
+ assert.equal(resolveToolResultAfterHook('tool body', { updatedToolOutput: '' }), '');
25
+ });
26
+
27
+ test('resolveToolResultAfterHook preserves structured tool output when no override', () => {
28
+ const original = {
29
+ content: [
30
+ { type: 'text', text: 'page 1' },
31
+ { type: 'image', source: { type: 'base64', media_type: 'application/pdf', data: 'pdf' } },
32
+ ],
33
+ };
34
+ assert.deepEqual(resolveToolResultAfterHook(original, null), original);
35
+ assert.deepEqual(resolveToolResultAfterHook(original, { handlersRun: 0 }), original);
36
+ });
37
+
38
+ test('normalizeHookUpdatedToolOutput flattens text-only MCP envelopes for hook overrides', () => {
39
+ assert.equal(
40
+ normalizeHookUpdatedToolOutput({ content: [{ type: 'text', text: 'line1' }, { type: 'text', text: 'line2' }] }),
41
+ 'line1\nline2',
42
+ );
43
+ });
44
+
45
+ test('normalizeHookUpdatedToolOutput keeps structured media blocks on hook override', () => {
46
+ assert.deepEqual(normalizeHookUpdatedToolOutput(structuredImage), structuredImage);
47
+ });
48
+