mixdog 0.8.1 → 0.9.1

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 (299) hide show
  1. package/README.md +47 -23
  2. package/package.json +34 -28
  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 +205 -33
  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 +1222 -90
  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 -29
  62. package/src/lib/plugin-paths.cjs +1 -7
  63. package/src/lib/rules-builder.cjs +242 -98
  64. package/src/lib/text-utils.cjs +1 -1
  65. package/src/mixdog-session-runtime.mjs +2211 -446
  66. package/src/output-styles/default.md +12 -28
  67. package/src/output-styles/extreme-simple.md +9 -6
  68. package/src/output-styles/simple.md +22 -9
  69. package/src/repl.mjs +108 -59
  70. package/src/rules/agent/00-common.md +15 -0
  71. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  72. package/src/rules/agent/30-explorer.md +22 -0
  73. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  75. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  76. package/src/rules/lead/01-general.md +9 -5
  77. package/src/rules/lead/04-workflow.md +51 -12
  78. package/src/rules/lead/lead-tool.md +6 -0
  79. package/src/rules/shared/01-tool.md +12 -1
  80. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  81. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  82. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  83. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  85. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  86. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  87. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  88. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  89. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  90. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  91. package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
  92. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  93. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  94. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  95. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +380 -268
  96. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +131 -93
  97. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  98. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  99. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  100. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  101. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  102. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  103. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  104. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  107. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +203 -71
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +512 -312
  109. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -518
  110. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  111. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  112. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +289 -11
  113. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  114. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  115. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  116. package/src/runtime/agent/orchestrator/session/loop.mjs +840 -612
  117. package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
  118. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  119. package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
  120. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  121. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  122. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  123. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +35 -8
  124. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -8
  125. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -53
  126. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  127. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  128. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  129. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  130. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  131. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  132. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  133. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  137. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  138. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  139. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  140. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  141. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  142. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  143. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  144. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  145. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  146. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  147. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  148. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  149. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  150. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  151. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  152. package/src/runtime/channels/index.mjs +149 -51
  153. package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
  154. package/src/runtime/channels/lib/config.mjs +0 -1
  155. package/src/runtime/channels/lib/drop-trace.mjs +1 -1
  156. package/src/runtime/channels/lib/executor.mjs +0 -3
  157. package/src/runtime/channels/lib/memory-client.mjs +0 -38
  158. package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
  159. package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
  160. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  161. package/src/runtime/channels/lib/session-discovery.mjs +3 -6
  162. package/src/runtime/channels/lib/tool-format.mjs +0 -1
  163. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -12
  164. package/src/runtime/channels/lib/webhook.mjs +1 -1
  165. package/src/runtime/channels/tool-defs.mjs +29 -29
  166. package/src/runtime/lib/keychain-cjs.cjs +0 -1
  167. package/src/runtime/memory/data/runtime-manifest.json +6 -7
  168. package/src/runtime/memory/index.mjs +519 -118
  169. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  170. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  171. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  172. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  173. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  174. package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
  175. package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
  176. package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
  177. package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
  178. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -2
  179. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  180. package/src/runtime/memory/lib/memory.mjs +88 -9
  181. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  182. package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
  183. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  184. package/src/runtime/memory/lib/runtime-fetcher.mjs +80 -21
  185. package/src/runtime/memory/lib/session-ingest.mjs +196 -0
  186. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  187. package/src/runtime/memory/tool-defs.mjs +46 -37
  188. package/src/runtime/search/index.mjs +104 -473
  189. package/src/runtime/search/lib/config.mjs +0 -4
  190. package/src/runtime/search/lib/state.mjs +1 -15
  191. package/src/runtime/search/lib/web-tools.mjs +44 -25
  192. package/src/runtime/search/tool-defs.mjs +16 -23
  193. package/src/runtime/shared/abort-controller.mjs +1 -1
  194. package/src/runtime/shared/atomic-file.mjs +4 -3
  195. package/src/runtime/shared/background-tasks.mjs +122 -11
  196. package/src/runtime/shared/child-spawn-gate.mjs +139 -0
  197. package/src/runtime/shared/config.mjs +7 -4
  198. package/src/runtime/shared/err-text.mjs +131 -4
  199. package/src/runtime/shared/llm/cost.mjs +2 -2
  200. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  201. package/src/runtime/shared/llm/index.mjs +34 -11
  202. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  203. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  204. package/src/runtime/shared/singleton-owner.mjs +104 -0
  205. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  206. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  207. package/src/runtime/shared/tool-surface.mjs +624 -95
  208. package/src/runtime/shared/user-data-guard.mjs +0 -2
  209. package/src/standalone/agent-task-status.mjs +203 -0
  210. package/src/standalone/agent-task-status.test.mjs +76 -0
  211. package/src/standalone/agent-tool.mjs +1913 -0
  212. package/src/standalone/channel-worker.mjs +370 -14
  213. package/src/standalone/explore-tool.mjs +165 -70
  214. package/src/standalone/folder-dialog.mjs +314 -0
  215. package/src/standalone/hook-bus.mjs +898 -22
  216. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  217. package/src/standalone/projects.mjs +226 -0
  218. package/src/standalone/provider-admin.mjs +41 -24
  219. package/src/standalone/seeds.mjs +1 -78
  220. package/src/standalone/usage-dashboard.mjs +96 -8
  221. package/src/tui/App.jsx +4800 -2140
  222. package/src/tui/components/AnsiText.jsx +39 -28
  223. package/src/tui/components/ContextPanel.jsx +14 -4
  224. package/src/tui/components/Markdown.jsx +43 -77
  225. package/src/tui/components/MarkdownTable.jsx +9 -184
  226. package/src/tui/components/Message.jsx +28 -11
  227. package/src/tui/components/Picker.jsx +95 -56
  228. package/src/tui/components/PromptInput.jsx +428 -239
  229. package/src/tui/components/QueuedCommands.jsx +1 -1
  230. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  231. package/src/tui/components/Spinner.jsx +67 -38
  232. package/src/tui/components/StatusLine.jsx +606 -38
  233. package/src/tui/components/TextEntryPanel.jsx +128 -9
  234. package/src/tui/components/ToolExecution.jsx +597 -362
  235. package/src/tui/components/TurnDone.jsx +3 -3
  236. package/src/tui/components/UsagePanel.jsx +3 -5
  237. package/src/tui/components/tool-output-format.mjs +499 -0
  238. package/src/tui/components/tool-output-format.test.mjs +312 -0
  239. package/src/tui/dist/index.mjs +9192 -2399
  240. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  241. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  242. package/src/tui/engine.mjs +1455 -279
  243. package/src/tui/figures.mjs +21 -40
  244. package/src/tui/index.jsx +75 -31
  245. package/src/tui/input-editing.mjs +25 -0
  246. package/src/tui/markdown/format-token.mjs +671 -69
  247. package/src/tui/markdown/format-token.test.mjs +312 -0
  248. package/src/tui/markdown/render-ansi.mjs +94 -0
  249. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  250. package/src/tui/markdown/stream-fence.mjs +34 -0
  251. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  252. package/src/tui/markdown/table-layout.mjs +250 -0
  253. package/src/tui/paste-attachments.mjs +0 -7
  254. package/src/tui/spinner-verbs.mjs +1 -2
  255. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  256. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  257. package/src/tui/theme.mjs +756 -24
  258. package/src/tui/time-format.mjs +1 -1
  259. package/src/tui/transcript-tool-failures.mjs +67 -0
  260. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  261. package/src/ui/ansi.mjs +1 -2
  262. package/src/ui/markdown.mjs +85 -26
  263. package/src/ui/markdown.test.mjs +70 -0
  264. package/src/ui/model-display.mjs +121 -0
  265. package/src/ui/session-stats.mjs +44 -0
  266. package/src/ui/statusline-context-label.test.mjs +15 -0
  267. package/src/ui/statusline.mjs +386 -178
  268. package/src/ui/tool-card.mjs +2 -13
  269. package/src/vendor/statusline/bin/statusline-lib.mjs +1 -620
  270. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  271. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  272. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  273. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  274. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  275. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  276. package/src/workflows/default/WORKFLOW.md +12 -5
  277. package/src/workflows/default/workflow.json +0 -1
  278. package/src/workflows/solo/WORKFLOW.md +15 -0
  279. package/src/workflows/solo/workflow.json +7 -0
  280. package/vendor/ink/build/ink.js +54 -8
  281. package/vendor/ink/build/output.js +6 -1
  282. package/src/agents/scheduler-task.md +0 -3
  283. package/src/agents/web-researcher/AGENT.md +0 -3
  284. package/src/agents/web-researcher/agent.json +0 -6
  285. package/src/agents/webhook-handler.md +0 -3
  286. package/src/hooks/lib/permission-rules.cjs +0 -170
  287. package/src/hooks/lib/settings-loader.cjs +0 -112
  288. package/src/lib/hook-pipe-path.cjs +0 -10
  289. package/src/rules/bridge/00-common.md +0 -5
  290. package/src/rules/bridge/30-explorer.md +0 -4
  291. package/src/rules/lead/00-tool-lead.md +0 -5
  292. package/src/rules/shared/00-language.md +0 -3
  293. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  294. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  295. package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
  296. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  297. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  298. package/src/standalone/bridge-tool.mjs +0 -1414
  299. package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env node
