mixdog 0.9.95 → 0.9.97

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 (282) hide show
  1. package/LICENSES/Apache-2.0.txt +201 -0
  2. package/LICENSES/MIT.txt +56 -0
  3. package/LICENSES/codex-NOTICE.txt +6 -0
  4. package/NOTICE.md +46 -9
  5. package/README.md +0 -17
  6. package/package.json +16 -12
  7. package/scripts/.tmp-coverage-report.mjs +96 -0
  8. package/scripts/build-tui.mjs +17 -1
  9. package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
  10. package/scripts/fixtures/patch-replay-corpus.json +98 -0
  11. package/src/defaults/mixdog-config.template.json +5 -8
  12. package/src/defaults/skills/setup/SKILL.md +1 -1
  13. package/src/lib/keychain-cjs.cjs +1 -1
  14. package/src/lib/mixdog-debug.cjs +2 -3
  15. package/src/rules/agent/30-explorer.md +7 -4
  16. package/src/rules/lead/01-general.md +1 -2
  17. package/src/rules/lead/lead-tool.md +0 -3
  18. package/src/rules/shared/01-tool.md +21 -29
  19. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +18 -2
  20. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +31 -1
  21. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +6 -17
  22. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -1
  23. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
  24. package/src/runtime/agent/orchestrator/config.mjs +246 -67
  25. package/src/runtime/agent/orchestrator/mcp/client.mjs +104 -41
  26. package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +23 -0
  27. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +113 -14
  28. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +5 -1
  29. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +32 -11
  30. package/src/runtime/agent/orchestrator/providers/gemini.mjs +89 -2
  31. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -1
  32. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +19 -1
  33. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +144 -39
  34. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -8
  35. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -1
  36. package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +1 -1
  37. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +3 -4
  38. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
  39. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
  40. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +67 -7
  41. package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +291 -0
  42. package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +21 -0
  43. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +36 -10
  44. package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
  45. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -33
  46. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +13 -0
  47. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +2 -2
  48. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +0 -24
  49. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +21 -29
  50. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +7 -10
  51. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +45 -25
  52. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
  53. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +142 -84
  54. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
  55. package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +4 -0
  56. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +31 -1
  57. package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
  58. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +29 -9
  59. package/src/runtime/agent/orchestrator/session/manager.mjs +2 -0
  60. package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
  61. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +86 -8
  62. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
  63. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
  64. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +71 -42
  65. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +4 -33
  66. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +4 -5
  67. package/src/runtime/agent/orchestrator/stall-policy.mjs +1 -1
  68. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
  69. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +35 -1
  70. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +116 -26
  71. package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
  72. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +8 -9
  73. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +2 -2
  74. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +2 -2
  75. package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
  76. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +6 -15
  77. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +12 -26
  78. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +5 -5
  79. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +83 -33
  80. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +42 -45
  81. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +27 -9
  82. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +7 -0
  83. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +17 -39
  84. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +73 -0
  85. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +1 -1
  86. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +0 -2
  87. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +3 -8
  88. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +30 -0
  89. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +5 -5
  90. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +142 -26
  91. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
  92. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +47 -19
  93. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -2
  94. package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
  95. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +83 -13
  96. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +39 -1
  97. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
  98. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +94 -20
  99. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +16 -0
  100. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +108 -12
  101. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +12 -11
  102. package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
  103. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +63 -9
  104. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
  105. package/src/runtime/channels/lib/config.mjs +34 -34
  106. package/src/runtime/channels/lib/inbound-handler.mjs +8 -8
  107. package/src/runtime/channels/lib/interaction-handlers.mjs +6 -6
  108. package/src/runtime/channels/lib/memory-client.mjs +31 -157
  109. package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
  110. package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
  111. package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
  112. package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
  113. package/src/runtime/channels/lib/runtime-paths.mjs +4 -30
  114. package/src/runtime/channels/lib/scheduler.mjs +2 -2
  115. package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
  116. package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
  117. package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
  118. package/src/runtime/channels/lib/worker-ipc.mjs +7 -7
  119. package/src/runtime/channels/lib/worker-main.mjs +38 -38
  120. package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
  121. package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
  122. package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
  123. package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
  124. package/src/runtime/channels/tool-defs.mjs +1 -1
  125. package/src/runtime/memory/index.mjs +76 -248
  126. package/src/runtime/memory/lib/agent-ipc.mjs +146 -81
  127. package/src/runtime/memory/lib/compact-vector-cache.mjs +88 -0
  128. package/src/runtime/memory/lib/core-memory-store.mjs +18 -1
  129. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +13 -34
  130. package/src/runtime/memory/lib/embedding-provider.mjs +47 -26
  131. package/src/runtime/memory/lib/embedding-worker.mjs +30 -34
  132. package/src/runtime/memory/lib/http-router.mjs +9 -6
  133. package/src/runtime/memory/lib/ko-morph.mjs +3 -20
  134. package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -1
  135. package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
  136. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -0
  137. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -2
  138. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -0
  139. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
  140. package/src/runtime/memory/lib/memory-cycle3.mjs +3 -1
  141. package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
  142. package/src/runtime/memory/lib/memory-log.mjs +1 -1
  143. package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
  144. package/src/runtime/memory/lib/memory.mjs +1 -1
  145. package/src/runtime/memory/lib/pg/process.mjs +32 -9
  146. package/src/runtime/memory/lib/session-ingest.mjs +1 -1
  147. package/src/runtime/memory/tool-defs.mjs +1 -5
  148. package/src/runtime/search/index.mjs +2 -2
  149. package/src/runtime/search/tool-defs.mjs +2 -2
  150. package/src/runtime/shared/abort-race.mjs +70 -0
  151. package/src/runtime/shared/agent-route-config.mjs +119 -0
  152. package/src/runtime/shared/atomic-file.mjs +80 -4
  153. package/src/runtime/shared/channel-notification-routing.mjs +2 -2
  154. package/src/runtime/shared/child-guardian.mjs +204 -108
  155. package/src/runtime/shared/child-spawn-gate.mjs +139 -72
  156. package/src/runtime/shared/config.mjs +120 -47
  157. package/src/runtime/shared/llm/index.mjs +1 -1
  158. package/src/runtime/shared/owner-fair-gate.mjs +134 -0
  159. package/src/runtime/shared/resource-admission.mjs +150 -100
  160. package/src/runtime/shared/stream-progress.mjs +6 -0
  161. package/src/runtime/shared/tool-execution-owner.mjs +12 -0
  162. package/src/runtime/shared/tool-result-summary.mjs +1 -1
  163. package/src/runtime/shared/tool-surface.mjs +91 -27
  164. package/src/runtime/shared/tool-workload-gates.mjs +29 -0
  165. package/src/runtime/shared/turn-snapshot.mjs +47 -6
  166. package/src/runtime/shared/turn-worktree-snapshot.mjs +14 -2
  167. package/src/session-runtime/channel-config-api.mjs +5 -5
  168. package/src/session-runtime/config-helpers.mjs +39 -37
  169. package/src/session-runtime/config-lifecycle.mjs +27 -27
  170. package/src/session-runtime/lifecycle-api.mjs +22 -3
  171. package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
  172. package/src/session-runtime/model-route-api.mjs +7 -5
  173. package/src/session-runtime/prewarm.mjs +1 -1
  174. package/src/session-runtime/provider-auth-api.mjs +0 -7
  175. package/src/session-runtime/provider-models.mjs +1 -1
  176. package/src/session-runtime/remote-control.mjs +6 -7
  177. package/src/session-runtime/runtime-core.mjs +63 -39
  178. package/src/session-runtime/runtime-tunables.mjs +4 -4
  179. package/src/session-runtime/self-update.mjs +1 -1
  180. package/src/session-runtime/session-lifecycle.mjs +45 -17
  181. package/src/session-runtime/session-title.mjs +1 -1
  182. package/src/session-runtime/session-turn-api.mjs +96 -24
  183. package/src/session-runtime/settings-api.mjs +12 -24
  184. package/src/session-runtime/tool-defs.mjs +3 -3
  185. package/src/session-runtime/workflow-agents-api.mjs +55 -81
  186. package/src/session-runtime/workflow.mjs +14 -24
  187. package/src/standalone/agent-dispatch-broker.mjs +198 -0
  188. package/src/standalone/agent-tool/helpers.mjs +2 -2
  189. package/src/standalone/agent-tool/spawn-flow.mjs +2 -0
  190. package/src/standalone/agent-tool/spawn-preset.mjs +11 -21
  191. package/src/standalone/agent-tool/tool-def.mjs +0 -14
  192. package/src/standalone/channel-admin.mjs +53 -64
  193. package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +33 -19
  194. package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +127 -46
  195. package/src/standalone/channel-worker.mjs +124 -647
  196. package/src/standalone/{backend-daemon.mjs → daemon.mjs} +270 -154
  197. package/src/standalone/explore-tool.mjs +187 -70
  198. package/src/standalone/fair-call-scheduler.mjs +264 -0
  199. package/src/standalone/memory-runtime-proxy.mjs +58 -1
  200. package/src/standalone/session-client.mjs +1134 -0
  201. package/src/standalone/session-protocol.mjs +192 -0
  202. package/src/standalone/session-runtime-pool.mjs +393 -0
  203. package/src/standalone/session-runtime-record.mjs +20 -0
  204. package/src/standalone/session-runtime-worker.mjs +203 -0
  205. package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +481 -224
  206. package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +390 -121
  207. package/src/standalone/session-wire.mjs +55 -0
  208. package/src/tui/App.jsx +34 -21
  209. package/src/tui/app/app-format.mjs +1 -1
  210. package/src/tui/app/app-view.jsx +5 -2
  211. package/src/tui/app/channel-pickers.mjs +41 -41
  212. package/src/tui/app/core-memory-picker.mjs +19 -19
  213. package/src/tui/app/doctor.mjs +5 -11
  214. package/src/tui/app/input-parsers.mjs +2 -2
  215. package/src/tui/app/message-selector.mjs +103 -0
  216. package/src/tui/app/onboarding-steps.mjs +3 -4
  217. package/src/tui/app/project-picker.mjs +80 -56
  218. package/src/tui/app/prompt-submit.mjs +48 -43
  219. package/src/tui/app/resume-picker.mjs +2 -2
  220. package/src/tui/app/route-pickers.mjs +3 -1
  221. package/src/tui/app/settings-picker.mjs +31 -33
  222. package/src/tui/app/transcript-window.mjs +38 -0
  223. package/src/tui/app/use-prompt-draft-flow.mjs +5 -7
  224. package/src/tui/app/use-prompt-handlers.mjs +86 -21
  225. package/src/tui/app/use-prompt-hint.mjs +3 -2
  226. package/src/tui/app/use-prompt-queue-history.mjs +10 -3
  227. package/src/tui/app/use-transcript-scroll.mjs +10 -3
  228. package/src/tui/app/use-transcript-window.mjs +50 -17
  229. package/src/tui/components/PromptInput.jsx +71 -38
  230. package/src/tui/components/Spinner.jsx +1 -1
  231. package/src/tui/components/ToolExecution.jsx +1 -1
  232. package/src/tui/components/prompt-input/edit-helpers.mjs +9 -11
  233. package/src/tui/components/prompt-input/escape-policy.mjs +42 -0
  234. package/src/tui/components/prompt-input/immediate-render.mjs +0 -10
  235. package/src/tui/components/prompt-input/interrupt-policy.mjs +10 -0
  236. package/src/tui/components/prompt-input/restore-policy.mjs +10 -0
  237. package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
  238. package/src/tui/dist/index.mjs +1031 -1676
  239. package/src/tui/hooks/useSession.mjs +16 -0
  240. package/src/tui/index.jsx +12 -13
  241. package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
  242. package/src/tui/{engine → session}/agent-job-feed.mjs +4 -4
  243. package/src/tui/{engine → session}/boot-profile.mjs +2 -2
  244. package/src/tui/{engine → session}/context-state.mjs +3 -3
  245. package/src/tui/{engine → session}/labels.mjs +2 -2
  246. package/src/tui/{engine → session}/live-share.mjs +38 -4
  247. package/src/tui/{engine → session}/notice-text.mjs +2 -2
  248. package/src/tui/{engine → session}/notification-plan.mjs +3 -3
  249. package/src/tui/session/oauth-flows.mjs +140 -0
  250. package/src/tui/{engine → session}/prompt-history.mjs +3 -3
  251. package/src/tui/{engine → session}/queue-helpers.mjs +2 -2
  252. package/src/tui/{engine → session}/render-timing.mjs +2 -2
  253. package/src/tui/{engine → session}/session-api-ext.mjs +30 -20
  254. package/src/tui/{engine → session}/session-api.mjs +110 -93
  255. package/src/tui/{engine → session}/session-flow.mjs +8 -2
  256. package/src/tui/{engine → session}/session-stats.mjs +3 -3
  257. package/src/tui/{engine → session}/tool-approval.mjs +3 -3
  258. package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
  259. package/src/tui/{engine → session}/tool-card-results.mjs +8 -6
  260. package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
  261. package/src/tui/{engine → session}/tool-result-text.mjs +13 -3
  262. package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
  263. package/src/tui/{engine → session}/turn.mjs +38 -176
  264. package/src/tui/{engine-local-session.mjs → session-local.mjs} +103 -103
  265. package/src/tui/session.mjs +22 -0
  266. package/src/tui/spinner-meta.mjs +4 -4
  267. package/src/tui/spinner-verbs.mjs +51 -3
  268. package/src/ui/statusline-segments.mjs +1 -1
  269. package/src/ui/statusline.mjs +1 -1
  270. package/vendor/ink/build/components/App.js +10 -1
  271. package/src/session-runtime/memory-daemon-probe.mjs +0 -61
  272. package/src/standalone/engine-daemon-client.mjs +0 -894
  273. package/src/standalone/engine-daemon-local-bridge.mjs +0 -20
  274. package/src/standalone/engine-daemon-protocol.mjs +0 -33
  275. package/src/tui/engine/turn-watchdog.mjs +0 -92
  276. package/src/tui/engine.mjs +0 -22
  277. package/src/tui/hooks/useEngine.mjs +0 -16
  278. /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
  279. /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
  280. /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
  281. /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
  282. /package/src/tui/{engine → session}/tui-steering-persist.mjs +0 -0
