mixdog 0.9.96 → 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 (170) hide show
  1. package/README.md +0 -17
  2. package/package.json +8 -7
  3. package/scripts/build-tui.mjs +6 -6
  4. package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
  5. package/src/defaults/skills/setup/SKILL.md +1 -1
  6. package/src/lib/keychain-cjs.cjs +1 -1
  7. package/src/lib/mixdog-debug.cjs +2 -3
  8. package/src/rules/lead/01-general.md +1 -2
  9. package/src/rules/lead/lead-tool.md +0 -3
  10. package/src/rules/shared/01-tool.md +11 -10
  11. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
  12. package/src/runtime/agent/orchestrator/mcp/client.mjs +81 -33
  13. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
  14. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
  15. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +8 -6
  16. package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
  17. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -4
  18. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +20 -2
  19. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +4 -3
  20. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +40 -25
  21. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
  22. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
  23. package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
  24. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
  25. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
  26. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +59 -42
  27. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +3 -2
  28. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +12 -22
  29. package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
  30. package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
  31. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +2 -0
  32. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +4 -4
  33. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +80 -30
  34. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +41 -22
  35. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +6 -2
  36. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +55 -11
  37. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
  38. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +55 -18
  39. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -0
  40. package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
  41. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +2 -2
  42. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
  43. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +67 -21
  44. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +2 -1
  45. package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
  46. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -1
  47. package/src/runtime/channels/lib/config.mjs +33 -33
  48. package/src/runtime/channels/lib/inbound-handler.mjs +7 -7
  49. package/src/runtime/channels/lib/interaction-handlers.mjs +6 -6
  50. package/src/runtime/channels/lib/memory-client.mjs +31 -157
  51. package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
  52. package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
  53. package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
  54. package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
  55. package/src/runtime/channels/lib/runtime-paths.mjs +4 -30
  56. package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
  57. package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
  58. package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
  59. package/src/runtime/channels/lib/worker-ipc.mjs +7 -7
  60. package/src/runtime/channels/lib/worker-main.mjs +38 -38
  61. package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
  62. package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
  63. package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
  64. package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
  65. package/src/runtime/channels/tool-defs.mjs +1 -1
  66. package/src/runtime/memory/index.mjs +72 -211
  67. package/src/runtime/memory/lib/agent-ipc.mjs +9 -8
  68. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +2 -2
  69. package/src/runtime/memory/lib/http-router.mjs +1 -6
  70. package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
  71. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
  72. package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
  73. package/src/runtime/memory/lib/memory-log.mjs +1 -1
  74. package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
  75. package/src/runtime/memory/lib/memory.mjs +1 -1
  76. package/src/runtime/memory/lib/session-ingest.mjs +1 -1
  77. package/src/runtime/search/index.mjs +2 -2
  78. package/src/runtime/shared/abort-race.mjs +70 -0
  79. package/src/runtime/shared/child-spawn-gate.mjs +101 -63
  80. package/src/runtime/shared/config.mjs +11 -24
  81. package/src/runtime/shared/llm/index.mjs +1 -1
  82. package/src/runtime/shared/owner-fair-gate.mjs +134 -0
  83. package/src/runtime/shared/resource-admission.mjs +6 -5
  84. package/src/runtime/shared/tool-execution-owner.mjs +12 -0
  85. package/src/runtime/shared/tool-result-summary.mjs +1 -1
  86. package/src/runtime/shared/tool-surface.mjs +91 -22
  87. package/src/runtime/shared/tool-workload-gates.mjs +29 -0
  88. package/src/runtime/shared/turn-snapshot.mjs +17 -2
  89. package/src/session-runtime/channel-config-api.mjs +5 -5
  90. package/src/session-runtime/config-lifecycle.mjs +27 -27
  91. package/src/session-runtime/lifecycle-api.mjs +22 -3
  92. package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
  93. package/src/session-runtime/prewarm.mjs +1 -1
  94. package/src/session-runtime/provider-models.mjs +1 -1
  95. package/src/session-runtime/remote-control.mjs +6 -7
  96. package/src/session-runtime/runtime-core.mjs +52 -28
  97. package/src/session-runtime/self-update.mjs +1 -1
  98. package/src/session-runtime/session-lifecycle.mjs +36 -15
  99. package/src/session-runtime/session-title.mjs +1 -1
  100. package/src/session-runtime/session-turn-api.mjs +55 -15
  101. package/src/session-runtime/settings-api.mjs +7 -7
  102. package/src/standalone/agent-dispatch-broker.mjs +1 -1
  103. package/src/standalone/channel-admin.mjs +42 -53
  104. package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +16 -18
  105. package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +45 -46
  106. package/src/standalone/channel-worker.mjs +124 -647
  107. package/src/standalone/{backend-daemon.mjs → daemon.mjs} +187 -132
  108. package/src/standalone/memory-runtime-proxy.mjs +58 -1
  109. package/src/standalone/{engine-daemon-client.mjs → session-client.mjs} +151 -86
  110. package/src/standalone/session-protocol.mjs +8 -4
  111. package/src/standalone/session-runtime-pool.mjs +393 -0
  112. package/src/standalone/session-runtime-record.mjs +20 -0
  113. package/src/standalone/session-runtime-worker.mjs +203 -0
  114. package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +170 -157
  115. package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +139 -61
  116. package/src/standalone/session-wire.mjs +55 -0
  117. package/src/tui/App.jsx +4 -4
  118. package/src/tui/app/app-format.mjs +1 -1
  119. package/src/tui/app/channel-pickers.mjs +41 -41
  120. package/src/tui/app/input-parsers.mjs +2 -2
  121. package/src/tui/app/onboarding-steps.mjs +2 -2
  122. package/src/tui/app/project-picker.mjs +8 -8
  123. package/src/tui/app/prompt-submit.mjs +8 -8
  124. package/src/tui/app/settings-picker.mjs +25 -25
  125. package/src/tui/app/use-prompt-queue-history.mjs +1 -1
  126. package/src/tui/app/use-transcript-scroll.mjs +1 -1
  127. package/src/tui/components/ToolExecution.jsx +1 -1
  128. package/src/tui/components/prompt-input/restore-policy.mjs +1 -1
  129. package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
  130. package/src/tui/dist/index.mjs +154 -127
  131. package/src/tui/hooks/useSession.mjs +16 -0
  132. package/src/tui/index.jsx +11 -11
  133. package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
  134. package/src/tui/{engine → session}/agent-job-feed.mjs +4 -4
  135. package/src/tui/{engine → session}/boot-profile.mjs +2 -2
  136. package/src/tui/{engine → session}/context-state.mjs +3 -3
  137. package/src/tui/{engine → session}/labels.mjs +2 -2
  138. package/src/tui/{engine → session}/live-share.mjs +7 -7
  139. package/src/tui/{engine → session}/notice-text.mjs +2 -2
  140. package/src/tui/{engine → session}/notification-plan.mjs +3 -3
  141. package/src/tui/session/oauth-flows.mjs +140 -0
  142. package/src/tui/{engine → session}/prompt-history.mjs +3 -3
  143. package/src/tui/{engine → session}/queue-helpers.mjs +2 -2
  144. package/src/tui/{engine → session}/render-timing.mjs +2 -2
  145. package/src/tui/{engine → session}/session-api-ext.mjs +30 -17
  146. package/src/tui/{engine → session}/session-api.mjs +7 -60
  147. package/src/tui/{engine → session}/session-flow.mjs +1 -1
  148. package/src/tui/{engine → session}/session-stats.mjs +3 -3
  149. package/src/tui/{engine → session}/tool-approval.mjs +3 -3
  150. package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
  151. package/src/tui/{engine → session}/tool-card-results.mjs +4 -4
  152. package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
  153. package/src/tui/{engine → session}/tool-result-text.mjs +3 -3
  154. package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
  155. package/src/tui/{engine → session}/turn.mjs +29 -107
  156. package/src/tui/{engine-local-session.mjs → session-local.mjs} +59 -75
  157. package/src/tui/session.mjs +22 -0
  158. package/src/ui/statusline-segments.mjs +1 -1
  159. package/src/ui/statusline.mjs +1 -1
  160. package/vendor/ink/build/components/App.js +10 -1
  161. package/src/session-runtime/memory-daemon-probe.mjs +0 -61
  162. package/src/standalone/engine-daemon-protocol.mjs +0 -32
  163. package/src/tui/engine/turn-watchdog.mjs +0 -92
  164. package/src/tui/engine.mjs +0 -22
  165. package/src/tui/hooks/useEngine.mjs +0 -16
  166. /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
  167. /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
  168. /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
  169. /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
  170. /package/src/tui/{engine → session}/tui-steering-persist.mjs +0 -0
