mixdog 0.9.18 → 0.9.20

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 (214) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +93 -29
  3. package/package.json +5 -3
  4. package/scripts/build-runtime-windows.ps1 +242 -242
  5. package/scripts/build-tui.mjs +6 -0
  6. package/scripts/hook-bus-test.mjs +8 -0
  7. package/scripts/log-writer-guard-smoke.mjs +131 -0
  8. package/scripts/output-style-smoke.mjs +2 -2
  9. package/scripts/reactive-compact-persist-smoke.mjs +22 -6
  10. package/scripts/recall-bench-cases.json +10 -0
  11. package/scripts/recall-bench.mjs +91 -2
  12. package/scripts/recall-quality-cases.json +1 -2
  13. package/scripts/recall-usecase-cases.json +1 -1
  14. package/scripts/session-ingest-compaction-smoke.mjs +241 -0
  15. package/scripts/smoke-runtime-negative.ps1 +106 -106
  16. package/scripts/tool-efficiency-diag.mjs +5 -2
  17. package/scripts/tool-smoke.mjs +251 -72
  18. package/src/agents/debugger/AGENT.md +3 -3
  19. package/src/agents/heavy-worker/AGENT.md +7 -10
  20. package/src/agents/maintainer/AGENT.md +1 -2
  21. package/src/agents/reviewer/AGENT.md +1 -2
  22. package/src/agents/worker/AGENT.md +5 -8
  23. package/src/defaults/agents.json +3 -0
  24. package/src/help.mjs +2 -5
  25. package/src/lib/mixdog-debug.cjs +13 -0
  26. package/src/mixdog-session-runtime.mjs +7 -3311
  27. package/src/rules/agent/00-core.md +4 -3
  28. package/src/rules/agent/30-explorer.md +53 -22
  29. package/src/rules/lead/02-channels.md +3 -3
  30. package/src/rules/lead/lead-tool.md +3 -2
  31. package/src/rules/shared/01-tool.md +24 -29
  32. package/src/runtime/agent/orchestrator/context/collect.mjs +33 -9
  33. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +1 -1
  34. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -1
  35. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +24 -3
  36. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +3 -3
  37. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +663 -0
  38. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +6 -0
  39. package/src/runtime/agent/orchestrator/session/context-utils.mjs +2 -2
  40. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +153 -0
  41. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +49 -0
  42. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +250 -35
  43. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -1
  44. package/src/runtime/agent/orchestrator/session/loop.mjs +8 -1860
  45. package/src/runtime/agent/orchestrator/session/manager/agent-runtime-singleton.mjs +29 -0
  46. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +686 -0
  47. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +60 -12
  48. package/src/runtime/agent/orchestrator/session/manager/env-utils.mjs +8 -0
  49. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +159 -0
  50. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +143 -0
  51. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +268 -0
  52. package/src/runtime/agent/orchestrator/session/manager/provider-cache-key.mjs +22 -0
  53. package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +26 -0
  54. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +124 -0
  55. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +258 -0
  56. package/src/runtime/agent/orchestrator/session/manager/session-errors.mjs +20 -0
  57. package/src/runtime/agent/orchestrator/session/manager/session-id.mjs +9 -0
  58. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +475 -0
  59. package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +23 -0
  60. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +6 -4
  61. package/src/runtime/agent/orchestrator/session/manager.mjs +88 -2285
  62. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +440 -0
  63. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +153 -0
  64. package/src/runtime/agent/orchestrator/session/store.mjs +4 -1
  65. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +642 -0
  66. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +23 -3
  67. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +198 -6
  68. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -2
  69. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +13 -15
  70. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +6 -1
  71. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +45 -3
  72. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -2
  73. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +195 -3
  74. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +108 -2
  75. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +15 -3
  76. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +7 -0
  77. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +24 -2
  78. package/src/runtime/agent/orchestrator/tools/builtin.mjs +17 -1
  79. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +38 -0
  80. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -4
  81. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +2 -2
  82. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +72 -8
  83. package/src/runtime/agent/orchestrator/tools/shell-policy-danger-target.mjs +5 -1
  84. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +1 -1
  85. package/src/runtime/channels/backends/discord-gateway.mjs +14 -1
  86. package/src/runtime/channels/backends/discord.mjs +43 -1
  87. package/src/runtime/channels/index.mjs +6 -2096
  88. package/src/runtime/channels/lib/inbound-handler.mjs +328 -0
  89. package/src/runtime/channels/lib/inbound-routing.mjs +19 -12
  90. package/src/runtime/channels/lib/interaction-handlers.mjs +260 -0
  91. package/src/runtime/channels/lib/memory-client.mjs +32 -14
  92. package/src/runtime/channels/lib/network-retry.mjs +23 -0
  93. package/src/runtime/channels/lib/output-forwarder.mjs +38 -7
  94. package/src/runtime/channels/lib/owned-runtime.mjs +547 -0
  95. package/src/runtime/channels/lib/owner-heartbeat.mjs +13 -4
  96. package/src/runtime/channels/lib/runtime-paths.mjs +35 -1
  97. package/src/runtime/channels/lib/tool-dispatch.mjs +17 -7
  98. package/src/runtime/channels/lib/transcript-binding.mjs +336 -0
  99. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +39 -2
  100. package/src/runtime/channels/lib/worker-bootstrap.mjs +97 -0
  101. package/src/runtime/channels/lib/worker-ipc.mjs +201 -0
  102. package/src/runtime/channels/lib/worker-main.mjs +771 -0
  103. package/src/runtime/memory/index.mjs +73 -1725
  104. package/src/runtime/memory/lib/embedding-provider.mjs +4 -2
  105. package/src/runtime/memory/lib/embedding-worker.mjs +47 -6
  106. package/src/runtime/memory/lib/http-router.mjs +772 -0
  107. package/src/runtime/memory/lib/memory-action-handlers.mjs +901 -0
  108. package/src/runtime/memory/lib/memory-embed.mjs +28 -7
  109. package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +8 -2
  110. package/src/runtime/memory/lib/memory-recall-store.mjs +182 -9
  111. package/src/runtime/memory/lib/query-handlers.mjs +38 -6
  112. package/src/runtime/memory/lib/recall-format.mjs +106 -6
  113. package/src/runtime/memory/lib/session-ingest-runtime.mjs +401 -0
  114. package/src/runtime/memory/lib/session-ingest.mjs +1 -1
  115. package/src/runtime/shared/atomic-file.mjs +10 -4
  116. package/src/runtime/shared/background-tasks.mjs +4 -2
  117. package/src/runtime/shared/tool-execution-contract.mjs +1 -1
  118. package/src/runtime/shared/tool-result-summary.mjs +1 -1
  119. package/src/runtime/shared/tool-surface.mjs +30 -1
  120. package/src/session-runtime/boot-profile.mjs +36 -0
  121. package/src/session-runtime/channel-config-api.mjs +70 -0
  122. package/src/session-runtime/config-lifecycle.mjs +1 -1
  123. package/src/session-runtime/context-status.mjs +181 -0
  124. package/src/session-runtime/cwd-plugins.mjs +46 -3
  125. package/src/session-runtime/env.mjs +17 -0
  126. package/src/session-runtime/lifecycle-api.mjs +242 -0
  127. package/src/session-runtime/mcp-glue.mjs +24 -3
  128. package/src/session-runtime/model-route-api.mjs +198 -0
  129. package/src/session-runtime/output-styles.mjs +44 -10
  130. package/src/session-runtime/provider-auth-api.mjs +135 -0
  131. package/src/session-runtime/resource-api.mjs +282 -0
  132. package/src/session-runtime/runtime-core.mjs +2046 -0
  133. package/src/session-runtime/session-turn-api.mjs +274 -0
  134. package/src/session-runtime/tool-catalog.mjs +18 -264
  135. package/src/session-runtime/tool-defs.mjs +2 -2
  136. package/src/session-runtime/workflow-agents-api.mjs +238 -0
  137. package/src/session-runtime/workflow.mjs +16 -1
  138. package/src/standalone/agent-tool.mjs +2 -2
  139. package/src/standalone/channel-worker.mjs +78 -5
  140. package/src/standalone/explore-tool.mjs +1 -1
  141. package/src/standalone/memory-runtime-proxy.mjs +56 -6
  142. package/src/tui/App.jsx +88 -97
  143. package/src/tui/app/channel-pickers.mjs +45 -0
  144. package/src/tui/app/core-memory-picker.mjs +1 -1
  145. package/src/tui/app/slash-commands.mjs +0 -1
  146. package/src/tui/app/slash-dispatch.mjs +0 -16
  147. package/src/tui/app/transcript-window.mjs +44 -1
  148. package/src/tui/app/use-mouse-input.mjs +15 -2
  149. package/src/tui/app/use-prompt-handlers.mjs +7 -94
  150. package/src/tui/app/use-transcript-scroll.mjs +82 -4
  151. package/src/tui/app/use-transcript-window.mjs +65 -5
  152. package/src/tui/components/PromptInput.jsx +33 -64
  153. package/src/tui/components/ToolExecution.jsx +2 -2
  154. package/src/tui/dist/index.mjs +7908 -7558
  155. package/src/tui/engine/context-state.mjs +145 -0
  156. package/src/tui/engine/prompt-history.mjs +27 -0
  157. package/src/tui/engine/session-api-ext.mjs +478 -0
  158. package/src/tui/engine/session-api.mjs +545 -0
  159. package/src/tui/engine/session-flow.mjs +485 -0
  160. package/src/tui/engine/turn.mjs +1078 -0
  161. package/src/tui/engine.mjs +69 -2582
  162. package/src/tui/index.jsx +7 -0
  163. package/src/tui/lib/voice-setup.mjs +166 -0
  164. package/src/tui/paste-attachments.mjs +12 -5
  165. package/src/tui/prompt-history-store.mjs +125 -12
  166. package/vendor/ink/build/ink.js +16 -1
  167. package/vendor/ink/build/output.js +30 -4
  168. package/vendor/ink/build/render.js +5 -0
  169. package/scripts/bench/cache-probe-tasks.json +0 -8
  170. package/scripts/bench/lead-review-tasks-r3.json +0 -20
  171. package/scripts/bench/lead-review-tasks.json +0 -20
  172. package/scripts/bench/r4-mixed-tasks.json +0 -20
  173. package/scripts/bench/r5-orchestrated-task.json +0 -7
  174. package/scripts/bench/review-tasks.json +0 -20
  175. package/scripts/bench/round-codex.json +0 -114
  176. package/scripts/bench/round-mixdog-lead-r3.json +0 -269
  177. package/scripts/bench/round-mixdog-lead.json +0 -269
  178. package/scripts/bench/round-mixdog.json +0 -126
  179. package/scripts/bench/round-r10-bigsample.json +0 -679
  180. package/scripts/bench/round-r11-codexalign.json +0 -257
  181. package/scripts/bench/round-r13-clientmeta.json +0 -464
  182. package/scripts/bench/round-r14-betafeatures.json +0 -466
  183. package/scripts/bench/round-r15-fulldefault.json +0 -462
  184. package/scripts/bench/round-r16-sessionid.json +0 -466
  185. package/scripts/bench/round-r17-wirebytes.json +0 -456
  186. package/scripts/bench/round-r18-prewarm.json +0 -468
  187. package/scripts/bench/round-r19-clean.json +0 -472
  188. package/scripts/bench/round-r20-prewarm-clean.json +0 -475
  189. package/scripts/bench/round-r21-delta-retry.json +0 -473
  190. package/scripts/bench/round-r22-full-probe.json +0 -693
  191. package/scripts/bench/round-r23-itemprobe.json +0 -701
  192. package/scripts/bench/round-r24-shapefix.json +0 -677
  193. package/scripts/bench/round-r25-serial.json +0 -464
  194. package/scripts/bench/round-r26-parallel3.json +0 -671
  195. package/scripts/bench/round-r27-parallel10.json +0 -894
  196. package/scripts/bench/round-r28-parallel10-stagger.json +0 -882
  197. package/scripts/bench/round-r29-parallel10-stagger166.json +0 -886
  198. package/scripts/bench/round-r30-instid.json +0 -253
  199. package/scripts/bench/round-r31-upgradeprobe.json +0 -256
  200. package/scripts/bench/round-r32-vs-codex-lead.json +0 -254
  201. package/scripts/bench/round-r33-vs-codex-codex.json +0 -115
  202. package/scripts/bench/round-r34-orchestrated.json +0 -120
  203. package/scripts/bench/round-r35-orchestrated-codex.json +0 -61
  204. package/scripts/bench/round-r36-orchestrated-capped.json +0 -128
  205. package/scripts/bench/round-r4-codex.json +0 -114
  206. package/scripts/bench/round-r4-mixed.json +0 -225
  207. package/scripts/bench/round-r5-gpt-lead.json +0 -259
  208. package/scripts/bench/round-r6-codex.json +0 -114
  209. package/scripts/bench/round-r6-solo.json +0 -257
  210. package/scripts/bench/round-r7-full.json +0 -254
  211. package/scripts/bench/round-r8-fulldefault.json +0 -255
  212. package/src/tui/components/prompt-input/voice-indicator.mjs +0 -39
  213. package/src/tui/lib/voice-recorder.mjs +0 -469
  214. package/src/workflows/sequential/WORKFLOW.md +0 -51