@@ -28,6 +28,8 @@ import {
28
28
  findLineSequence,
29
29
  findLineSequenceEscapeEquiv,
30
30
  longestCommonSubstringLen,
31
+ boundedEditDistance,
32
+ EDIT_DISTANCE_ALLOWANCE_PER_LINE,
31
33
  splitTextLinesForPatch,
32
34
  firstMeaningfulPatchLine,
33
35
  nearestPatchLineHint,
@@ -162,6 +164,18 @@ function noteV4AHunkAmbiguity(displayPath, sourceLines, loc) {
162
164
  );
163
165
  }
164
166
 
167
+ // `*** End of File` was relaxed to locate this hunk: report where it actually
168
+ // landed so a mid-file hunk that carried the marker by mistake is visible in
169
+ // the same turn instead of silently looking like an end-of-file edit.
170
+ function noteV4AEofSignalIgnored(displayPath, loc) {
171
+ if (_v4aAmbiguityNotices.size >= V4A_AMBIGUITY_NOTICE_CAP) return;
172
+ if (!loc?.eofSignalIgnored) return;
173
+ _v4aAmbiguityNotices.add(
174
+ `${displayPath}: hunk carried *** End of File but its context is at line ${loc.oldStartIdx + 1}, `
175
+ + 'not the end of file; the marker was ignored. Drop it unless the hunk really ends the file.',
176
+ );
177
+ }
178
+
165
179
  export function drainV4AAmbiguityNotices() {
166
180
  if (_v4aAmbiguityNotices.size === 0) return [];
167
181
  const list = [..._v4aAmbiguityNotices];
@@ -299,14 +313,50 @@ function findContextTolerantWindow(sourceLines, oldLines, oldTags) {
299
313
  return windows.length === 1 ? { start: windows[0] } : null;
300
314
  }
301
315
 
302
- // Last-resort recovery for a model that accidentally copied 1-2 unrelated
303
- // unchanged context lines just outside the real edit. Trim only contiguous
304
- // outer ' ' lines — never '-' or '+' lines — and accept the shortened old
305
- // block only when it matches byte-exactly at exactly one place in the ENTIRE
306
- // file. Requiring a deletion keeps this fallback off insertion-only hunks;
307
- // exact whole-block matching proves every deletion line is current. Any
308
- // competing trim plan or duplicate occurrence is ambiguous and stays a hard
309
- // context miss.
316
+ // Bounded edit-distance tier (fuzzy, after the context-tolerance tier).
317
+ // Recovers the block a model retyped from memory with a couple of characters
318
+ // off — a dropped bracket, a mistyped short token — where the tolerance tier
319
+ // refuses because the drift sits on a deletion line. Guards keep it safe:
320
+ // - TOTAL distance across the block stays within ~0.34 characters per line
321
+ // (3 lines buy exactly 1 character), measured on trimmed text;
322
+ // - at least 2 non-blank lines must match byte-exactly as anchors;
323
+ // - the qualifying window must be UNIQUE across the ENTIRE file;
324
+ // - the caller REMAPS every old line to the file's on-disk text, so the
325
+ // emitted hunk stays byte-exact for the applier.
326
+ function findEditDistanceWindow(sourceLines, oldLines) {
327
+ const n = (oldLines || []).length;
328
+ const maxDistance = Math.floor(n * EDIT_DISTANCE_ALLOWANCE_PER_LINE);
329
+ if (n < 3 || maxDistance <= 0) return null;
330
+ const windows = [];
331
+ for (let i = 0; i + n <= sourceLines.length && windows.length < 2; i++) {
332
+ let total = 0;
333
+ let exact = 0;
334
+ let ok = true;
335
+ for (let k = 0; k < n; k++) {
336
+ const pat = String(oldLines[k] ?? '');
337
+ const src = String(sourceLines[i + k] ?? '');
338
+ if (src === pat) {
339
+ if (pat.trim()) exact++;
340
+ continue;
341
+ }
342
+ total += boundedEditDistance(src.trim(), pat.trim(), maxDistance - total);
343
+ if (total > maxDistance) { ok = false; break; }
344
+ }
345
+ if (ok && total > 0 && exact >= 2) windows.push(i);
346
+ }
347
+ return windows.length === 1 ? { start: windows[0] } : null;
348
+ }
349
+
350
+ // Recovery for outer context the model got wrong — a stray copied line, or a
351
+ // whole surrounding block retyped from memory — around an edit whose deletion
352
+ // lines are current. Trim only contiguous outer ' ' lines — never '-' or '+'
353
+ // lines — smallest trim first, and accept the shortened old block only when it
354
+ // matches byte-exactly at exactly one place in the ENTIRE file. That
355
+ // uniqueness IS the position proof, so the trim budget runs to all available
356
+ // outer context: a byte-exact, file-unique deletion core cannot land anywhere
357
+ // else, whatever the caller wrote around it. Requiring a deletion keeps this
358
+ // off insertion-only hunks (they carry no payload to anchor on), and any
359
+ // competing trim plan or duplicate occurrence stays a hard context miss.
310
360
  function findOuterContextTrimmedWindow(sourceLines, hunk, stats) {
311
361
  const body = (hunk?.lines || []).filter(
312
362
  (line) => typeof line === 'string'
@@ -325,7 +375,8 @@ function findOuterContextTrimmedWindow(sourceLines, hunk, stats) {
325
375
  ) trailingAvailable++;
326
376
  if (leadingAvailable === 0 && trailingAvailable === 0) return null;
327
377
 
328
- for (let totalTrimmed = 1; totalTrimmed <= 2; totalTrimmed++) {
378
+ const maxTrimmed = leadingAvailable + trailingAvailable;
379
+ for (let totalTrimmed = 1; totalTrimmed <= maxTrimmed; totalTrimmed++) {
329
380
  const plans = [];
330
381
  let ambiguous = false;
331
382
  for (let leading = 0; leading <= Math.min(totalTrimmed, leadingAvailable); leading++) {
@@ -424,7 +475,23 @@ function resolveV4AHunkPosition(sourceLines, hunk, nextSearchLine, options = {})
424
475
  // of the patch's real character. On match, remap old/context lines to the
425
476
  // file's on-disk form so untouched context stays byte-identical and the
426
477
  // escape representation survives the edit.
427
- if (oldStartIdx < 0 && fuzzy && !eof && oldLinesPattern.length > 0) {
478
+ // `*** End of File` is a HINT, not a hard constraint. When the EOF-anchored
479
+ // seek misses, retry the pattern as an ordinary in-file search: a mid-file
480
+ // hunk that carried the marker by mistake used to fail with a "context not
481
+ // found" that pointed at its own byte-perfect context, costing a whole retry
482
+ // turn. Once that happens the EOF signal is exhausted, so every recovery
483
+ // tier below treats the hunk as unmarked.
484
+ let eofSignalIgnored = false;
485
+ if (oldStartIdx < 0 && eof && oldLinesPattern.length > 0) {
486
+ const relaxedFrom = Math.max(0, anchorLine - 1);
487
+ const relaxed = findLineSequence(sourceLines, oldLinesPattern, relaxedFrom, relaxedFrom, { fuzzy, eof: false });
488
+ if (relaxed >= 0) {
489
+ oldStartIdx = relaxed;
490
+ eofSignalIgnored = true;
491
+ }
492
+ }
493
+ if (oldStartIdx < 0 && fuzzy && oldLinesPattern.length > 0) {
494
+ if (eof) eofSignalIgnored = true;
428
495
  const from = Math.max(0, anchorLine - 1);
429
496
  const alt = findLineSequenceEscapeEquiv(sourceLines, oldLinesPattern, from, from);
430
497
  if (alt >= 0) {
@@ -447,7 +514,7 @@ function resolveV4AHunkPosition(sourceLines, hunk, nextSearchLine, options = {})
447
514
  // lines are remapped to the file's actual lines on BOTH the old and new
448
515
  // side (same shape as the escape-equiv remap above); remapping bails on any
449
516
  // string ambiguity so an unrelated occurrence can never be rewritten.
450
- if (oldStartIdx < 0 && fuzzy && !eof && oldLinesPattern.length >= 3) {
517
+ if (oldStartIdx < 0 && fuzzy && oldLinesPattern.length >= 3) {
451
518
  const tolTags = trimmedTrailing ? stats.oldTags.slice(0, -1) : stats.oldTags;
452
519
  const tol = findContextTolerantWindow(sourceLines, oldLinesPattern, tolTags);
453
520
  if (tol) {
@@ -474,7 +541,7 @@ function resolveV4AHunkPosition(sourceLines, hunk, nextSearchLine, options = {})
474
541
  // This fallback is deliberately narrower than the context-tolerance tier:
475
542
  // fuzzy mode only, non-EOF, no newline-sentinel interaction, deletion hunks
476
543
  // only, byte-exact remaining block, and one unique whole-file location.
477
- if (oldStartIdx < 0 && fuzzy && !eof && trimmedTrailing === 0) {
544
+ if (oldStartIdx < 0 && fuzzy && trimmedTrailing === 0) {
478
545
  const trimmed = findOuterContextTrimmedWindow(sourceLines, hunk, stats);
479
546
  if (trimmed) {
480
547
  oldStartIdx = trimmed.start;
@@ -484,6 +551,31 @@ function resolveV4AHunkPosition(sourceLines, hunk, nextSearchLine, options = {})
484
551
  trimmedTrailingContext = trimmed.trailing;
485
552
  }
486
553
  }
554
+ // Edit-distance tier: same remap contract as the context-tolerance tier —
555
+ // the window's on-disk lines replace the patch's near-miss lines on BOTH
556
+ // sides, and any string ambiguity abandons the rescue.
557
+ if (oldStartIdx < 0 && fuzzy && oldLinesPattern.length >= 3) {
558
+ const near = findEditDistanceWindow(sourceLines, oldLinesPattern);
559
+ if (near) {
560
+ const remapped = new Map();
561
+ let ambiguous = false;
562
+ for (let k = 0; k < oldLinesPattern.length; k++) {
563
+ const pat = oldLinesPattern[k];
564
+ const src = sourceLines[near.start + k];
565
+ if (src === pat) continue;
566
+ if (
567
+ oldLinesPattern.filter((l) => l === pat).length > 1
568
+ || (remapped.has(pat) && remapped.get(pat) !== src)
569
+ ) { ambiguous = true; break; }
570
+ remapped.set(pat, src);
571
+ }
572
+ if (!ambiguous) {
573
+ newLinesPattern = newLinesPattern.map((l) => remapped.get(l) ?? l);
574
+ oldLinesPattern = oldLinesPattern.map((_, k) => sourceLines[near.start + k]);
575
+ oldStartIdx = near.start;
576
+ }
577
+ }
578
+ }
487
579
  if (oldStartIdx < 0) {
488
580
  const msg = `V4A hunk context not found: ${formatV4AHunkLocator(hunk)};${formatV4AContextMissHint(sourceLines, stats, anchorLine)}`;
489
581
  return { error: msg };
@@ -506,6 +598,9 @@ function resolveV4AHunkPosition(sourceLines, hunk, nextSearchLine, options = {})
506
598
  // hunk carried an @@ anchor (an anchored hunk is never ambiguous).
507
599
  pattern: oldLinesPattern,
508
600
  anchored: (hunk.anchors || []).filter(Boolean).length > 0,
601
+ // True when the hunk's `*** End of File` marker had to be relaxed to
602
+ // locate it (drives the non-fatal notice on the success output).
603
+ eofSignalIgnored,
509
604
  };
510
605
  }
511
606
 
@@ -1011,6 +1106,7 @@ export async function convertV4ASectionsToUnifiedPatch(sections, basePath, optio
1011
1106
  ],
1012
1107
  });
1013
1108
  noteV4AHunkAmbiguity(displayPath, sourceLines, loc);
1109
+ noteV4AEofSignalIgnored(displayPath, loc);
1014
1110
  nextSearchLine = loc.nextSearchLine;
1015
1111
  }
1016
1112
  sectionHunkEntries.sort((a, b) => (a.start - b.start) || (a.order - b.order));
@@ -1,5 +1,6 @@
1
- const APPLY_PATCH_LARK_GRAMMAR = `start: begin_patch hunk+ end_patch
1
+ const APPLY_PATCH_LARK_GRAMMAR = `start: begin_patch root_line? hunk+ end_patch
2
2
  begin_patch: "*** Begin Patch" LF
3
+ root_line: "*** Root: " filename LF
3
4
  end_patch: "*** End Patch" LF?
4
5
 
5
6
  hunk: add_hunk | delete_hunk | update_hunk
@@ -28,7 +29,7 @@ eof_line: "*** End of File" LF
28
29
  // Batching stays a rules-level policy: every new edit goes in one patch, with
29
30
  // one file block per target.
30
31
  const APPLY_PATCH_FREEFORM_DESCRIPTION =
31
- 'Use the `apply_patch` tool to edit files. This is a FREEFORM tool, so do not wrap the patch in JSON.';
32
+ 'Edit files with `apply_patch`. FREEFORM input; do not wrap the patch in JSON.';
32
33
 
33
34
  // JSON-schema fallback providers (Anthropic and other non-grammar surfaces)
34
35
  // get the full V4A instructions inline: without a grammar the model has
@@ -41,13 +42,10 @@ const APPLY_PATCH_JSON_DESCRIPTION = [
41
42
  '*** Begin Patch',
42
43
  '[file sections]',
43
44
  '*** End Patch',
44
- 'Every section starts with exactly one header:',
45
- '- *** Add File: <path> then +content lines.',
46
- '- *** Delete File: <path> with nothing after it.',
47
- '- *** Update File: <path>, optionally followed by *** Move to: <new path>.',
48
- 'Update hunks open with @@ or @@ <enclosing class/function>; prefix every line with space (context), - (remove), or + (add); a hunk at end of file may close with *** End of File.',
49
- 'Copy 3 context lines above and below verbatim from the newest tool output of the file (after your own patch, use post-patch content; never retype from memory). Do not duplicate overlapping context; if context is not unique, add enclosing @@ headers.',
50
- 'Use project-relative paths. Every added file line needs +. Never submit a compacted-history marker; re-read and create a fresh patch.',
45
+ 'Every section starts with exactly one header: *** Add File: <path> (+content lines), *** Delete File: <path> (nothing after), *** Update File: <path> (optional *** Move to: <new path>).',
46
+ 'Hunks open with @@ or @@ <symbol>; prefix lines with space, -, or +; an end-of-file hunk may close with *** End of File.',
47
+ 'Copy 3 context lines above/below verbatim from the newest tool output — after your own patch use its post-patch body, never memory. No duplicate overlapping context; if still ambiguous, stack @@ headers: @@ class Foo then @@ def bar.',
48
+ 'Project-relative paths; every added line needs +. Never submit a compacted-history marker — re-read and send a fresh patch.',
51
49
  ].join('\n');
52
50
 
53
51
  export const PATCH_TOOL_DEFS = [
@@ -65,8 +63,11 @@ export const PATCH_TOOL_DEFS = [
65
63
  inputSchema: {
66
64
  type: 'object',
67
65
  properties: {
68
- patch: { type: 'string', description: 'The V4A patch text to apply (format and context rules in the tool description).' },
69
- post_shell: { type: 'string', description: 'Verification command run when the patch applies; skipped on patch failure.' },
66
+ patch: { type: 'string', description: 'V4A patch text to apply; rules above.' },
67
+ root: {
68
+ type: 'string',
69
+ description: 'Write root; required only for targets outside the session directory.',
70
+ },
70
71
  },
71
72
  required: ['patch'],
72
73
  additionalProperties: false,
@@ -20,7 +20,7 @@
20
20
  // ./patch/orchestrator.mjs — apply_patch orchestration + executePatchTool
21
21
  // + replay capture + UI-diff side-channel.
22
22
  //
23
- // Backend is unchanged: NATIVE-ONLY dispatch to the mixdog-patch Rust engine,
23
+ // Executor is unchanged: NATIVE-ONLY dispatch to the mixdog-patch Rust engine,
24
24
  // no JS apply fallback. Public exports below match the pre-split surface.
25
25
 
26
26
  // Side-effect import: schedules the native-patch prewarm on module load, as
@@ -75,8 +75,8 @@ const SHELL_ADMISSION_WAIT_MS = Number.isFinite(_envAdmissionWait) && _envAdmiss
75
75
  ? _envAdmissionWait
76
76
  : 30_000;
77
77
 
78
- // CC parity default: capture child output via file fds (TaskOutput direct
79
- // mode) instead of parent-side pipes. Opt back into pipe capture with
78
+ // Default: capture child output via file fds (direct mode) instead of
79
+ // parent-side pipes. Opt back into pipe capture with
80
80
  // MIXDOG_SHELL_PIPE_CAPTURE=1 (diagnostic escape hatch).
81
81
  // win32 EXCEPTION: fd-based stdio entries are UV_INHERIT_FD, which makes
82
82
  // libuv DROP CREATE_NO_WINDOW (libuv PR #1659) — the child shell then
@@ -145,9 +145,30 @@ function _abortableDelay(ms, signal) {
145
145
  });
146
146
  }
147
147
 
148
- export async function acquireShellLeaseBounded(admission, { abortSignal, label, dependency = 'scoped' } = {}) {
148
+ // True when an abort was raised because the user sent a NEW message while the
149
+ // command was running (session-api.mjs raises 'interrupt' when steering is
150
+ // pending, 'user-cancel' for a plain ESC). The reason travels as the
151
+ // SessionClosedError's `reason` field; a bare string / message fallback keeps
152
+ // non-session callers working. Anything unrecognized is treated as a real
153
+ // cancellation, so the kill path stays the default.
154
+ export function _abortReasonIsInterrupt(abortSignal) {
155
+ const raw = abortSignal?.reason;
156
+ if (!raw) return false;
157
+ if (typeof raw === 'string') return raw === 'interrupt';
158
+ if (typeof raw === 'object') {
159
+ if (raw.reason === 'interrupt') return true;
160
+ if (typeof raw.message === 'string' && /\breason=interrupt\b/.test(raw.message)) return true;
161
+ }
162
+ return false;
163
+ }
164
+
165
+ export async function acquireShellLeaseBounded(admission, {
166
+ abortSignal, label, dependency = 'scoped', ownerKey = null,
167
+ } = {}) {
149
168
  if (!(SHELL_ADMISSION_WAIT_MS > 0)) {
150
- return admission.acquire('shell', { signal: abortSignal || null, label, dependency });
169
+ return admission.acquire('shell', {
170
+ signal: abortSignal || null, label, dependency, ownerKey,
171
+ });
151
172
  }
152
173
  const ctl = new AbortController();
153
174
  const onAbort = () => {
@@ -165,7 +186,9 @@ export async function acquireShellLeaseBounded(admission, { abortSignal, label,
165
186
  try {
166
187
  for (;;) {
167
188
  try {
168
- const lease = await admission.acquire('shell', { signal: ctl.signal, label, dependency });
189
+ const lease = await admission.acquire('shell', {
190
+ signal: ctl.signal, label, dependency, ownerKey,
191
+ });
169
192
  // Hand governance back to the caller's signal: the internal deadline
170
193
  // controller may still fire in a lost race after grant, and a stale
171
194
  // aborted signal on the lease would poison later parent-restore paths.
@@ -235,6 +258,7 @@ export function execShellCommand({
235
258
  ownerSessionId,
236
259
  backgroundOnTimeout,
237
260
  promotedTimeoutMs = 0,
261
+ backgroundDeadlineMs = 0,
238
262
  admission = resourceAdmission,
239
263
  }) {
240
264
  return new Promise(async (resolve) => {
@@ -363,6 +387,7 @@ export function execShellCommand({
363
387
  resourceLease = await acquireShellLeaseBounded(admission, {
364
388
  abortSignal,
365
389
  label: String(command || '').slice(0, 120),
390
+ ownerKey: ownerSessionId,
366
391
  });
367
392
  const _policyErr = await _execPolicyBlockMessage(command);
368
393
  if (_policyErr) {
@@ -443,7 +468,6 @@ export function execShellCommand({
443
468
  childPid: child.pid,
444
469
  childGroupPid: child.pid,
445
470
  label: 'shell-command',
446
- protectHostMemory: true,
447
471
  });
448
472
  // Windows has no process-group handle. Begin observing while the owned
449
473
  // root still exists so descendants can be proven before root exit.
@@ -768,7 +792,12 @@ export function execShellCommand({
768
792
  const stdoutPath = taskOutput.spilled ? taskOutput.stdoutPath : null;
769
793
  const stderrPath = taskOutput.spilled ? taskOutput.stderrPath : null;
770
794
  let job = null;
771
- const adoptedTimeoutMs = reason === 'timeout' ? promotedTimeoutMs : 0;
795
+ const elapsedMs = Math.max(0, Date.now() - _startMs);
796
+ const adoptedTimeoutMs = reason === 'timeout'
797
+ ? promotedTimeoutMs
798
+ : (backgroundDeadlineMs > 0
799
+ ? Math.max(1, backgroundDeadlineMs - elapsedMs)
800
+ : 0);
772
801
  try {
773
802
  job = adoptForegroundShellJob({
774
803
  command,
@@ -830,7 +859,12 @@ export function execShellCommand({
830
859
  // Re-check after the awaited capture reads: cancellation can race after
831
860
  // adoption commits. Never report that cancelled process as a successful
832
861
  // still-running background task.
833
- if (abortSignal && abortSignal.aborted) {
862
+ // EXCEPTION: an interrupt-driven promotion starts FROM an aborted signal
863
+ // by design (the user typed a new message), so this guard must not undo
864
+ // the very transition it was asked to perform. A plain cancellation still
865
+ // reverts adoption and kills.
866
+ if (abortSignal && abortSignal.aborted
867
+ && !(reason === 'interrupt' && _abortReasonIsInterrupt(abortSignal))) {
834
868
  killed = true;
835
869
  killCause = 'cancellation';
836
870
  try { killShellJob(jobId); } catch {}
@@ -855,7 +889,7 @@ export function execShellCommand({
855
889
  }));
856
890
  return;
857
891
  }
858
- // CC parity (BashTool completed-during-promotion race): the child
892
+ // Completed-during-promotion race: the child
859
893
  // finished while adoption was committing. Report a clean COMPLETED
860
894
  // result instead of backgrounded — the caller then never arms the
861
895
  // completion watcher, so no redundant task notification fires. Write
@@ -1037,6 +1071,26 @@ export function execShellCommand({
1037
1071
 
1038
1072
  if (abortSignal) {
1039
1073
  abortHandler = () => {
1074
+ // Interrupt (the user typed a NEW message while this was running) is a
1075
+ // "also look at this" signal, not "stop that" — CC backgrounds instead
1076
+ // of killing there, and throwing away a long build the user never asked
1077
+ // to stop is the worse outcome. Explicit cancellation (ESC) keeps the
1078
+ // kill. The promotion reuses the timeout path's guards verbatim so a
1079
+ // detached / already-settled / already-exited child can never be
1080
+ // adopted as a job.
1081
+ if (
1082
+ _abortReasonIsInterrupt(abortSignal) &&
1083
+ backgroundOnTimeout &&
1084
+ !_isBackground &&
1085
+ !settled &&
1086
+ !autoBackgrounded &&
1087
+ !killed &&
1088
+ child?.exitCode == null &&
1089
+ child?.signalCode == null
1090
+ ) {
1091
+ fireAutoBackground({ reason: 'interrupt' });
1092
+ return;
1093
+ }
1040
1094
  _treeKillForceSettle('cancellation');
1041
1095
  };
1042
1096
  try {
@@ -209,7 +209,7 @@ export class TaskOutput {
209
209
  this.stdoutFileSize = 0;
210
210
  this.stderrFileSize = 0;
211
211
  this.writeError = null;
212
- // CC parity: direct-capture mode hands the spill files to the child as
212
+ // Direct-capture mode hands the spill files to the child as
213
213
  // stdio fds (no JS pipes). Sizes are then tracked via stat, not writes.
214
214
  this.direct = false;
215
215
  // fsync throttle: task wait/read + tail-read polling can call getStdout/
@@ -292,7 +292,7 @@ export class TaskOutput {
292
292
  this._ensureFileBacking();
293
293
  }
294
294
 
295
- // CC parity (ShellCommand file mode): open the spill files BEFORE spawn and
295
+ // File mode: open the spill files BEFORE spawn and
296
296
  // hand their fds to the child as stdio[1]/stdio[2]. The child (and any
297
297
  // grandchildren) write straight to disk with no parent-side pipes, so a
298
298
  // surviving grandchild can never wedge the caller by holding a pipe handle.
@@ -1,7 +1,7 @@
1
1
  import { readFileSync, mkdirSync } from "fs";
2
2
  import { join } from "path";
3
- import { DiscordBackend } from "../backends/discord.mjs";
4
- import { TelegramBackend } from "../backends/telegram.mjs";
3
+ import { DiscordProvider } from "../providers/discord.mjs";
4
+ import { TelegramProvider } from "../providers/telegram.mjs";
5
5
  import {
6
6
  readSection,
7
7
  updateSection,
@@ -22,11 +22,11 @@ const DEFAULT_ACCESS = {
22
22
  channels: {}
23
23
  };
24
24
  const DEFAULT_CONFIG = {
25
- backend: "discord",
25
+ provider: "discord",
26
26
  discord: { token: "" },
27
27
  telegram: { token: "" },
28
28
  access: DEFAULT_ACCESS,
29
- channel: { channelId: "" }
29
+ channel: {}
30
30
  };
31
31
  // Shared defaults layer. Merge semantics: user values win; defaults
32
32
  // only fill missing fields. Helper is exported so the setup UI and
@@ -40,18 +40,18 @@ function applyDefaults(config) {
40
40
  return out;
41
41
  }
42
42
 
43
- function channelIdForBackend(entry = {}, backend = "discord") {
44
- if (backend === "telegram") {
45
- return String(entry?.telegramChatId || (entry?.discordChannelId ? "" : entry?.channelId) || "");
43
+ function channelIdForProvider(entry = {}, provider = "discord") {
44
+ if (provider === "telegram") {
45
+ return String(entry?.telegramChatId || "");
46
46
  }
47
- return String(entry?.discordChannelId || (entry?.telegramChatId ? "" : entry?.channelId) || "");
47
+ return String(entry?.discordChannelId || "");
48
48
  }
49
49
 
50
- // Resolve the single active-backend channel id from the config's `channel`
50
+ // Resolve the single active-provider channel id from the config's `channel`
51
51
  // section. Disk config carries a single `channel` object only.
52
- function resolveChannelId(raw = {}, backend = "discord") {
52
+ function resolveChannelId(raw = {}, provider = "discord") {
53
53
  const channel = raw.channel && typeof raw.channel === "object" ? raw.channel : null;
54
- if (channel) return channelIdForBackend(channel, backend);
54
+ if (channel) return channelIdForProvider(channel, provider);
55
55
  return "";
56
56
  }
57
57
 
@@ -88,20 +88,20 @@ async function loadConfig({ freshSecrets = false } = {}) {
88
88
  if (discordTokenProblem) {
89
89
  process.stderr.write(`mixdog: discord token ignored: ${discordTokenProblem}\n`);
90
90
  }
91
- // Single-backend select: config.backend picks ONE of discord|telegram.
91
+ // Single-provider select: config.provider picks ONE of discord|telegram.
92
92
  // Anything else falls back to the discord default.
93
- const backend = raw.backend === "telegram" ? "telegram" : "discord";
93
+ const provider = raw.provider === "telegram" ? "telegram" : "discord";
94
94
  const telegramToken = getTelegramToken();
95
- const channelId = resolveChannelId(raw, backend);
95
+ const channelId = resolveChannelId(raw, provider);
96
96
  // The runtime reads only the resolved `channelId`; disk carries a single
97
97
  // `channel` object, so spread `raw` directly.
98
98
  return applyDefaults({
99
99
  ...DEFAULT_CONFIG,
100
100
  ...raw,
101
- backend,
101
+ provider,
102
102
  channelId,
103
103
  discord: { ...DEFAULT_CONFIG.discord, ...(({ token: _, ...rest }) => rest)(raw.discord || {}), ...(discordToken && !discordTokenProblem ? { token: discordToken } : {}) },
104
- // Merge the keychain-resolved telegram token (harmless when backend is
104
+ // Merge the keychain-resolved telegram token (harmless when provider is
105
105
  // discord; the secret never lands in the on-disk config either way).
106
106
  telegram: { ...DEFAULT_CONFIG.telegram, ...(({ token: _t, ...rest }) => rest)(raw.telegram || {}), ...(telegramToken ? { token: telegramToken } : {}) },
107
107
  access: {
@@ -117,7 +117,7 @@ async function loadConfig({ freshSecrets = false } = {}) {
117
117
  updateSection("channels", () => DEFAULT_CONFIG);
118
118
  process.stderr.write(
119
119
  `mixdog: default channels config created in ${MIXDOG_CONFIG_PATH}
120
- edit discord.token and channel.channelId to connect.
120
+ edit the active provider channel id to connect.
121
121
  `
122
122
  );
123
123
  return applyDefaults(DEFAULT_CONFIG);
@@ -125,7 +125,7 @@ async function loadConfig({ freshSecrets = false } = {}) {
125
125
  throw err;
126
126
  }
127
127
  }
128
- const HEADLESS_BACKEND = {
128
+ const HEADLESS_PROVIDER = {
129
129
  name: "headless",
130
130
  MAX_MESSAGE_LENGTH: 2000,
131
131
  formatOutgoing(t) {
@@ -156,31 +156,31 @@ const HEADLESS_BACKEND = {
156
156
  on() {
157
157
  }
158
158
  };
159
- function createBackend(config) {
159
+ function createProvider(config) {
160
160
  // Channels-module toggle is MESSAGING-ONLY: automation (scheduler/webhooks)
161
- // keeps the worker alive, so a disabled module runs the headless backend
161
+ // keeps the worker alive, so a disabled module runs the headless provider
162
162
  // instead of blocking the whole runtime.
163
163
  if (config.enabled === false) {
164
164
  process.stderr.write("mixdog: channels messaging disabled; channel runtime running in headless mode\n");
165
- return HEADLESS_BACKEND;
165
+ return HEADLESS_PROVIDER;
166
166
  }
167
- // Single-backend select: exactly one backend is constructed based on
168
- // config.backend (discord|telegram). The two are mutually exclusive.
169
- if (config.backend === "telegram") {
167
+ // Single-provider select: exactly one provider is constructed based on
168
+ // config.provider (discord|telegram). The two are mutually exclusive.
169
+ if (config.provider === "telegram") {
170
170
  const telegramToken = getTelegramToken();
171
171
  if (!telegramToken) {
172
172
  process.stderr.write("mixdog: telegram bot not configured; channel runtime running in headless mode\n");
173
- return HEADLESS_BACKEND;
173
+ return HEADLESS_PROVIDER;
174
174
  }
175
175
  const tgStateDir = config.telegram?.stateDir ?? join(DATA_DIR, "telegram");
176
176
  mkdirSync(tgStateDir, { recursive: true });
177
- return new TelegramBackend({
177
+ return new TelegramProvider({
178
178
  ...config.telegram,
179
179
  configPath: CONFIG_FILE,
180
180
  access: config.access,
181
181
  // Single-source channel setup: the main chat is auto-allowed inside
182
- // TelegramBackend.loadAccess() so a configured channel.channelId
183
- // is enough for both inbound gating and outbound.
182
+ // TelegramProvider.loadAccess() so the configured Telegram target is
183
+ // enough for both inbound gating and outbound.
184
184
  mainChannelId: config.channelId
185
185
  }, tgStateDir);
186
186
  }
@@ -189,19 +189,19 @@ function createBackend(config) {
189
189
  if (discordTokenProblem) {
190
190
  process.stderr.write(`mixdog: discord token ignored: ${discordTokenProblem}\n`);
191
191
  }
192
- if (config.backend !== "discord" || !discordToken || discordTokenProblem) {
192
+ if (config.provider !== "discord" || !discordToken || discordTokenProblem) {
193
193
  process.stderr.write("mixdog: discord bot not configured; channel runtime running in headless mode\n");
194
- return HEADLESS_BACKEND;
194
+ return HEADLESS_PROVIDER;
195
195
  }
196
196
  const stateDir = config.discord.stateDir ?? join(DATA_DIR, "discord");
197
197
  mkdirSync(stateDir, { recursive: true });
198
- return new DiscordBackend({
198
+ return new DiscordProvider({
199
199
  ...config.discord,
200
200
  configPath: CONFIG_FILE,
201
201
  access: config.access,
202
202
  // Single-source channel setup: the main channel is auto-allowed inside
203
- // DiscordBackend.loadAccess() so a configured channel.channelId
204
- // is enough for both inbound and outbound — no separate access.channels entry.
203
+ // DiscordProvider.loadAccess() so the configured Discord target is enough
204
+ // for both inbound and outbound — no separate access.channels entry.
205
205
  mainChannelId: config.channelId
206
206
  }, stateDir);
207
207
  }
@@ -215,7 +215,7 @@ function loadProfileConfig() {
215
215
  }
216
216
  export {
217
217
  DATA_DIR,
218
- createBackend,
218
+ createProvider,
219
219
  getDiscordToken,
220
220
  getTelegramToken,
221
221
  loadConfig,
@@ -7,14 +7,14 @@ import {
7
7
  import { refreshActiveInstance } from "./runtime-paths.mjs";
8
8
  import { isNetworkError, retryOnNetwork } from "./network-retry.mjs";
9
9
  // Inbound message pipeline extracted from channels/index.mjs (behavior-
10
- // preserving): serial inbound queue, backend.onMessage transcript (re)bind +
10
+ // preserving): serial inbound queue, provider.onMessage transcript (re)bind +
11
11
  // steal logic, and handleInbound voice-transcription + parent notify. Bound to
12
12
  // live runtime getters and shared primitives.
13
13
  function isImageAttachment(contentType) {
14
14
  return typeof contentType === "string" && contentType.toLowerCase().startsWith("image/");
15
15
  }
16
16
  export function createInboundHandler({
17
- getBackend,
17
+ getProvider,
18
18
  getConfig,
19
19
  getBridgeRuntimeConnected,
20
20
  getChannelBridgeActive,
@@ -55,7 +55,7 @@ const inboundQueue = (() => {
55
55
  };
56
56
  })();
57
57
 
58
- getBackend().onMessage = (msg) => {
58
+ getProvider().onMessage = (msg) => {
59
59
  const receivedAtMs = Number.isFinite(msg.receivedAtMs) ? msg.receivedAtMs : Date.now();
60
60
  const onMessageAtMs = Date.now();
61
61
  const gateBlocked = (!getBridgeRuntimeConnected() || !getBridgeOwnershipSnapshot().owned)
@@ -63,7 +63,7 @@ getBackend().onMessage = (msg) => {
63
63
  : (!getChannelBridgeActive() ? "bridge-inactive" : "");
64
64
  if (gateBlocked) {
65
65
  if (gateBlocked === "bridge-seat") refreshBridgeOwnershipSafe();
66
- // CC parity: an inbound message is never silently swallowed. The bridge
66
+ // An inbound message is never silently swallowed. The bridge
67
67
  // seat may be mid-handover or the channel bridge may still be activating
68
68
  // (app relaunch / boot); retry with backoff before giving up, and make
69
69
  // the final drop loud so a real loss stays diagnosable.
@@ -72,7 +72,7 @@ getBackend().onMessage = (msg) => {
72
72
  msg.__bridgeGateRetries = attempt;
73
73
  msg.receivedAtMs = receivedAtMs;
74
74
  setTimeout(() => {
75
- try { getBackend().onMessage?.(msg); } catch { /* best-effort */ }
75
+ try { getProvider().onMessage?.(msg); } catch { /* best-effort */ }
76
76
  }, 2000 * attempt)?.unref?.();
77
77
  } else {
78
78
  try { process.stderr.write(`mixdog: inbound message dropped after ${attempt - 1} ${gateBlocked} retries (channel=${msg.chatId}, id=${msg.messageId})\n`); } catch {}
@@ -242,7 +242,7 @@ getBackend().onMessage = (msg) => {
242
242
  });
243
243
  void (async () => {
244
244
  try {
245
- await getBackend().react(msg.chatId, msg.messageId, "\u{1F914}");
245
+ await getProvider().react(msg.chatId, msg.messageId, "\u{1F914}");
246
246
  } catch {
247
247
  }
248
248
  await bindingDone;
@@ -271,7 +271,7 @@ async function handleInbound(msg, route, options = {}) {
271
271
  const files = await retryOnNetwork(
272
272
  // Short per-attempt timeout (vs the 180s default) bounds worst-case
273
273
  // blockage of the serial inboundQueue on a bad voice message.
274
- () => getBackend().downloadAttachment(msg.chatId, msg.messageId, { timeoutMs: 20_000 }),
274
+ () => getProvider().downloadAttachment(msg.chatId, msg.messageId, { timeoutMs: 20_000 }),
275
275
  { label: "voice.download" }
276
276
  );
277
277
  // concurrency handled inside transcribeVoice queue; loop is sequential so last att wins
@@ -312,7 +312,7 @@ async function handleInbound(msg, route, options = {}) {
312
312
  if (imageAtts.length > 0) {
313
313
  try {
314
314
  const files = (await retryOnNetwork(
315
- () => getBackend().downloadAttachment(msg.chatId, msg.messageId, {
315
+ () => getProvider().downloadAttachment(msg.chatId, msg.messageId, {
316
316
  timeoutMs: 20_000,
317
317
  filter: (a) => isImageAttachment(a.contentType),
318
318
  }),