@@ -144,8 +144,8 @@ function restorePatchRollbackState(snapshots, readStateScope) {
144
144
  }
145
145
 
146
146
  // Apply one "wave" (a set of unique-target parsed entries) via the native
147
- // (+ JS out-of-base) split. Returns { backend, text } on success or
148
- // { backend, error } so the caller decides whether earlier waves already
147
+ // (+ JS out-of-base) split. Returns { executor, text } on success or
148
+ // { executor, error } so the caller decides whether earlier waves already
149
149
  // committed to disk. Extracted verbatim from the inline applyWave closure so
150
150
  // both the default wave loop and sequence mode share identical apply
151
151
  // semantics.
@@ -156,7 +156,7 @@ async function applyParsedWave({ parsed: wparsed, entries: wentries, headerRewri
156
156
  const parsedInside = (wparsed || []).filter(
157
157
  (entry) => !isResolvedPathOutsideBase(parsedEntryResolvedPath(entry, basePath), basePath),
158
158
  );
159
- const backend = outsideEntries.length > 0
159
+ const executor = outsideEntries.length > 0
160
160
  ? (insideEntries.length > 0 ? 'native+js-patch' : 'js-patch')
161
161
  : 'native-patch';
162
162
  const resultParts = [];
@@ -173,7 +173,7 @@ async function applyParsedWave({ parsed: wparsed, entries: wentries, headerRewri
173
173
  signal: abortSignal,
174
174
  parsed: parsedInside,
175
175
  });
176
- if (isPatchErrorText(nativeResult)) return { backend, error: nativeResult };
176
+ if (isPatchErrorText(nativeResult)) return { executor, error: nativeResult };
177
177
  resultParts.push(nativeResult);
178
178
  }
179
179
  if (outsideEntries.length > 0) {
@@ -187,10 +187,10 @@ async function applyParsedWave({ parsed: wparsed, entries: wentries, headerRewri
187
187
  fuzzy,
188
188
  readStateScope,
189
189
  });
190
- if (isPatchErrorText(jsResult)) return { backend, error: jsResult };
190
+ if (isPatchErrorText(jsResult)) return { executor, error: jsResult };
191
191
  resultParts.push(jsResult);
192
192
  }
193
- return { backend, text: resultParts.join('\n') };
193
+ return { executor, text: resultParts.join('\n') };
194
194
  }