2
+ // Focused smoke for the spawn tag-reuse priority added to agent-tool.mjs.
3
+ //
4
+ // Validates the unified 4-tier spawn dispatch when an explicit tag is pinned:
5
+ // 1) live + idle -> spawn reuses the existing session (reused:true, send path)
6
+ // 2) live + busy -> spawn queues the prompt instead of throwing
7
+ // 3) lingering terminal trace (no live session) -> spawn/send error, no respawn
8
+ // 4) genuinely new tag -> a fresh spawn (no reused/respawned flag)
9
+ //
10
+ // No network: a fake askSession drives status transitions. Uses the REAL
11
+ // session manager so resolveTag / worker-index behavior is exercised end to end.
12
+ //
13
+ // Run: node scripts/agent-tag-reuse-smoke.mjs
14
+ import { mkdtempSync, mkdirSync, rmSync } from 'node:fs';
15
+ import { join } from 'node:path';
16
+ import { tmpdir } from 'node:os';
17
+ import { createStandaloneAgent } from '../src/standalone/agent-tool.mjs';
18
+ import { initProviders } from '../src/runtime/agent/orchestrator/providers/registry.mjs';
19
+ import {
20
+ closeSession,
21
+ enqueuePendingMessage,
22
+ getSession,
23
+ getSessionLastProgressAt,
24
+ getSessionRuntime,
25
+ listSessions,
26
+ } from '../src/runtime/agent/orchestrator/session/manager.mjs';
27
+
28
+ process.env.MIXDOG_AGENT_TRACE_DISABLE = '1';
29
+
30
+ const root = mkdtempSync(join(tmpdir(), 'mixdog-agent-reuse-'));
31
+ const dataDir = join(root, '.mixdog-data');
32
+ mkdirSync(dataDir, { recursive: true });
33
+
34
+ function assert(condition, message) {
35
+ if (!condition) throw new Error(message);
36
+ }
37
+ function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
38
+ function taskId(text) { return String(text).match(/agent task: (\S+)/)?.[1] || null; }
39
+
40
+ const askLog = [];
41
+ let holdBusy = false;
42
+ async function fakeAskSession(sessionId, prompt) {
43
+ const session = getSession(sessionId);
44
+ if (session) { session.status = 'running'; session.lastStreamDeltaAt = Date.now(); }
45
+ askLog.push({ sessionId, prompt });
46
+ try {
47
+ if (holdBusy && /stay busy/i.test(prompt)) {
48
+ for (let i = 0; i < 40 && holdBusy; i += 1) await sleep(25);
49
+ }
50
+ return { content: `ack: ${String(prompt).split('\n').pop()}` };
51
+ } finally {
52
+ if (session) { session.status = 'idle'; session.lastUsedAt = new Date().toISOString(); session.lastStreamDeltaAt = Date.now(); }
53
+ }
54
+ }
55
+
56
+ const cfgMod = {
57
+ loadConfig() {
58
+ return {
59
+ providers: { 'openai-oauth': { enabled: true } },
60
+ agents: {
61
+ reviewer: { provider: 'openai-oauth', model: 'gpt-5.5', effort: 'low' },
62
+ worker: { provider: 'openai-oauth', model: 'gpt-5.5', effort: 'low', fast: true },
63
+ },
64
+ presets: [
65
+ { id: 'fake-reviewer', name: 'Fake Reviewer', provider: 'openai-oauth', model: 'gpt-5.5', tools: 'readonly', effort: 'low' },
66
+ { id: 'fake-worker', name: 'Fake Worker', provider: 'openai-oauth', model: 'gpt-5.5', tools: 'full', effort: 'low', fast: true },
67
+ ],
68
+ };
69
+ },
70
+ resolveRuntimeSpec(preset, ctx) {
71
+ return { lane: 'agent', scopeKey: `reuse:${ctx.agentId}`, provider: preset.provider, model: preset.model };
72
+ },
73
+ };
74
+ const reg = { initProviders };
75
+ const mgr = {
76
+ getSession,
77
+ listSessions,
78
+ closeSession,
79
+ getSessionRuntime,
80
+ enqueuePendingMessage,
81
+ getSessionLastProgressAt,
82
+ askSession: fakeAskSession,
83
+ };
84
+
85
+ const ctx = { invocationSource: 'model-tool', cwd: root, clientHostPid: 0xBEEF };
86
+ const agent = createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: root, defaultMode: 'async' });
87
+
88
+ async function waitJob(out, pattern, label, tries = 60) {
89
+ const id = taskId(out);
90
+ assert(id, `missing task id for ${label}: ${out}`);
91
+ let last = '';
92
+ for (let i = 0; i < tries; i += 1) {
93
+ last = await agent.execute({ type: 'read', task_id: id }, ctx);
94
+ if (pattern.test(last)) return last;
95
+ if (/status: error/.test(last)) break;
96
+ await sleep(40);
97
+ }
98
+ throw new Error(`${label} did not match ${pattern}: ${last}`);
99
+ }
100
+ function sessionForTag(tag) {
101
+ return listSessions().find((s) => s?.agentTag === tag && !s.closed) || null;
102
+ }
103
+ async function waitSessionForTag(tag, label, tries = 60) {
104
+ for (let i = 0; i < tries; i += 1) {
105
+ const s = sessionForTag(tag);
106
+ if (s) return s;
107
+ await sleep(25);
108
+ }
109
+ throw new Error(`${label}: live session for tag ${tag} never appeared`);
110
+ }
111
+
112
+ async function main() {
113
+ await initProviders({ 'openai-oauth': { enabled: true } });
114
+
115
+ // --- tier 4: a genuinely new explicit tag spawns fresh (no reuse flag) ---
116
+ const firstOut = await agent.execute({
117
+ type: 'spawn', agent: 'reviewer', tag: 'reviewerA', prompt: 'first review pass', cwd: root,
118
+ }, ctx);
119
+ assert(/agent task:/.test(firstOut), `first spawn must return a task: ${firstOut}`);
120
+ assert(!/reused: true/.test(firstOut) && !/respawned/.test(firstOut), `fresh tag must not be flagged reused/respawned: ${firstOut}`);
121
+ await waitJob(firstOut, /ack: first review pass/, 'first spawn');
122
+ const sid = (await waitSessionForTag('reviewerA', 'first spawn')).id;
123
+ assert(sid, 'reviewerA session should remain live (idle) within the reap window');
124
+
125
+ // --- tier 1 (the headline bug): re-spawn the SAME tag while idle+live ---
126
+ const reuseOut = await agent.execute({
127
+ type: 'spawn', agent: 'reviewer', tag: 'reviewerA', prompt: 'second review pass', cwd: root,
128
+ }, ctx);
129
+ assert(!/^Error/.test(reuseOut), `idle same-tag re-spawn must not error: ${reuseOut}`);
130
+ assert(/reused: true/.test(reuseOut), `idle same-tag re-spawn must be flagged reused: ${reuseOut}`);
131
+ await waitJob(reuseOut, /ack: second review pass/, 'reuse spawn');
132
+ const sid2 = (await waitSessionForTag('reviewerA', 'reuse spawn')).id;
133
+ assert(sid2 === sid, `reuse must continue the SAME session (context kept): ${sid} -> ${sid2}`);
134
+
135
+ // --- tier 2: re-spawn the SAME tag while BUSY -> queue, do not throw ---
136
+ holdBusy = true;
137
+ const busyStart = await agent.execute({
138
+ type: 'spawn', agent: 'worker', tag: 'workerB', prompt: 'stay busy please', cwd: root,
139
+ }, ctx);
140
+ const busySession = await waitSessionForTag('workerB', 'busy worker');
141
+ for (let i = 0; i < 40; i += 1) {
142
+ if (getSession(busySession.id)?.status === 'running') break;
143
+ await sleep(25);
144
+ }
145
+ const queuedOut = await agent.execute({
146
+ type: 'spawn', agent: 'worker', tag: 'workerB', prompt: 'stacked while busy', cwd: root,
147
+ }, ctx);
148
+ assert(/agent message queued/.test(queuedOut), `busy same-tag re-spawn must queue, got: ${queuedOut}`);
149
+ assert(/reused: true/.test(queuedOut), `busy re-spawn queue should carry reused flag: ${queuedOut}`);
150
+ holdBusy = false;
151
+ await waitJob(busyStart, /ack: stay busy please/, 'busy worker finish');
152
+
153
+ // --- tier 3: terminal trace only -> coldRespawn keeps the tag ---
154
+ // --- tier 3: lingering trace without live session must not cold-respawn ---
155
+ closeSession(sid, 'smoke-session-closed-trace');
156
+ const traceSpawnOut = await agent.execute({
157
+ type: 'spawn', agent: 'reviewer', tag: 'reviewerA', prompt: 'must not respawn', cwd: root,
158
+ }, ctx);
159
+ assert(/^Error:/.test(traceSpawnOut), `terminal-trace spawn must error, got: ${traceSpawnOut}`);
160
+ assert(/finished or closed worker/i.test(traceSpawnOut), `spawn error should mention trace: ${traceSpawnOut}`);
161
+ const traceSendOut = await agent.execute({
162
+ type: 'send', tag: 'reviewerA', message: 'must not cold-respawn', cwd: root,
163
+ }, ctx);
164
+ assert(/^Error:/.test(traceSendOut), `terminal-trace send must error, got: ${traceSendOut}`);
165
+ assert(/not found|closed/i.test(traceSendOut), `send should surface prepareSend failure: ${traceSendOut}`);
166
+
167
+ // agent close clears the trace; same tag can spawn fresh again
168
+ await agent.execute({ type: 'close', tag: 'reviewerA' }, ctx);
169
+ const freshAfterClose = await agent.execute({
170
+ type: 'spawn', agent: 'reviewer', tag: 'reviewerA', prompt: 'post-forget fresh', cwd: root,
171
+ }, ctx);
172
+ assert(!/^Error/.test(freshAfterClose), `post-forget spawn must work: ${freshAfterClose}`);
173
+ await waitJob(freshAfterClose, /ack: post-forget fresh/, 'post-forget spawn');
174
+
175
+ process.stdout.write(`agent tag-reuse smoke passed (asks=${askLog.length})\n`);
176
+ }
177
+
178
+ try {
179
+ await main();
180
+ } finally {
181
+ try { agent.closeAll('agent-tag-reuse-smoke-end'); } catch {}
182
+ rmSync(root, { recursive: true, force: true });
183
+ }
@@ -0,0 +1,38 @@
1
+ import assert from 'node:assert/strict';
2
+ import {
3
+ registerBackgroundTask,
4
+ renderBackgroundTask,
5
+ taskSummary,
6
+ } from '../src/runtime/shared/background-tasks.mjs';
7
+
8
+ const task = registerBackgroundTask({
9
+ surface: 'agent',
10
+ operation: 'spawn',
11
+ label: 'smoke',
12
+ meta: {
13
+ tag: 'w1',
14
+ role: 'worker',
15
+ firstResponseTimeoutMs: 30_000,
16
+ idleTimeoutMs: 120_000,
17
+ spawnPrepTimeoutMs: 90_000,
18
+ watchdogPolicy: { firstResponseMs: 30_000 },
19
+ },
20
+ });
21
+
22
+ const rendered = renderBackgroundTask(task, { includeResult: false });
23
+ for (const forbidden of [
24
+ 'firstResponseTimeoutMs',
25
+ 'idleTimeoutMs',
26
+ 'spawnPrepTimeoutMs',
27
+ 'watchdogPolicy',
28
+ ]) {
29
+ assert.equal(rendered.includes(forbidden), false, `render leaked ${forbidden}`);
30
+ }
31
+
32
+ const summary = taskSummary(task);
33
+ assert.equal(summary.firstResponseTimeoutMs, undefined);
34
+ assert.equal(summary.idleTimeoutMs, undefined);
35
+ assert.equal(summary.spawnPrepTimeoutMs, undefined);
36
+ assert.equal(summary.tag, 'w1');
37
+
38
+ console.log('background-task-meta-smoke: ok');
@@ -10,8 +10,18 @@ const FAST_BACKGROUND_ENV = {
10
10
  MIXDOG_DISABLE_PROVIDER_WARMUP: '1',
11
11
  MIXDOG_DISABLE_CHANNEL_START: '1',
12
12
  };