@@ -0,0 +1,201 @@
1
+ import { performance } from "perf_hooks";
2
+ // IPC worker-mode message loop extracted from channels/index.mjs
3
+ // (behavior-preserving). Installs shutdown handlers, the parent->worker message
4
+ // router, and the retrying start() bootstrap. Call once from the worker entry
5
+ // when _isWorkerMode && process.send.
6
+ export function runWorkerIpc({
7
+ start,
8
+ stop,
9
+ stopVoiceWhisperServer,
10
+ cleanupInstanceRuntimeFiles,
11
+ clearServerPid,
12
+ instanceId,
13
+ statusState,
14
+ getBackend,
15
+ getConfig,
16
+ pendingPermRequests,
17
+ refreshToolExecConsumerMarker,
18
+ handleMemoryCallResponse,
19
+ handleToolCallWithBridgeRetry,
20
+ bootProfile,
21
+ }) {
22
+ // SIGTERM/SIGINT/IPC shutdown handler — mirrors src/memory/index.mjs pattern.
23
+ // Cleans up in-progress webhook/scheduler state, removes runtime files, then exits.
24
+ let _channelsStopInFlight = false
25
+ let _channelsForceExitTimer = null
26
+ const _channelsShutdownHandler = async (sig) => {
27
+ if (_channelsStopInFlight) {
28
+ process.stderr.write(`[channels-worker] ${sig} — shutdown already in flight, ignoring\n`)
29
+ return
30
+ }
31
+ _channelsStopInFlight = true
32
+ process.stderr.write(`[channels-worker] received ${sig} — shutting down cleanly\n`)
33
+ _channelsForceExitTimer = setTimeout(() => {
34
+ process.stderr.write(`[channels-worker] stop() timed out after 6000ms — forcing exit(2)\n`)
35
+ process.exit(2)
36
+ }, 6000)
37
+ try { await stopVoiceWhisperServer() } catch (e) {
38
+ process.stderr.write(`[channels-worker] stopVoiceWhisperServer() error on ${sig}: ${e && (e.message || e)}\n`)
39
+ }
40
+ try { await stop() } catch (e) {
41
+ process.stderr.write(`[channels-worker] stop() error on ${sig}: ${e && (e.message || e)}\n`)
42
+ }
43
+ if (_channelsForceExitTimer) clearTimeout(_channelsForceExitTimer)
44
+ try { cleanupInstanceRuntimeFiles(instanceId) } catch {}
45
+ try { clearServerPid() } catch {}
46
+ process.exit(0)
47
+ }
48
+ process.on('SIGTERM', () => _channelsShutdownHandler('SIGTERM'))
49
+ process.on('SIGINT', () => _channelsShutdownHandler('SIGINT'))
50
+
51
+ // Map of callId → AbortController for in-flight IPC calls.
52
+ const _inFlightChannelCalls = new Map()
53
+
54
+ process.on('message', async (msg) => {
55
+ // Parent-initiated graceful shutdown — mirrors memory worker IPC pattern.
56
+ if (msg && msg.type === 'shutdown') {
57
+ process.stderr.write('[channels-worker] received IPC shutdown — calling stop()\n')
58
+ _channelsShutdownHandler('IPC:shutdown')
59
+ return
60
+ }
61
+ // Silent-to-agent lifecycle forward — parent (server.mjs) asks the
62
+ // channels worker to post status pings to the active bridge Discord
63
+ // channel without the Lead-notify hop. Best-effort: unknown channel or
64
+ // getBackend() failure is swallowed; lifecycle pings are non-critical.
65
+ if (msg && msg.type === 'forward_to_discord') {
66
+ try {
67
+ const target = msg.channelId
68
+ || (statusState?.read?.().channelId)
69
+ || null;
70
+ if (target && getBackend()?.sendMessage && typeof msg.content === 'string' && msg.content) {
71
+ await getBackend().sendMessage(target, msg.content).catch(() => {});
72
+ }
73
+ } catch { /* best-effort */ }
74
+ return;
75
+ }
76
+ // Host permission request → Discord Allow/Deny prompt.
77
+ // Parent (server.mjs) receives notifications/claude/channel/permission_request
78
+ // from the MCP host and forwards the params here. We post a buttoned message;
79
+ // button clicks are handled in getBackend().onInteraction and sent back to CC as
80
+ // notifications/claude/channel/permission via sendNotifyToParent.
81
+ if (msg && msg.type === 'permission_request_inbound') {
82
+ try {
83
+ const { request_id, tool_name, description, input_preview } = msg.params || {};
84
+ // tool_input arrives via the passthrough() schema in server.mjs when
85
+ // The host includes it in the permission_request notification.
86
+ // Used to bind the pendingPermRequest to a specific file so two
87
+ // concurrent Edit/Write requests cannot cross-approve via the
88
+ // terminal signal.
89
+ const toolInputParam = (msg.params && (msg.params.tool_input || msg.params.toolInput)) || {};
90
+ const filePathParam = toolInputParam.file_path || '';
91
+ if (!request_id || !tool_name) return;
92
+ if (pendingPermRequests.size > 100) {
93
+ const cutoff = Date.now() - 30 * 60 * 1000;
94
+ for (const [k, v] of pendingPermRequests) {
95
+ if (v.createdAt < cutoff) pendingPermRequests.delete(k);
96
+ }
97
+ refreshToolExecConsumerMarker();
98
+ }
99
+ const target = (statusState?.read?.().channelId)
100
+ || getConfig()?.channelId
101
+ || null;
102
+ if (!target || !getBackend()?.sendMessage) {
103
+ process.stderr.write(`mixdog channels: permission_request dropped, no target channel (request_id=${request_id})\n`);
104
+ return;
105
+ }
106
+ const lines = [`🔐 **Permission Request**`, `Tool: \`${tool_name}\``];
107
+ if (description) lines.push(description);
108
+ if (input_preview) lines.push('```\n' + String(input_preview).slice(0, 800) + '\n```');
109
+ const content = lines.join('\n');
110
+ const components = [{
111
+ type: 1,
112
+ components: [
113
+ { type: 2, style: 3, label: 'Allow', custom_id: `perm-ch-${request_id}-allow` },
114
+ { type: 2, style: 1, label: 'Session Allow', custom_id: `perm-ch-${request_id}-session` },
115
+ { type: 2, style: 4, label: 'Deny', custom_id: `perm-ch-${request_id}-deny` },
116
+ ],
117
+ }];
118
+ let sentIds = null;
119
+ try {
120
+ const sendResult = await getBackend().sendMessage(target, content, { components });
121
+ sentIds = sendResult?.sentIds;
122
+ } catch (err) {
123
+ process.stderr.write(`mixdog channels: permission_request Discord send failed: ${err && err.message || err}\n`);
124
+ return;
125
+ }
126
+ const messageId = Array.isArray(sentIds) && sentIds.length > 0 ? sentIds[0] : null;
127
+ pendingPermRequests.set(request_id, {
128
+ toolName: tool_name,
129
+ filePath: filePathParam,
130
+ createdAt: Date.now(),
131
+ channelId: target,
132
+ messageId,
133
+ });
134
+ refreshToolExecConsumerMarker();
135
+ } catch (err) {
136
+ try { process.stderr.write(`mixdog channels: permission_request handler error: ${err && err.message || err}\n`); } catch {}
137
+ }
138
+ return;
139
+ }
140
+ if (handleMemoryCallResponse(msg)) return;
141
+ if (msg.type === 'cancel' && msg.callId) {
142
+ const entry = _inFlightChannelCalls.get(msg.callId)
143
+ if (entry) {
144
+ entry.abort()
145
+ _inFlightChannelCalls.delete(msg.callId)
146
+ }
147
+ process.send({ type: 'result', callId: msg.callId, error: 'cancelled' })
148
+ return
149
+ }
150
+ if (msg.type !== 'call' || !msg.callId) return
151
+ try {
152
+ const ac = new AbortController()
153
+ _inFlightChannelCalls.set(msg.callId, ac)
154
+ let result
155
+ try {
156
+ result = await handleToolCallWithBridgeRetry(msg.name, msg.args || {}, ac.signal)
157
+ } finally {
158
+ _inFlightChannelCalls.delete(msg.callId)
159
+ }
160
+ process.send({ type: 'result', callId: msg.callId, result })
161
+ } catch (e) {
162
+ process.send({ type: 'result', callId: msg.callId, error: e.message })
163
+ }
164
+ })
165
+ void (async () => {
166
+ const startedAt = performance.now()
167
+ const MAX_START_ATTEMPTS = 3
168
+ const BASE_BACKOFF_MS = 250
169
+ const isTransientStartErr = (err) =>
170
+ err?.code === 'ELOCKTIMEOUT' || /atomic lock timeout/i.test(err?.message || '')
171
+ let lastErr
172
+ for (let attempt = 1; attempt <= MAX_START_ATTEMPTS; attempt++) {
173
+ if (_channelsStopInFlight) return
174
+ try {
175
+ await start()
176
+ bootProfile("worker:ready", { ms: (performance.now() - startedAt).toFixed(1), attempt })
177
+ process.send({ type: 'ready' })
178
+ return
179
+ } catch (e) {
180
+ lastErr = e
181
+ const transient = isTransientStartErr(e)
182
+ bootProfile("worker:start-failed", { attempt, transient, error: e?.message || String(e) })
183
+ process.stderr.write(`[channels-worker] start() failed (attempt ${attempt}/${MAX_START_ATTEMPTS}, transient=${transient}): ${e && (e.message || e)}\n`)
184
+ if (!transient || attempt >= MAX_START_ATTEMPTS) break
185
+ const backoff = BASE_BACKOFF_MS * attempt + Math.floor(Math.random() * BASE_BACKOFF_MS)
186
+ await new Promise((r) => setTimeout(r, backoff))
187
+ if (_channelsStopInFlight) return
188
+ }
189
+ }
190
+ // A stop landed while we were failing — let clean shutdown proceed, never exit over it.
191
+ if (_channelsStopInFlight) return
192
+ // Terminal failure: do NOT mask as a (degraded) ready. Exit non-zero so the
193
+ // parent's exit-before-ready path respawns or rejects startRemote instead of
194
+ // silently losing remote output forwarding.
195
+ bootProfile("worker:failed", { ms: (performance.now() - startedAt).toFixed(1), error: lastErr?.message || String(lastErr) })
196
+ process.stderr.write(`[channels-worker] start() giving up after ${MAX_START_ATTEMPTS} attempts: ${lastErr && (lastErr.message || lastErr)}\n`)
197
+ // Exit 2 = terminal (non-transient) start failure: parent must reject, not respawn.
198
+ // Exit 1 = exhausted transient retries: parent may respawn.
199
+ process.exit(isTransientStartErr(lastErr) ? 1 : 2)
200
+ })()
201
+ }