195
195
 
196
196
  // Default ordered section mode. Apply each file section in listed order,
@@ -384,7 +384,7 @@ async function applyPatchSequence(patchStr, requestedFormat, basePath, ctx) {
384
384
  const skipped = [];
385
385
  let failed = null;
386
386
  let failedIndex = -1;
387
- let backend = 'native-patch';
387
+ let executor = 'native-patch';
388
388
  for (let i = 0; i < units.length; i++) {
389
389
  const unit = units[i];
390
390
  if (failed) { skipped.push(unit.displayPath); continue; }
@@ -421,7 +421,7 @@ async function applyPatchSequence(patchStr, requestedFormat, basePath, ctx) {
421
421
  }
422
422
  }
423
423
  const res = await applyParsedWave(wave, basePath, waveOpts);
424
- backend = res.backend;
424
+ executor = res.executor;
425
425
  if (res.error) {
426
426
  failed = { displayPath: unit.displayPath, error: res.error };
427
427
  failedIndex = i;
@@ -459,7 +459,7 @@ async function applyPatchSequence(patchStr, requestedFormat, basePath, ctx) {
459
459
  }
460
460
  const head = `apply_patch: ${verb} ${units.length} section(s)`;
461
461
  const body = (appliedTexts ? `${head}\n${appliedTexts}` : head) + dryNote + rejectedTail;
462
- return wrapPatchMutationOutput(body, mutationPlan, { backend });
462
+ return wrapPatchMutationOutput(body, mutationPlan, { executor });
463
463
  }
464
464
  const failMsg = failed.error.replace(/^Error:\s*/, '');
465
465
  const rollbackErrors = (!dryRun && rollbackOnFailure)
@@ -499,7 +499,7 @@ async function applyPatchSequence(patchStr, requestedFormat, basePath, ctx) {
499
499
  if (skipped.length > 0) {
500
500
  lines.push(`--- skipped (not attempted): ${skipped.join(', ')} ---`);
501
501
  }
502
- return wrapPatchMutationOutput(lines.join('\n') + dryNote + rejectedTail, mutationPlan, { backend });
502
+ return wrapPatchMutationOutput(lines.join('\n') + dryNote + rejectedTail, mutationPlan, { executor });
503
503
  }));