13
+ const DEFAULT_BACKGROUND_ENV = {
14
+ MIXDOG_DISABLE_SESSION_PREWARM: '',
15
+ MIXDOG_ENABLE_SESSION_PREWARM: '',
16
+ MIXDOG_SESSION_PREWARM_DELAY_MS: '',
17
+ MIXDOG_DISABLE_PROVIDER_WARMUP: '',
18
+ MIXDOG_ENABLE_PROVIDER_WARMUP: '',
19
+ MIXDOG_PROVIDER_WARMUP_BEFORE_FIRST_TURN: '',
20
+ MIXDOG_PROVIDER_WARMUP_DELAY_MS: '',
21
+ MIXDOG_PROVIDER_MODEL_WARMUP_DELAY_MS: '',
22
+ };
13
23
 
14
- function runCase(name, args, { env = {}, input = null, expectStdout = null, expectStderr = null } = {}) {
24
+ function runCase(name, args, { env = {}, input = null, expectStdout = null, expectStderr = null, maxMs = THRESHOLD_MS } = {}) {
15
25
  const startedAt = performance.now();
16
26
  const result = spawnSync(process.execPath, args, {
17
27
  cwd: root,
@@ -19,7 +29,7 @@ function runCase(name, args, { env = {}, input = null, expectStdout = null, expe
19
29
  input,
20
30
  encoding: 'utf8',
21
31
  stdio: [input == null ? 'ignore' : 'pipe', 'pipe', 'pipe'],
22
- timeout: Math.max(10_000, THRESHOLD_MS + 5_000),
32
+ timeout: Math.max(10_000, maxMs + 5_000),
23
33
  });
24
34
  const ms = performance.now() - startedAt;
25
35
  const stdout = result.stdout || '';
@@ -33,8 +43,8 @@ function runCase(name, args, { env = {}, input = null, expectStdout = null, expe
33
43
  if (expectStderr && !stderr.includes(expectStderr)) {
34
44
  throw new Error(`${name} stderr missing ${expectStderr}:\n${stderr.slice(0, 1000)}`);
35
45
  }
36
- if (ms > THRESHOLD_MS) {
37
- throw new Error(`${name} boot smoke exceeded ${THRESHOLD_MS}ms (${ms.toFixed(1)}ms)`);
46
+ if (ms > maxMs) {
47
+ throw new Error(`${name} boot smoke exceeded ${maxMs}ms (${ms.toFixed(1)}ms)`);
38
48
  }
39
49
  return { name, ms: Math.round(ms * 10) / 10 };
40
50
  }
@@ -54,19 +64,22 @@ const rows = [
54
64
  try {
55
65
  const status = runtime.toolsStatus();
56
66
  const active = new Set(status.activeTools || []);
57
- for (const name of ['read','code_graph','grep','glob','list','apply_patch','explore','bridge','tool_search','shell']) {
67
+ for (const name of ['read','code_graph','grep','find','glob','list','apply_patch','explore','agent','shell','task','cwd','recall','search','web_fetch','Skill','tool_search']) {
58
68
  if (!active.has(name)) throw new Error('missing ' + name + ' in ' + [...active].join(','));
59
69
  }
60
- for (const name of ['bash','task']) {
70
+ for (const name of ['bash']) {
61
71
  if (active.has(name)) throw new Error('unexpected ' + name + ' in ' + [...active].join(','));
62
72
  }
63
73
  const result = runtime.selectTools('shell');
64
- if (!result.already.includes('shell') || !result.added.includes('task')) {
65
- throw new Error('shell alias should keep shell active and add task: ' + JSON.stringify(result));
74
+ const selectedNames = new Set([...(result.added || []), ...(result.already || [])]);
75
+ if (!selectedNames.has('shell') || !selectedNames.has('task')) {
76
+ throw new Error('shell alias should resolve shell and task: ' + JSON.stringify(result));
66
77
  }
67
78
  const nextActive = new Set(result.status.activeTools || []);
79
+ const nextDiscovered = new Set(result.status.discoveredTools || []);
68
80
  for (const name of ['shell','task']) {
69
- if (!nextActive.has(name)) throw new Error('selected tool missing ' + name + ' in ' + [...nextActive].join(','));
81
+ const selected = result.native === true ? nextDiscovered : nextActive;
82
+ if (!selected.has(name) && !nextActive.has(name)) throw new Error('selected tool missing ' + name + ' in ' + [...selected].join(','));
70
83
  }
71
84
  console.log('runtime_tools active=' + status.activeCount + '/' + status.count + ' selected=' + result.added.join(','));
72
85
  } finally {
@@ -86,6 +99,36 @@ const rows = [
86
99
  input: '/quit\n',
87
100
  expectStdout: 'bye.',
88
101
  }),
102
+ runCase('runtime_idle_exit', ['--input-type=module', '-e', `
103
+ const { createMixdogSessionRuntime } = await import('./src/mixdog-session-runtime.mjs');
104
+ const runtime = await createMixdogSessionRuntime({ toolMode: 'full' });
105
+ await new Promise((resolve) => setTimeout(resolve, 450));
106
+ const startedAt = performance.now();
107
+ await runtime.close('runtime-idle-exit-smoke', { waitForExit: false });
108
+ console.log('runtime_idle_exit close_ms=' + (performance.now() - startedAt).toFixed(1));
109
+ `], {
110
+ env: { ...DEFAULT_BACKGROUND_ENV, MIXDOG_BOOT_PROFILE: '1' },
111
+ expectStdout: 'runtime_idle_exit close_ms=',
112
+ expectStderr: 'session:prewarm-skipped',
113
+ maxMs: 2_000,
114
+ }),
115
+ runCase('runtime_idle_exit_provider_optin', ['--input-type=module', '-e', `
116
+ const { createMixdogSessionRuntime } = await import('./src/mixdog-session-runtime.mjs');
117
+ const runtime = await createMixdogSessionRuntime({ toolMode: 'full' });
118
+ await new Promise((resolve) => setTimeout(resolve, 250));
119
+ await runtime.close('runtime-provider-optin-exit-smoke', { waitForExit: false });
120
+ console.log('runtime_idle_exit_provider_optin ok');
121
+ `], {
122
+ env: {
123
+ ...DEFAULT_BACKGROUND_ENV,
124
+ MIXDOG_BOOT_PROFILE: '1',
125
+ MIXDOG_ENABLE_PROVIDER_WARMUP: '1',
126
+ MIXDOG_PROVIDER_WARMUP_DELAY_MS: '100',
127
+ },
128
+ expectStdout: 'runtime_idle_exit_provider_optin ok',
129
+ expectStderr: 'providers:warm-deferred',
130
+ maxMs: 2_000,
131
+ }),
89
132
  ];
90
133
 
91
134
  for (const row of rows) {
@@ -0,0 +1,348 @@
1
+ #!/usr/bin/env bash
2
+ # build-runtime-linux.sh — Build self-contained PostgreSQL 16 + pgvector runtime on Linux.
3
+ # Designed to run inside ubuntu:20.04 container (glibc 2.31 floor) so produced
4
+ # binaries run on every distro from Ubuntu 20.04 / Debian 11 / RHEL 8 forward.
5
+ # Targets the runner's native arch (x64 or arm64).
6
+ # Produces: dist/mixdog-runtime-linux-{arch}-pg{pgver}-pgvector{vecver}.tar.gz
7
+ # Bundles foreign dyn deps via ldd transitive closure (binaries + ALL extension
8
+ # modules under lib/postgresql/) + patchelf rpath rewrite. Final smoke: initdb,
9
+ # pg_ctl start, CREATE EXTENSION vector, distance query, stop.
10
+
11
+ set -euo pipefail
12
+
13
+ PG_VERSION="16.4"
14
+ PGVECTOR_VERSION="0.8.2"
15
+ TARGET_OS="${TARGET_OS:-linux}"
16
+ TARGET_ARCH="${TARGET_ARCH:-x64}"
17
+
18
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19
+ ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
20
+ BUILD_DIR="$ROOT_DIR/build/runtime-linux-$TARGET_ARCH"
21
+ STAGE_DIR="$BUILD_DIR/stage"
22
+ DIST_DIR="$ROOT_DIR/dist"
23
+ RUNTIME_DIR="$BUILD_DIR/runtime"
24
+
25
+ OUTPUT_NAME="mixdog-runtime-${TARGET_OS}-${TARGET_ARCH}-pg${PG_VERSION}-pgvector${PGVECTOR_VERSION}.tar.gz"
26
+
27
+ mkdir -p "$BUILD_DIR" "$STAGE_DIR" "$DIST_DIR" "$RUNTIME_DIR"/{bin,lib,share}
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Build deps. SUDO=sudo iff non-root; inside ubuntu:20.04 container we are root.
31
+ # ---------------------------------------------------------------------------
32
+ SUDO=""
33
+ if [[ "$(id -u)" -ne 0 ]]; then SUDO="sudo"; fi
34
+ # Export rather than inline-assign before $SUDO: when SUDO is empty (root),
35
+ # `$SUDO DEBIAN_FRONTEND=... apt-get` would parse the env-assignment as a
36
+ # command name and fail with "command not found".
37
+ export DEBIAN_FRONTEND=noninteractive
38
+
39
+ echo "==> Installing build dependencies"
40
+ $SUDO apt-get update -qq
41
+ $SUDO apt-get install -y --no-install-recommends \
42
+ build-essential libreadline-dev zlib1g-dev libssl-dev \
43
+ libicu-dev libxml2-dev libzstd-dev liblz4-dev \
44
+ pkg-config curl git ca-certificates patchelf file \
45
+ bsdmainutils
46
+
47
+ if [[ -x "$STAGE_DIR/bin/postgres" ]]; then
48
+ echo "==> Cache hit: PG already built at $STAGE_DIR — skipping configure/make"
49
+ unset TARGET_OS TARGET_ARCH
50
+ else
51
+ echo "==> Downloading PostgreSQL $PG_VERSION source"
52
+ cd "$BUILD_DIR"
53
+ if [[ ! -f "postgresql-${PG_VERSION}.tar.gz" ]]; then
54
+ curl -fsSL "https://ftp.postgresql.org/pub/source/v${PG_VERSION}/postgresql-${PG_VERSION}.tar.gz" \
55
+ -o "postgresql-${PG_VERSION}.tar.gz"
56
+ fi
57
+ rm -rf "postgresql-${PG_VERSION}"
58
+ tar xzf "postgresql-${PG_VERSION}.tar.gz"
59
+
60
+ echo "==> Configuring PostgreSQL"
61
+ cd "postgresql-${PG_VERSION}"
62
+ ./configure \
63
+ --prefix="$STAGE_DIR" \
64
+ --without-perl \
65
+ --without-python \
66
+ --without-tcl \
67
+ --with-openssl \
68
+ --with-libxml \
69
+ --with-icu \
70
+ --with-readline \
71
+ --enable-thread-safety \
72
+ CFLAGS="-O2"
73
+
74
+ echo "==> Building PostgreSQL"
75
+ # PG Makefile.global has its own TARGET_ARCH var; env-passed TARGET_ARCH=x64
76
+ # would collide as a make-variable override and leak into compile commands.
77
+ unset TARGET_OS TARGET_ARCH
78
+ make -j"$(nproc)"
79
+ make install
80
+ make -C contrib/pgcrypto install
81
+ fi
82
+
83
+ PG_CONFIG="$STAGE_DIR/bin/pg_config"
84
+ export PATH="$STAGE_DIR/bin:$PATH"
85
+
86
+ echo "==> Stripping PostgreSQL binaries"
87
+ find "$STAGE_DIR" -name '*.so*' -type f -exec strip --strip-debug {} \; 2>/dev/null || true
88
+ find "$STAGE_DIR/bin" -type f -exec strip --strip-all {} \; 2>/dev/null || true
89
+
90
+ if [[ -f "$STAGE_DIR/lib/postgresql/vector.so" ]]; then
91
+ echo "==> Cache hit: pgvector already installed — skipping clone/build"
92
+ else
93
+ echo "==> Cloning pgvector $PGVECTOR_VERSION"
94
+ cd "$BUILD_DIR"
95
+ rm -rf pgvector
96
+ git clone --branch "v${PGVECTOR_VERSION}" --depth 1 \
97
+ https://github.com/pgvector/pgvector.git pgvector
98
+
99
+ echo "==> Building pgvector"
100
+ cd pgvector
101
+ make PG_CONFIG="$PG_CONFIG" -j"$(nproc)"
102
+ make PG_CONFIG="$PG_CONFIG" install
103
+ fi
104
+
105
+ echo "==> Assembling runtime layout"
106
+ rm -rf "$RUNTIME_DIR"
107
+ mkdir -p "$RUNTIME_DIR"/{bin,lib,share}
108
+ cp -a "$STAGE_DIR/bin/postgres" "$RUNTIME_DIR/bin/"
109
+ cp -a "$STAGE_DIR/bin/pg_ctl" "$RUNTIME_DIR/bin/"
110
+ cp -a "$STAGE_DIR/bin/pg_dump" "$RUNTIME_DIR/bin/"
111
+ cp -a "$STAGE_DIR/bin/pg_restore" "$RUNTIME_DIR/bin/"
112
+ cp -a "$STAGE_DIR/bin/psql" "$RUNTIME_DIR/bin/"
113
+ cp -a "$STAGE_DIR/bin/initdb" "$RUNTIME_DIR/bin/"
114
+
115
+ cp -a "$STAGE_DIR/lib"/. "$RUNTIME_DIR/lib/" 2>/dev/null || true
116
+ cp -a "$STAGE_DIR/share"/. "$RUNTIME_DIR/share/" 2>/dev/null || true
117
+
118
+ # ---------------------------------------------------------------------------
119
+ # Bundle foreign dyn deps — seed from binaries AND every extension module
120
+ # under lib/postgresql/. dlopen-loaded modules (extensions) need the same
121
+ # treatment as direct binary deps; missing them = runtime "ERROR: could not
122
+ # load library" on CREATE EXTENSION.
123
+ # ---------------------------------------------------------------------------
124
+ echo "==> Bundling foreign dynamic dependencies (binaries + extensions)"
125
+
126
+ KEEP_RE='^(linux-vdso|ld-linux|libc\.so|libm\.so|libpthread\.so|libdl\.so|librt\.so|libnsl\.so|libresolv\.so)'
127
+
128
+ collect_deps() {
129
+ ldd "$1" 2>/dev/null \
130
+ | awk '/=>/{print $3}' \
131
+ | grep -E '^/' \
132
+ | while read -r path; do
133
+ bn=$(basename "$path")
134
+ if echo "$bn" | grep -Eq "$KEEP_RE"; then
135
+ continue # system lib — keep on host
136
+ fi
137
+ echo "$path"
138
+ done \
139
+ | sort -u
140
+ }
141
+
142
+ declare -A SEEN
143
+ declare -a QUEUE
144
+ # Seeds: binaries + every .so under lib/postgresql/ (extensions, contrib mods).
145
+ for seed in "$RUNTIME_DIR/bin/postgres" "$RUNTIME_DIR/bin/psql" \
146
+ "$RUNTIME_DIR/bin/pg_ctl"; do
147
+ [[ -f "$seed" ]] && QUEUE+=("$seed")
148
+ done
149
+ while IFS= read -r -d '' ext; do
150
+ QUEUE+=("$ext")
151
+ done < <(find "$RUNTIME_DIR/lib/postgresql" -name '*.so' -print0 2>/dev/null)
152
+
153
+ declare -a FOREIGN
154
+ while [[ ${#QUEUE[@]} -gt 0 ]]; do
155
+ current="${QUEUE[0]}"
156
+ QUEUE=("${QUEUE[@]:1}")
157
+ while IFS= read -r dep; do
158
+ real="$(readlink -f "$dep")"
159
+ [[ -n "${SEEN[$real]+x}" ]] && continue
160
+ SEEN[$real]=1
161
+ FOREIGN+=("$dep")
162
+ QUEUE+=("$dep")
163
+ done < <(collect_deps "$current")
164
+ done
165
+
166
+ # Sanity: glibc / loader must never be bundled — if KEEP_RE regresses this catches it.
167
+ if find "$RUNTIME_DIR/lib" -maxdepth 1 \
168
+ \( -name 'libc.so*' -o -name 'ld-linux*' -o -name 'libpthread.so*' \) \
169
+ | grep -q .; then
170
+ echo "FATAL: bundled glibc/loader detected — KEEP_RE filter did not work"
171
+ exit 1
172
+ fi
173
+
174
+ echo " bundling ${#FOREIGN[@]} foreign libraries"
175
+ for so_path in "${FOREIGN[@]}"; do
176
+ real_path="$(readlink -f "$so_path")"
177
+ real_name="$(basename "$real_path")"
178
+ if [[ ! -f "$RUNTIME_DIR/lib/$real_name" ]]; then
179
+ cp -L "$real_path" "$RUNTIME_DIR/lib/$real_name"
180
+ chmod u+w "$RUNTIME_DIR/lib/$real_name"
181
+ echo " + $real_name"
182
+ fi
183
+ link="$so_path"
184
+ while [[ -L "$link" ]]; do
185
+ link_name="$(basename "$link")"
186
+ link_target="$(readlink "$link")"
187
+ if [[ ! -e "$RUNTIME_DIR/lib/$link_name" ]]; then
188
+ ln -sf "$(basename "$link_target")" "$RUNTIME_DIR/lib/$link_name"
189
+ fi
190
+ link="$(dirname "$link")/$link_target"
191
+ done
192
+ done
193
+
194
+ echo "==> Stripping static archives from lib/"
195
+ find "$RUNTIME_DIR/lib" -name '*.a' -delete
196
+
197
+ echo "==> Patching rpath"
198
+ # Binaries: $ORIGIN/../lib (from bin/ to lib/)
199
+ find "$RUNTIME_DIR/bin" -type f -executable | while read -r bin; do
200
+ if file "$bin" 2>/dev/null | grep -q ELF; then
201
+ patchelf --set-rpath '$ORIGIN/../lib' "$bin" 2>/dev/null || true
202
+ fi
203
+ done
204
+ # Top-level lib/*.so*: $ORIGIN
205
+ find "$RUNTIME_DIR/lib" -maxdepth 1 -type f -name '*.so*' | while read -r so; do
206
+ if file "$so" 2>/dev/null | grep -q ELF; then
207
+ patchelf --set-rpath '$ORIGIN' "$so" 2>/dev/null || true
208
+ fi
209
+ done
210
+ # Every extension module under lib/postgresql/: $ORIGIN/.. (=lib/) and $ORIGIN/../.. (=runtime root)
211
+ find "$RUNTIME_DIR/lib/postgresql" -name '*.so' 2>/dev/null | while read -r ext; do
212
+ if file "$ext" 2>/dev/null | grep -q ELF; then
213
+ patchelf --set-rpath '$ORIGIN/..:$ORIGIN/../..' "$ext" 2>/dev/null || true
214
+ fi
215
+ done
216
+
217
+ # ---------------------------------------------------------------------------
218
+ # Self-contained smoke — full PG lifecycle, not just --version
219
+ # ---------------------------------------------------------------------------
220
+ echo "==> Self-contained smoke test (initdb + CREATE EXTENSION vector + distance query)"
221
+ unset LD_LIBRARY_PATH
222
+ SMOKE_DATA="$BUILD_DIR/smoke-pgdata"
223
+ SMOKE_LOG="$BUILD_DIR/smoke-pg.log"
224
+ SMOKE_PORT=55899
225
+
226
+ # PG initdb refuses to run as root. Inside ubuntu:20.04 container we are root,
227
+ # so create an unprivileged user and run the smoke under it.
228
+ if [[ "$(id -u)" -eq 0 ]]; then
229
+ if ! id pguser >/dev/null 2>&1; then useradd -m -s /bin/bash pguser; fi
230
+ chown -R pguser:pguser "$BUILD_DIR"
231
+ RUN_AS=(runuser -u pguser --)
232
+ else
233
+ RUN_AS=()
234
+ fi
235
+
236
+ "${RUN_AS[@]}" "$RUNTIME_DIR/bin/postgres" --version || { echo "FAIL: postgres --version"; exit 1; }
237
+ MISSING="$(ldd "$RUNTIME_DIR/bin/postgres" 2>&1 | grep 'not found' || true)"
238
+ if [[ -n "$MISSING" ]]; then echo "FAIL: missing deps in postgres:"; echo "$MISSING"; exit 1; fi
239
+
240
+ SMOKE_OK=""
241
+ for attempt in 1 2 3; do
242
+ echo "==> Self-contained smoke (attempt $attempt/3)"
243
+ rm -rf "$SMOKE_DATA"
244
+ set +e
245
+ (
246
+ set -e
247
+ "${RUN_AS[@]}" "$RUNTIME_DIR/bin/initdb" -D "$SMOKE_DATA" --auth-local=trust --no-locale -E UTF8 -U postgres > /dev/null
248
+ "${RUN_AS[@]}" "$RUNTIME_DIR/bin/pg_ctl" -D "$SMOKE_DATA" -o "-p $SMOKE_PORT -h 127.0.0.1" -l "$SMOKE_LOG" -w start
249
+ "${RUN_AS[@]}" "$RUNTIME_DIR/bin/psql" -h 127.0.0.1 -p "$SMOKE_PORT" -U postgres -d postgres -c "CREATE EXTENSION vector;" > /dev/null
250
+ EXTV="$("${RUN_AS[@]}" "$RUNTIME_DIR/bin/psql" -h 127.0.0.1 -p "$SMOKE_PORT" -U postgres -d postgres -tAc "SELECT extversion FROM pg_extension WHERE extname='vector';")"
251
+ DIST="$("${RUN_AS[@]}" "$RUNTIME_DIR/bin/psql" -h 127.0.0.1 -p "$SMOKE_PORT" -U postgres -d postgres -tAc "SELECT '[1,2,3]'::vector <-> '[1,2,4]'::vector;")"
252
+ echo " vector extension version: $EXTV"
253
+ echo " distance query result: $DIST"
254
+ [[ "$EXTV" == "$PGVECTOR_VERSION" ]] || { echo "FAIL: extversion=$EXTV expected=$PGVECTOR_VERSION"; exit 1; }
255
+ "${RUN_AS[@]}" "$RUNTIME_DIR/bin/pg_ctl" -D "$SMOKE_DATA" -m fast stop > /dev/null
256
+ )
257
+ attempt_rc=$?
258
+ set -e
259
+ if [[ $attempt_rc -eq 0 ]]; then
260
+ rm -rf "$SMOKE_DATA"
261
+ echo " PASS smoke (extension load + vector distance)"
262
+ SMOKE_OK=1
263
+ break
264
+ fi
265
+ echo " attempt $attempt failed (rc=$attempt_rc) — cleaning up"
266
+ echo " -- last 20 lines of $SMOKE_LOG --"
267
+ tail -20 "$SMOKE_LOG" 2>/dev/null || true
268
+ "${RUN_AS[@]}" "$RUNTIME_DIR/bin/pg_ctl" -D "$SMOKE_DATA" -m immediate stop > /dev/null 2>&1 || true
269
+ pkill -f "$RUNTIME_DIR/bin/postgres" > /dev/null 2>&1 || true
270
+ rm -rf "$SMOKE_DATA"
271
+ done
272
+ if [[ -z "${SMOKE_OK:-}" ]]; then
273
+ echo "FAIL: smoke failed all 3 attempts"
274
+ exit 1
275
+ fi
276
+
277
+ # Licenses
278
+ curl -fsSL "https://raw.githubusercontent.com/postgres/postgres/REL_16_STABLE/COPYRIGHT" \
279
+ -o "$RUNTIME_DIR/LICENSE.postgresql"
280
+ cp "$BUILD_DIR/pgvector/LICENSE" "$RUNTIME_DIR/LICENSE.pgvector"
281
+
282
+ echo "==> Creating tarball: $OUTPUT_NAME"
283
+ tar czf "$DIST_DIR/$OUTPUT_NAME" -C "$RUNTIME_DIR" .
284
+
285
+ echo "==> Generating sha256 sidecar"
286
+ cd "$DIST_DIR"
287
+ sha256sum "$OUTPUT_NAME" > "${OUTPUT_NAME}.sha256"
288
+
289
+ # ---------------------------------------------------------------------------
290
+ # Phase A: Re-smoke from EXTRACTED tarball with hostile env. Catches false-pass
291
+ # where the build host happens to satisfy a dep that the tarball is missing.
292
+ # Uses `env -i` to clear all variables, minimal PATH, fresh data dir.
293
+ # ---------------------------------------------------------------------------
294
+ echo "==> Re-smoke from extracted tarball (hostile env, verify self-contained)"
295
+ EXTRACT_DIR="$BUILD_DIR/extract-smoke"
296
+ rm -rf "$EXTRACT_DIR"; mkdir -p "$EXTRACT_DIR"
297
+ tar xzf "$DIST_DIR/$OUTPUT_NAME" -C "$EXTRACT_DIR"
298
+ if [[ "$(id -u)" -eq 0 ]]; then chown -R pguser:pguser "$EXTRACT_DIR"; fi
299
+ EXTRACT_DATA="$EXTRACT_DIR/extract-pgdata"
300
+ EXTRACT_LOG="$EXTRACT_DIR/extract-pg.log"
301
+ EXTRACT_PORT=55898
302
+
303
+ "${RUN_AS[@]}" env -i HOME="$EXTRACT_DIR" PATH="/usr/bin:/bin" \
304
+ "$EXTRACT_DIR/bin/postgres" --version
305
+
306
+ EXTRACT_SMOKE_OK=""
307
+ for attempt in 1 2 3; do
308
+ echo "==> Re-smoke from extracted tarball (attempt $attempt/3)"
309
+ rm -rf "$EXTRACT_DATA"
310
+ set +e
311
+ (
312
+ set -e
313
+ "${RUN_AS[@]}" env -i HOME="$EXTRACT_DIR" PATH="/usr/bin:/bin" \
314
+ "$EXTRACT_DIR/bin/initdb" -D "$EXTRACT_DATA" --auth-local=trust --no-locale -E UTF8 -U postgres > /dev/null
315
+ "${RUN_AS[@]}" env -i HOME="$EXTRACT_DIR" PATH="/usr/bin:/bin" \
316
+ "$EXTRACT_DIR/bin/pg_ctl" -D "$EXTRACT_DATA" -o "-p $EXTRACT_PORT -h 127.0.0.1" -l "$EXTRACT_LOG" -w start
317
+ "${RUN_AS[@]}" env -i HOME="$EXTRACT_DIR" PATH="/usr/bin:/bin" \
318
+ "$EXTRACT_DIR/bin/psql" -h 127.0.0.1 -p "$EXTRACT_PORT" -U postgres -d postgres -c "CREATE EXTENSION vector;" > /dev/null
319
+ EXTV2="$("${RUN_AS[@]}" env -i HOME="$EXTRACT_DIR" PATH="/usr/bin:/bin" \
320
+ "$EXTRACT_DIR/bin/psql" -h 127.0.0.1 -p "$EXTRACT_PORT" -U postgres -d postgres -tAc \
321
+ "SELECT extversion FROM pg_extension WHERE extname='vector';")"
322
+ [[ "$EXTV2" == "$PGVECTOR_VERSION" ]] || { echo "FAIL: extracted-smoke extversion=$EXTV2"; exit 1; }
323
+ "${RUN_AS[@]}" env -i HOME="$EXTRACT_DIR" PATH="/usr/bin:/bin" \
324
+ "$EXTRACT_DIR/bin/pg_ctl" -D "$EXTRACT_DATA" -m fast stop > /dev/null
325
+ )
326
+ attempt_rc=$?
327
+ set -e
328
+ if [[ $attempt_rc -eq 0 ]]; then
329
+ rm -rf "$EXTRACT_DIR"
330
+ echo " PASS extracted-tarball smoke"
331
+ EXTRACT_SMOKE_OK=1
332
+ break
333
+ fi
334
+ echo " attempt $attempt failed (rc=$attempt_rc) — cleaning up"
335
+ echo " -- last 20 lines of $EXTRACT_LOG --"
336
+ tail -20 "$EXTRACT_LOG" 2>/dev/null || true
337
+ "${RUN_AS[@]}" env -i HOME="$EXTRACT_DIR" PATH="/usr/bin:/bin" \
338
+ "$EXTRACT_DIR/bin/pg_ctl" -D "$EXTRACT_DATA" -m immediate stop > /dev/null 2>&1 || true
339
+ pkill -f "$EXTRACT_DIR/bin/postgres" > /dev/null 2>&1 || true
340
+ rm -rf "$EXTRACT_DATA"
341
+ done
342
+ if [[ -z "${EXTRACT_SMOKE_OK:-}" ]]; then
343
+ echo "FAIL: extracted-tarball smoke failed all 3 attempts"
344
+ exit 1
345
+ fi
346
+
347
+ echo "==> Done: $DIST_DIR/$OUTPUT_NAME"
348
+ ls -lh "$DIST_DIR/$OUTPUT_NAME"