504
504
  }
505
505
 
@@ -723,9 +723,41 @@ function salvageShatteredV4APatchArgs(args) {
723
723
  return cleaned;
724
724
  }
725
725
 
726
+ function extractInlinePatchRoot(patchText) {
727
+ const text = String(patchText || '');
728
+ const match = /^(\*\*\* Begin Patch\r?\n)\*\*\* Root:[ \t]*(.*?)\r?\n/.exec(text);
729
+ if (!match) return { patch: text, root: null };
730
+ const root = String(match[2] || '').trim();
731
+ if (!root) throw new Error('apply_patch: "*** Root:" requires a containing directory');
732
+ return {
733
+ patch: `${match[1]}${text.slice(match[0].length)}`,
734
+ root,
735
+ };
736
+ }
737
+
738
+ function isFilesystemRootSpecifier(value, cwd) {
739
+ const raw = String(value || '').trim();
740
+ if (!raw) return false;
741
+ // Keep Windows semantics testable on every host and reject the ambiguous
742
+ // drive-relative spelling (`C:`) alongside an actual drive root.
743
+ if (/^[A-Za-z]:(?:[\\/]?)$/.test(raw)) return true;
744
+ const resolved = resolveBasePath(cwd, raw);
745
+ return pathDirname(resolved) === resolved;
746
+ }
747
+
726
748
  async function apply_patch(args, cwd, options = {}) {
727
749
  args = salvageShatteredV4APatchArgs(args);
728
- const patchStr = salvageV4AOpening((typeof args?.patch === 'string' ? args.patch : '').replace(/^\uFEFF/, ''));
750
+ let patchStr = salvageV4AOpening((typeof args?.patch === 'string' ? args.patch : '').replace(/^\uFEFF/, ''));
751
+ const inlineRoot = extractInlinePatchRoot(patchStr);
752
+ patchStr = inlineRoot.patch;
753
+ if (inlineRoot.root) {
754
+ const argumentRoot = typeof args?.root === 'string' ? args.root.trim() : '';
755
+ if (argumentRoot
756
+ && pathResolve(resolveBasePath(cwd, argumentRoot)) !== pathResolve(resolveBasePath(cwd, inlineRoot.root))) {
757
+ throw new Error('apply_patch: root argument conflicts with the "*** Root:" directive');
758
+ }
759
+ args = { ...(args || {}), root: inlineRoot.root };
760
+ }
729
761
  if (!patchStr.trim()) {
730
762
  throw new Error('apply_patch: "patch" is required (unified diff or V4A patch string)');
731
763
  }
@@ -761,7 +793,21 @@ async function apply_patch(args, cwd, options = {}) {
761
793
  // a mis-scoped working directory can never silently rewrite another tree.
762
794
  // Editing outside stays possible, but only as a deliberate act: name the
763
795
  // root that contains those targets in the same call.
764
- const writeRoot = resolveBasePath(cwd, args?.root ?? args?.base_path ?? null);
796
+ const explicitRoot = typeof args?.root === 'string' && args.root.trim() ? args.root.trim() : null;
797
+ if (explicitRoot && isFilesystemRootSpecifier(explicitRoot, cwd)) {
798
+ throw new Error(`apply_patch: refusing filesystem root as write root: ${normalizeOutputPath(explicitRoot)}`);
799
+ }
800
+ const writeRoot = resolveBasePath(cwd, explicitRoot ?? args?.base_path ?? null);
801
+ if (explicitRoot) {
802
+ try {
803
+ await assertPathReachable(writeRoot);
804
+ if (!statSync(writeRoot).isDirectory()) {
805
+ throw new Error(`not a directory — ${normalizeOutputPath(writeRoot)}`);
806
+ }
807
+ } catch (err) {
808
+ throw new Error(`apply_patch: invalid write root ${normalizeOutputPath(writeRoot)}: ${err?.message || String(err)}`);
809
+ }
810
+ }
765
811
  const outsideTargets = [];
766
812
  for (const rel of patchTargetPaths(patchStr, basePath)) {
767
813
  const abs = isAbsolute(rel) ? pathResolve(rel) : pathResolve(basePath, rel);
@@ -773,7 +819,7 @@ async function apply_patch(args, cwd, options = {}) {
773
819
  const more = shown.length > 3 ? ` (+${shown.length - 3} more)` : '';
774
820
  throw new Error(
775
821
  `apply_patch: ${shown.length} target(s) fall outside the write root ${normalizeOutputPath(writeRoot)}: ${head}${more}. `
776
- + 'Check the paths first; if they are intended, re-issue the same patch with root set to the directory that contains them.',
822
+ + 'Check the paths first; if intended, set root (JSON) or add "*** Root: <containing directory>" after "*** Begin Patch" (freeform).',
777
823
  );
778
824
  }
779
825
  const rejectPartial = args?.reject_partial !== false;
@@ -917,17 +963,17 @@ async function apply_patch(args, cwd, options = {}) {
917
963
  if (v4aRenameOnly) {
918
964
  const lines = formatV4ARenameSuccessLines(v4aRenameResults);
919
965
  if (lines.length === 0) return 'Error: patch contained no applicable file sections';
920
- return wrapPatchMutationOutput(`${lines.join('\n')}\n`, mutationPlan, { backend: 'v4a-rename' });
966
+ return wrapPatchMutationOutput(`${lines.join('\n')}\n`, mutationPlan, { executor: 'v4a-rename' });
921
967
  }
922
968
  // Apply one wave (a set of unique targets) via applyParsedWave (native +
923
- // JS split). Returns { backend, text } on success or { backend, error }.
969
+ // JS split). Returns { executor, text } on success or { executor, error }.
924
970
  const applyWave = (wave) => applyParsedWave(wave, basePath, { fuzz, rejectPartial, dryRun, fuzzy, readStateScope, abortSignal });
925
971
 
926
972
  // Duplicate-target blocks were split into contiguous sequential groups
927
973
  // (listed order preserved); apply them in order, each against the prior
928
974
  // group's on-disk result.
929
975
  const waveTexts = [];
930
- let backend = 'native-patch';
976
+ let executor = 'native-patch';
931
977
  // dry_run never writes, so a later group would validate against unchanged
932
978
  // disk and false-fail on any block that depends on an earlier edit. Only
933
979
  // the first group is validated under dry_run; the rest are reported as
@@ -935,9 +981,9 @@ async function apply_patch(args, cwd, options = {}) {
935
981
  const groupCount = (dryRun && waveDispatch.length > 1) ? 1 : waveDispatch.length;
936
982
  for (let w = 0; w < groupCount; w++) {
937
983
  const res = await applyWave(waveDispatch[w]);
938
- backend = res.backend;
984
+ executor = res.executor;
939
985
  if (res.error) {
940
- if (w === 0) return wrapPatchMutationOutput(res.error, mutationPlan, { backend });
986
+ if (w === 0) return wrapPatchMutationOutput(res.error, mutationPlan, { executor });
941
987
  // A later group failed. rejectPartial makes each group all-or-nothing,
942
988
  // so every block in groups 1..w is fully committed to disk and left in
943
989
  // place. List them all so the caller knows the true on-disk state.
@@ -948,7 +994,7 @@ async function apply_patch(args, cwd, options = {}) {
948
994
  '--- failing block ---',
949
995
  failMsg,
950
996
  ].join('\n');
951
- return wrapPatchMutationOutput(note, mutationPlan, { backend });
997
+ return wrapPatchMutationOutput(note, mutationPlan, { executor });
952
998
  }
953
999
  waveTexts.push(res.text);
954
1000
  }
@@ -970,9 +1016,9 @@ async function apply_patch(args, cwd, options = {}) {
970
1016
  `hunk-level rejected (rejectPartial=false, V4A): ${rejectedV4AHunks.length}`,
971
1017
  ...rejectedV4AHunks.map((r) => ` REJECT ${r.file || '(unknown)'} — ${String(r.reason || '').split(';')[0].trim()}`),
972
1018
  ];
973
- return wrapPatchMutationOutput(`${combined}\n${tail.join('\n')}`, mutationPlan, { backend });
1019
+ return wrapPatchMutationOutput(`${combined}\n${tail.join('\n')}`, mutationPlan, { executor });
974
1020
  }
975
- return wrapPatchMutationOutput(combined, mutationPlan, { backend });
1021
+ return wrapPatchMutationOutput(combined, mutationPlan, { executor });
976
1022
  };
977
1023
 
978
1024
  return withBuiltinPathLocks(_lockPaths, () =>
@@ -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
@@ -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
@@ -258,6 +258,7 @@ export function execShellCommand({
258
258
  ownerSessionId,
259
259
  backgroundOnTimeout,
260
260
  promotedTimeoutMs = 0,
261
+ backgroundDeadlineMs = 0,
261
262
  admission = resourceAdmission,
262
263
  }) {
263
264
  return new Promise(async (resolve) => {
@@ -791,7 +792,12 @@ export function execShellCommand({
791
792
  const stdoutPath = taskOutput.spilled ? taskOutput.stdoutPath : null;
792
793
  const stderrPath = taskOutput.spilled ? taskOutput.stderrPath : null;
793
794
  let job = null;
794
- 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);
795
801
  try {
796
802
  job = adoptForegroundShellJob({
797
803
  command,
@@ -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,7 +22,7 @@ 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,
@@ -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 the active backend channel id 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)
@@ -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
  }),
@@ -1,11 +1,11 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  // Discord permission-prompt + interaction/modal routing extracted from
4
- // channels/index.mjs (behavior-preserving). Installs backend.onInteraction /
4
+ // channels/index.mjs (behavior-preserving). Installs provider.onInteraction /
5
5
  // onModalRequest handlers and the terminal tool-exec signal watcher; returns the
6
6
  // pending-permission map + marker refresher for the worker IPC handler.
7
7
  export function createInteractionHandlers({
8
- getBackend,
8
+ getProvider,
9
9
  getConfig,
10
10
  getBridgeRuntimeConnected,
11
11
  instanceId,
@@ -30,18 +30,18 @@ export function createInteractionHandlers({
30
30
  RUNTIME_ROOT,
31
31
  }) {
32
32
  function editDiscordMessage(channelId, messageId, label) {
33
- // Behavior-preserving: route through the getBackend() abstraction (which uses
33
+ // Behavior-preserving: route through the getProvider() abstraction (which uses
34
34
  // discord.js under the hood) instead of issuing a raw REST PATCH. Errors
35
35
  // are swallowed to stderr to match the prior fire-and-forget shape — the
36
36
  // call site never awaited the HTTPS request either.
37
37
  if (!getDiscordToken()) return;
38
38
  const text = `\u{1F510} **Permission Request** \u2014 ${label}`;
39
- void getBackend().editMessage(channelId, messageId, text, { components: [] }).catch((err) => {
39
+ void getProvider().editMessage(channelId, messageId, text, { components: [] }).catch((err) => {
40
40
  process.stderr.write(`mixdog: editDiscordMessage failed: ${err}
41
41
  `);
42
42
  });
43
43
  }
44
- getBackend().onModalRequest = async (rawInteraction) => {
44
+ getProvider().onModalRequest = async (rawInteraction) => {
45
45
  if (!getBridgeRuntimeConnected() || !getBridgeOwnershipSnapshot().owned) {
46
46
  refreshBridgeOwnershipSafe();
47
47
  return;
@@ -155,7 +155,7 @@ try {
155
155
  try { process.stderr.write(`mixdog channels: tool-exec signal watcher setup failed: ${err && err.message || err}\n`); } catch {}
156
156
  }
157
157
 
158
- getBackend().onInteraction = (interaction) => {
158
+ getProvider().onInteraction = (interaction) => {
159
159
  // Channel-route permission reply. Custom_id format: perm-ch-{request_id}-{allow|session|deny}.
160
160
  // request_id is the 5-letter short ID CC generates via shortRequestId().
161
161
  // Emit notifications/claude/channel/permission back to the MCP host; the race