mixdog 0.9.135 → 0.9.136

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 (121) hide show
  1. package/package.json +3 -1
  2. package/scripts/tool-stress.mjs +8 -1
  3. package/src/agents/maintainer/agent.json +1 -1
  4. package/src/defaults/skills/setup/SKILL.md +8 -43
  5. package/src/help.mjs +0 -2
  6. package/src/rules/lead/01-general.md +0 -4
  7. package/src/rules/shared/01-tool.md +10 -4
  8. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +30 -4
  9. package/src/runtime/agent/orchestrator/agent-trace-timing.test.mjs +57 -0
  10. package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +3 -3
  11. package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.test.mjs +40 -0
  12. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +7 -0
  13. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +0 -5
  14. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +17 -4
  15. package/src/runtime/agent/orchestrator/tools/builtin/edit-sequential-occupation.test.mjs +40 -0
  16. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +150 -41
  17. package/src/runtime/agent/orchestrator/tools/builtin/local-search-telemetry.mjs +4 -0
  18. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +34 -0
  19. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +18 -1
  20. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +10 -9
  21. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +124 -18
  22. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-top-k.test.mjs +39 -0
  23. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +7 -1
  24. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +4 -1
  25. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +82 -5
  26. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +2 -2
  27. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
  28. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +78 -12
  29. package/src/runtime/agent/orchestrator/tools/lib/shell-warm-standby.mjs +34 -3
  30. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +12 -5
  31. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +87 -0
  32. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
  33. package/src/runtime/channels/lib/config.mjs +14 -101
  34. package/src/runtime/channels/lib/index-drop-trace.mjs +2 -5
  35. package/src/runtime/channels/lib/owned-runtime.mjs +4 -80
  36. package/src/runtime/channels/lib/scheduler.mjs +75 -18
  37. package/src/runtime/channels/lib/status-snapshot.mjs +5 -68
  38. package/src/runtime/channels/lib/tool-dispatch.mjs +12 -55
  39. package/src/runtime/channels/lib/worker-ipc.mjs +1 -16
  40. package/src/runtime/channels/lib/worker-main.mjs +22 -307
  41. package/src/runtime/media/renditions.mjs +68 -5
  42. package/src/runtime/media/renditions.test.mjs +45 -0
  43. package/src/runtime/media/store.mjs +1 -0
  44. package/src/runtime/memory/index.mjs +4 -3
  45. package/src/runtime/memory/lib/cycle-scheduler.mjs +17 -6
  46. package/src/runtime/memory/lib/memory-cycle-packets.test.mjs +7 -1
  47. package/src/runtime/memory/lib/memory-cycle-requests.mjs +13 -0
  48. package/src/runtime/shared/config.mjs +8 -49
  49. package/src/runtime/shared/schedule-time.mjs +25 -0
  50. package/src/runtime/shared/schedule-time.test.mjs +29 -0
  51. package/src/runtime/shared/schedules-db.mjs +73 -1
  52. package/src/runtime/shared/tool-card-model.mjs +1 -1
  53. package/src/runtime/shared/transcript-writer.mjs +10 -17
  54. package/src/runtime/shared/update-checker.mjs +7 -2
  55. package/src/runtime/shared/update-checker.test.mjs +33 -0
  56. package/src/session-runtime/channel-config-api.mjs +3 -20
  57. package/src/session-runtime/config-lifecycle.mjs +1 -42
  58. package/src/session-runtime/lifecycle-api.mjs +1 -19
  59. package/src/session-runtime/model-route-api.mjs +0 -2
  60. package/src/session-runtime/model-settings-persist.test.mjs +0 -1
  61. package/src/session-runtime/prewarm.mjs +2 -7
  62. package/src/session-runtime/prewarm.test.mjs +0 -1
  63. package/src/session-runtime/runtime-core.mjs +6 -185
  64. package/src/session-runtime/session-lifecycle.mjs +4 -17
  65. package/src/session-runtime/session-transcript.mjs +54 -0
  66. package/src/session-runtime/session-turn-api.mjs +4 -44
  67. package/src/session-runtime/settings-api.mjs +0 -45
  68. package/src/session-runtime/tool-catalog-schema.mjs +1 -1
  69. package/src/standalone/channel-admin.mjs +8 -150
  70. package/src/standalone/daemon.mjs +5 -48
  71. package/src/standalone/seeds.mjs +18 -1
  72. package/src/standalone/session-protocol.mjs +0 -8
  73. package/src/standalone/session-service.mjs +1 -3
  74. package/src/tui/App.jsx +0 -5
  75. package/src/tui/app/app-view.jsx +1 -2
  76. package/src/tui/app/create-app-pickers.mjs +0 -20
  77. package/src/tui/app/doctor.mjs +2 -6
  78. package/src/tui/app/prompt-submit.mjs +2 -49
  79. package/src/tui/app/settings-picker.mjs +22 -96
  80. package/src/tui/app/slash-commands.mjs +0 -2
  81. package/src/tui/app/slash-dispatch.mjs +0 -17
  82. package/src/tui/app/use-prompt-handlers.mjs +4 -1
  83. package/src/tui/components/StatusLine.jsx +4 -4
  84. package/src/tui/dist/index.mjs +1003 -1461
  85. package/src/tui/session/context-state.mjs +0 -1
  86. package/src/tui/session/queue-helpers.mjs +1 -1
  87. package/src/tui/session/queue-helpers.test.mjs +10 -1
  88. package/src/tui/session/session-api-ext.mjs +0 -89
  89. package/src/tui/session/session-api.mjs +0 -2
  90. package/src/tui/session/tool-card-results.mjs +1 -1
  91. package/src/tui/session/tool-result-status.mjs +3 -3
  92. package/src/tui/session/tool-result-status.test.mjs +3 -3
  93. package/src/tui/session-local.mjs +0 -26
  94. package/src/workflows/default/WORKFLOW.md +13 -15
  95. package/src/workflows/solo/WORKFLOW.md +6 -7
  96. package/src/runtime/channels/lib/config-lock.mjs +0 -13
  97. package/src/runtime/channels/lib/drop-trace.mjs +0 -71
  98. package/src/runtime/channels/lib/format.mjs +0 -190
  99. package/src/runtime/channels/lib/inbound-handler.mjs +0 -387
  100. package/src/runtime/channels/lib/inbound-routing.mjs +0 -81
  101. package/src/runtime/channels/lib/interaction-handlers.mjs +0 -70
  102. package/src/runtime/channels/lib/interaction-workflows.mjs +0 -76
  103. package/src/runtime/channels/lib/memory-client.mjs +0 -289
  104. package/src/runtime/channels/lib/output-forwarder.mjs +0 -934
  105. package/src/runtime/channels/lib/session-control.mjs +0 -36
  106. package/src/runtime/channels/lib/session-discovery.mjs +0 -217
  107. package/src/runtime/channels/lib/session-discovery.test.mjs +0 -32
  108. package/src/runtime/channels/lib/telegram-format.mjs +0 -280
  109. package/src/runtime/channels/lib/tool-format.mjs +0 -125
  110. package/src/runtime/channels/lib/transcript-binding.mjs +0 -336
  111. package/src/runtime/channels/lib/transcript-discovery.mjs +0 -208
  112. package/src/runtime/channels/providers/discord-access.mjs +0 -30
  113. package/src/runtime/channels/providers/discord-attachments.mjs +0 -69
  114. package/src/runtime/channels/providers/discord-gateway.mjs +0 -230
  115. package/src/runtime/channels/providers/discord.mjs +0 -730
  116. package/src/runtime/channels/providers/telegram.mjs +0 -598
  117. package/src/runtime/channels/providers/telegram.test.mjs +0 -75
  118. package/src/session-runtime/remote-control.mjs +0 -126
  119. package/src/session-runtime/remote-transcript.mjs +0 -134
  120. package/src/session-runtime/remote-transition-queue.mjs +0 -14
  121. package/src/tui/app/channel-pickers.mjs +0 -384
@@ -1,29 +1,19 @@
1
- import { OutputForwarder } from "./output-forwarder.mjs";
2
-
3
- // Worker/HTTP tool-call dispatch + bridge auto-connect retry wrapper.
4
- // Extracted verbatim from channels/index.mjs (behavior-preserving). The
5
- // `handleToolCall` switch is entangled with ~8 runtime-lifecycle functions
6
- // (claim/refresh ownership, stop owned runtime, transcript rebind, config
7
- // reload) plus mutable owner state (channelBridgeActive, bridgeRuntimeConnected)
8
- // and the forwarder. Those are threaded in as a `lifecycle` bag of lazy
9
- // getters/functions so the module reads the live file-level references at
10
- // call time — matching the original in-file closure semantics.
1
+ // Worker/HTTP runtime-lifecycle dispatch (activate_channel_bridge /
2
+ // reload_config). Channel messaging — forwarder, transcript binding, typing —
3
+ // is deleted with Discord/Telegram; only the automation lifecycle remains.
4
+ // Lifecycle functions are threaded in as a bag of lazy getters so the module
5
+ // reads the live file-level references at call time.
11
6
  function createToolDispatch({
12
- getForwarder,
13
7
  isChannelsDegraded,
14
8
  lifecycle,
15
9
  }) {
16
10
  const {
17
- getBridgeRuntimeConnected,
18
11
  getChannelBridgeActive,
19
12
  getOwned,
20
13
  setChannelBridgeActive,
21
14
  writeBridgeState,
22
- stopServerTyping,
23
15
  notifyRemoteAcquired,
24
16
  refreshBridgeOwnership,
25
- bindPersistedTranscriptIfAny,
26
- rebindCurrentTranscript,
27
17
  startChannelBridge,
28
18
  stopOwnedRuntime,
29
19
  reloadRuntimeConfig,
@@ -46,8 +36,6 @@ function createToolDispatch({
46
36
  // (getOwned() is always true), so activate never needs to claim a
47
37
  // seat or pre-notify — the not-connected -> connected transition
48
38
  // inside startOwnedRuntime fires notifyRemoteAcquired exactly once.
49
- // refreshBridgeOwnership still re-pins the forwarder binding onto
50
- // THIS session.
51
39
  if (getOwned?.() !== true) {
52
40
  notifyRemoteAcquired?.();
53
41
  }
@@ -57,17 +45,12 @@ function createToolDispatch({
57
45
  } else {
58
46
  await refreshBridgeOwnership({ restoreBinding: true });
59
47
  }
60
- // An already-connected owner returns early from
61
- // startOwnedRuntime(), so rebind explicitly to follow the
62
- // current (parent-chain) session transcript.
63
- if (getBridgeRuntimeConnected()) await bindPersistedTranscriptIfAny();
64
48
  } catch (e) {
65
49
  process.stderr.write(`mixdog: bridge activate refresh failed (non-fatal): ${e?.message || e}\n`);
66
50
  }
67
51
  }
68
52
  if (!active && wasActive) {
69
- stopServerTyping();
70
- // Tear down the owner-side runtime so Discord/scheduler/webhook/
53
+ // Tear down the owner-side runtime so scheduler/webhook/
71
54
  // event-pipeline don't keep running on a deactivated bridge.
72
55
  try { await stopOwnedRuntime("bridge deactivated"); } catch (e) {
73
56
  process.stderr.write(`mixdog: stopOwnedRuntime on deactivate failed: ${e?.message || e}\n`);
@@ -99,13 +82,9 @@ function createToolDispatch({
99
82
  break;
100
83
  }
101
84
  case "rebind_current_transcript": {
102
- // Lead-pushed repoint to the transcript it just created/rebound.
103
- // Best-effort + idempotent: absent channelId or path => no-op; a
104
- // bind failure is swallowed by the outer try so lead paths never
105
- // throw. Same binding path as the inbound steal.
106
- const transcriptPath = typeof args.transcriptPath === "string" ? args.transcriptPath.trim() : "";
107
- if (transcriptPath) await rebindCurrentTranscript(transcriptPath);
108
- result = { content: [{ type: "text", text: `transcript rebind ${transcriptPath ? "pushed" : "skipped (no path)"}` }] };
85
+ // Transcript forwarding is retired with channel messaging: accept
86
+ // and no-op so legacy lead-side pushes never surface an error.
87
+ result = { content: [{ type: "text", text: "transcript rebind retired" }] };
109
88
  break;
110
89
  }
111
90
  // memory — handled by memory-service.mjs MCP
@@ -125,32 +104,10 @@ function createToolDispatch({
125
104
  return result;
126
105
  }
127
106
 
128
- // Bridge auto-connect retry + forwarder-aware tool dispatch wrapper. Used by
129
- // both the HTTP MCP path (createHttpMcpServer's CallTool handler can call this)
130
- // and the worker IPC handler at the bottom of index.mjs.
131
- // Last timestamp a forwardNewText() call was dispatched (debounce for item 4).
132
- let _lastForwardMs = 0;
133
-
107
+ // IPC/HTTP wrapper: with channel messaging retired there is no transcript
108
+ // pre-flush or tool-log relay — plain dispatch.
134
109
  async function handleToolCallWithBridgeRetry(toolName, args, signal) {
135
- const forwarder = getForwarder();
136
- // Debounce: only forward when ≥250 ms have elapsed since the last forward,
137
- // to avoid one HTTP roundtrip per tool call on rapid-fire sequences.
138
- const now = Date.now();
139
- // The transcript rebind op must repoint the forwarder BEFORE any flush;
140
- // running the pre-flush first would send stale-transcript text ahead of the
141
- // rebind. Skip the pre-flush for it so rebinding always precedes forwarding.
142
- if (toolName !== 'rebind_current_transcript' && now - _lastForwardMs >= 250) {
143
- _lastForwardMs = now;
144
- await forwarder.forwardNewText();
145
- }
146
- const result = await handleToolCall(toolName, args, signal);
147
- const toolLine = OutputForwarder.buildToolLine(toolName, args);
148
- if (toolLine) {
149
- // Distinct from the dispatch-log ok line (server-main.mjs): this forwards
150
- // a human-readable tool summary to Discord for the user, not operator stdout.
151
- void forwarder.forwardToolLog(toolLine, toolName, args);
152
- }
153
- return result;
110
+ return handleToolCall(toolName, args, signal);
154
111
  }
155
112
 
156
113
  return { handleToolCall, handleToolCallWithBridgeRetry };
@@ -67,21 +67,6 @@ export function runWorkerIpc({
67
67
  _channelsShutdownHandler('IPC:shutdown')
68
68
  return
69
69
  }
70
- // Silent-to-agent lifecycle forward — parent (server.mjs) asks the
71
- // channels worker to post status pings to the active bridge Discord
72
- // channel without the Lead-notify hop. Best-effort: unknown channel or
73
- // getProvider() failure is swallowed; lifecycle pings are non-critical.
74
- if (msg && msg.type === 'forward_to_discord') {
75
- try {
76
- const target = msg.channelId
77
- || (statusState?.read?.().channelId)
78
- || null;
79
- if (target && getProvider()?.sendMessage && typeof msg.content === 'string' && msg.content) {
80
- await getProvider().sendMessage(target, msg.content).catch(() => {});
81
- }
82
- } catch { /* best-effort */ }
83
- return;
84
- }
85
70
  if (handleMemoryCallResponse(msg)) return;
86
71
  if (msg.type === 'cancel' && msg.callId) {
87
72
  const entry = _inFlightChannelCalls.get(msg.callId)
@@ -137,7 +122,7 @@ export function runWorkerIpc({
137
122
  // A stop landed while we were failing — let clean shutdown proceed, never exit over it.
138
123
  if (_channelsStopInFlight) return
139
124
  // Terminal failure: do NOT mask as a (degraded) ready. Exit non-zero so the
140
- // parent's exit-before-ready path respawns or rejects startRemote instead of
125
+ // parent's exit-before-ready path respawns or rejects the start instead of
141
126
  // silently losing remote output forwarding.
142
127
  bootProfile("worker:failed", { ms: (performance.now() - startedAt).toFixed(1), error: lastErr?.message || String(lastErr) })
143
128
  process.stderr.write(`[channels-worker] start() giving up after ${MAX_START_ATTEMPTS} attempts: ${lastErr && (lastErr.message || lastErr)}\n`)
@@ -12,29 +12,17 @@ import { captureOriginalUserCwd, readLastSessionCwd } from "../../shared/user-cw
12
12
  import { ensurePrivateRuntimeRoot, resolveRuntimeRoot } from "../../shared/runtime-root.mjs";
13
13
  import { initProviders } from "../../agent/orchestrator/providers/registry.mjs";
14
14
  import { Scheduler } from "./scheduler.mjs";
15
- import { startSnapshotWriter, stopSnapshotWriter, recordFetchedMessages } from "./status-snapshot.mjs";
15
+ import { startSnapshotWriter, stopSnapshotWriter } from "./status-snapshot.mjs";
16
16
  import { hasPending as dispatchHasPending } from "../../agent/orchestrator/dispatch-persist.mjs";
17
17
  import { setListener as setActivityBusListener } from "../../agent/orchestrator/activity-bus.mjs";
18
18
  import { stripSoftWarns } from "../../agent/orchestrator/tool-loop-guard.mjs";
19
19
  import { WebhookServer } from "./webhook.mjs";
20
20
  import { EventPipeline } from "./event-pipeline.mjs";
21
21
  import { startCliWorker } from "./cli-worker-host.mjs";
22
- import {
23
- OutputForwarder,
24
- discoverSessionBoundTranscript,
25
- findLatestTranscriptByMtime,
26
- sameResolvedPath
27
- } from "./output-forwarder.mjs";
28
- import { controlClaudeSession } from "./session-control.mjs";
29
22
  import { JsonStateFile, ensureDir, removeFileIfExists, writeTextFile } from "./state-file.mjs";
30
- import {
31
- buildModalRequestSpec,
32
- PendingInteractionStore
33
- } from "./interaction-workflows.mjs";
34
23
  import {
35
24
  ensureRuntimeDirs,
36
25
  makeInstanceId,
37
- getTurnEndPath,
38
26
  getStatusPath,
39
27
  getChannelOwnerPath,
40
28
  getActiveOwnerPid,
@@ -61,24 +49,13 @@ import {
61
49
  BENIGN_CRASH_STREAK_WINDOW_MS,
62
50
  } from "./crash-log.mjs";
63
51
  import { dropTrace, preview, _dtIdxFlush } from "./index-drop-trace.mjs";
64
- import { createVoiceTranscription } from "./voice-transcription.mjs";
65
52
  import { createParentBridge } from "./parent-bridge.mjs";
66
- import { createInboundRouting } from "./inbound-routing.mjs";
67
53
  import { createToolDispatch } from "./tool-dispatch.mjs";
68
54
  import { createOwnerHeartbeat } from "./owner-heartbeat.mjs";
69
- import { createTranscriptBinding } from "./transcript-binding.mjs";
70
55
  import { isNetworkError, retryOnNetwork } from "./network-retry.mjs";
71
56
  import { runWorkerIpc } from "./worker-ipc.mjs";
72
- import { createInteractionHandlers } from "./interaction-handlers.mjs";
73
- import { createInboundHandler } from "./inbound-handler.mjs";
74
57
  import { createOwnedRuntime } from "./owned-runtime.mjs";
75
58
  import { runWorkerBootstrap } from "./worker-bootstrap.mjs";
76
- const memoryClientModulePath = new URL("./memory-client.mjs", import.meta.url).href;
77
- const {
78
- appendEntry: memoryAppendEntry,
79
- ingestTranscript: memoryIngestTranscript,
80
- drainBuffer: memoryDrainBuffer,
81
- } = await import(memoryClientModulePath);
82
59
  // Zombie-Lead repro (2026-07-02): logCrash-then-survive left a worker alive
83
60
  // after an unhandled rejection whose async state was already corrupted
84
61
  // (observed: EPERM on active-instance.json rename retry), so it spun
@@ -170,113 +147,12 @@ function writeBridgeState(active) {
170
147
  function isChannelBridgeActive() {
171
148
  return channelBridgeActive;
172
149
  }
173
- let typingChannelId = null;
174
- const pendingSetup = new PendingInteractionStore();
175
- function startServerTyping(channelId) {
176
- if (typingChannelId && typingChannelId !== channelId) {
177
- provider.stopTyping(typingChannelId);
178
- }
179
- typingChannelId = channelId;
180
- provider.startTyping(channelId);
181
- }
182
- function stopServerTyping() {
183
- if (typingChannelId) {
184
- provider.stopTyping(typingChannelId);
185
- typingChannelId = null;
186
- }
187
- }
188
- const TURN_END_FILE = getTurnEndPath(INSTANCE_ID);
189
- const TURN_END_BASENAME = path.basename(TURN_END_FILE);
190
- const TURN_END_DIR = path.dirname(TURN_END_FILE);
191
- let turnEndWatcher = null;
192
150
  // Config hot-reload watcher (installed by start(); torn down by stop()).
193
151
  let _configWatcher = null;
194
152
  let _reloadDebounce = null;
195
- if (!_isWorkerMode) {
196
- removeFileIfExists(TURN_END_FILE);
197
- turnEndWatcher = fs.watch(TURN_END_DIR, async (_event, filename) => {
198
- if (filename !== TURN_END_BASENAME) return;
199
- try {
200
- const stat = fs.statSync(TURN_END_FILE);
201
- if (stat.size > 0) {
202
- stopServerTyping();
203
- await forwarder.forwardFinalText();
204
- removeFileIfExists(TURN_END_FILE);
205
- }
206
- } catch {
207
- }
208
- });
209
- }
210
153
  const STATUS_FILE = getStatusPath(INSTANCE_ID);
211
154
  const statusState = new JsonStateFile(STATUS_FILE, {});
212
155
  statusState.ensure();
213
- const forwarder = new OutputForwarder({
214
- send: async (ch, text, opts) => {
215
- if (!channelBridgeActive) {
216
- throw new Error("send() called while channel bridge is inactive");
217
- }
218
- await provider.sendMessage(ch, text, opts);
219
- },
220
- formatOutgoing: (text) => provider.formatOutgoing ? provider.formatOutgoing(text) : text,
221
- recordAssistantTurn: async () => {
222
- },
223
- react: (ch, mid, emoji) => {
224
- if (!channelBridgeActive) return Promise.resolve();
225
- return provider.react(ch, mid, emoji);
226
- },
227
- removeReaction: (ch, mid, emoji) => {
228
- if (!channelBridgeActive) return Promise.resolve();
229
- return provider.removeReaction(ch, mid, emoji);
230
- },
231
- // Watchdog backstop: force the provider to tear down + rebuild a wedged
232
- // client so an over-budget send fails fast and the queue releases.
233
- resetProvider: async () => {
234
- if (typeof provider?._resetClient === "function") await provider._resetClient();
235
- }
236
- }, statusState);
237
- forwarder.setOnIdle(() => {
238
- stopServerTyping();
239
- void forwarder.forwardFinalText();
240
- });
241
- // Wire the forwarder ownership probe unconditionally. wireEventQueueHandlers()
242
- // also sets this, but that path only runs when the event pipeline starts
243
- // (webhook enabled or event rules present). Without an event pipeline the
244
- // forwarder's ownerGetter stayed null and _isOwner() failed open, letting a
245
- // non-owner process forward transcript output (duplicate Discord sends).
246
- // The closure reads bridgeRuntimeConnected at call time as a fast-path AND;
247
- // bridgeRuntimeConnected alone can go stale (e.g. this process lost the seat
248
- // but has not yet observed it), so currentOwnerState().owned is re-read at
249
- // probe time as the source of truth for ownership.
250
- forwarder.setOwnerGetter(() => bridgeRuntimeConnected && currentOwnerState().owned);
251
- // ── Transcript binding cluster ──────────────────────────────────────────────
252
- // Extracted → lib/transcript-binding.mjs. Bound to live config/identity/owner
253
- // getters so file-level reference semantics (runtime reloads, ownership flips)
254
- // are preserved.
255
- const {
256
- sessionIdFromTranscriptPath,
257
- getPersistedTranscriptPath,
258
- pickUsableTranscriptPath,
259
- applyTranscriptBinding,
260
- cancelPendingTranscriptRearm,
261
- schedulePendingTranscriptRearm,
262
- rebindTranscriptContext,
263
- bindPersistedTranscriptIfAny,
264
- } = createTranscriptBinding({
265
- forwarder,
266
- statusState,
267
- readActiveInstance,
268
- refreshActiveInstance,
269
- instanceId: INSTANCE_ID,
270
- memoryIngestTranscript,
271
- memoryDrainBuffer,
272
- dropTrace,
273
- discoverSessionBoundTranscript,
274
- sameResolvedPath,
275
- getConfig: () => config,
276
- getChannelBridgeActive: () => channelBridgeActive,
277
- getBridgeRuntimeConnected: () => bridgeRuntimeConnected,
278
- RUNTIME_ROOT,
279
- });
280
156
  const scheduler = new Scheduler(
281
157
  config.nonInteractive ?? [],
282
158
  config.interactive ?? [],
@@ -305,15 +181,6 @@ let bridgeRuntimeConnected = false;
305
181
  // in-flight start does not revive owner state after the stop already tore
306
182
  // the partial-start state down.
307
183
  const ACTIVE_OWNER_STALE_MS = 1e4;
308
- // Owner gating here is multi-process runtime coordination: only the active
309
- // bindingReady gates all send paths until the boot-time refreshBridgeOwnership
310
- // ({ restoreBinding: true }) call completes. Without this, scheduler/webhook
311
- // emissions fired within the first ~few hundred ms after restart drop because
312
- // the Discord provider binding has not yet been established.
313
- let bindingReadyStatus = "pending";
314
- let _bindingReadyResolve;
315
- const bindingReady = new Promise((r) => { _bindingReadyResolve = r; });
316
- dropTrace("bindingReady.create", { status: bindingReadyStatus });
317
184
  // ── Bridge ownership snapshot + owner heartbeat ─────────────────────────────
318
185
  // Extracted → lib/owner-heartbeat.mjs. Owns its own heartbeat timer + last-note
319
186
  // dedup; bound to live identity + active-instance primitives.
@@ -350,19 +217,13 @@ const {
350
217
  getChannelBridgeActive: () => channelBridgeActive,
351
218
  instanceId: INSTANCE_ID,
352
219
  TERMINAL_LEAD_PID,
353
- forwarder,
354
220
  sendNotifyToParent,
355
221
  scheduler,
356
222
  statusState,
357
223
  logOwnership,
358
224
  currentOwnerState,
359
- bindPersistedTranscriptIfAny,
360
- cancelPendingTranscriptRearm,
361
- schedulePendingTranscriptRearm,
362
- stopServerTyping,
363
225
  wireWebhookHandlers,
364
226
  wireEventQueueHandlers,
365
- memoryDrainBuffer,
366
227
  });
367
228
  function injectAndRecord(channelId, name, content, options) {
368
229
  // Strip soft-warn marker blocks (Tool-loop / Repeated-input / legacy
@@ -372,7 +233,7 @@ function injectAndRecord(channelId, name, content, options) {
372
233
  // intentionally prepended onto tool RESULTS upstream (tool-loop-guard.mjs
373
234
  // build*Warn) so the model
374
235
  // self-corrects, but agent roles commonly echo them and we don't want them
375
- // surfacing in Discord / Lead channel push.
236
+ // surfacing in the Lead channel push.
376
237
  if (typeof content === 'string') content = stripSoftWarns(content);
377
238
  const ts = new Date().toISOString();
378
239
  const now = new Date();
@@ -382,39 +243,10 @@ function injectAndRecord(channelId, name, content, options) {
382
243
  if (options?.instruction) meta.instruction = options.instruction;
383
244
  if (options?.type) meta.type = options.type;
384
245
  // `silent_to_agent` — lifecycle status pings (worker/iter/started echoes)
385
- // surface on Discord but should NOT land in Lead's context window. When
386
- // set, skip the parent-notify hop but keep the Discord-forward + event-log
387
- // record. The meta flag is also propagated downstream so consumers that
388
- // still see the notification (e.g. Lead itself if emission changes later)
389
- // can recognise and drop it. Default is false → legacy behaviour preserved.
390
- if (options?.silent_to_agent) meta.silent_to_agent = true;
391
- const silent = options?.silent_to_agent === true;
392
- if (!silent) {
393
- sendNotifyToParent("notifications/claude/channel", { content, meta });
394
- } else {
395
- forwardLifecycleToDiscord(channelId, content);
396
- }
397
- }
398
-
399
- // Best-effort direct Discord emission for silent-to-agent lifecycle pings.
400
- // Only used when the parent-notify hop is skipped, so the user still sees
401
- // the status on Discord even though Lead will never echo it through the
402
- // transcript-tail forwarder. Falls back to a no-op when no channel is
403
- // resolvable — lifecycle pings are non-critical.
404
- function forwardLifecycleToDiscord(channelId, content) {
405
- try {
406
- // Skip rather than guess: lifecycle callers pass the channelId they own;
407
- // falling back to statusState.channelId can route to a stale/unrelated
408
- // channel when the caller did not supply one intentionally.
409
- const target = channelId || null;
410
- dropTrace("send.lifecycle.entry", { channelId: target || "(none)", bindingReadyStatus, providerPresent: !!provider?.sendMessage, preview: preview(content) });
411
- if (!target || !provider?.sendMessage) return;
412
- void bindingReady.then(() =>
413
- provider.sendMessage(target, content)
414
- .then(() => dropTrace("send.lifecycle.ok", { channelId: target }))
415
- .catch((err) => dropTrace("send.lifecycle.err", { channelId: target, err: String(err) }))
416
- ).catch(() => {});
417
- } catch { /* best-effort */ }
246
+ // must NOT land in Lead's context window. The channel relay that used to
247
+ // surface them is retired, so silent pings are dropped entirely.
248
+ if (options?.silent_to_agent === true) return;
249
+ sendNotifyToParent("notifications/claude/channel", { content, meta });
418
250
  }
419
251
  scheduler.setInjectHandler((channelId, name, content, options) => {
420
252
  injectAndRecord(channelId, name, content, options);
@@ -423,24 +255,15 @@ scheduler.setInjectHandler((channelId, name, content, options) => {
423
255
  // actually holds the live bridge seat; otherwise the scheduler falls back to
424
256
  // the visible-session run so the fire is never lost.
425
257
  scheduler.setInjectReadyCheck(() => bridgeRuntimeConnected && currentOwnerState().owned);
426
- scheduler.setSendHandler(async (channelId, text) => {
427
- dropTrace("send.scheduler.entry", { channelId, preview: preview(text) });
428
- await bindingReady;
429
- dropTrace("send.scheduler.ready", { channelId });
430
- try {
431
- await provider.sendMessage(channelId, text);
432
- dropTrace("send.scheduler.ok", { channelId });
433
- } catch (err) {
434
- dropTrace("send.scheduler.err", { channelId, err: String(err) });
435
- throw err;
436
- }
258
+ scheduler.setSendHandler(async () => {
259
+ // Channel delivery is retired: schedule results surface in the app session
260
+ // (parent notify / Automations row) only.
437
261
  });
438
262
  function wireWebhookHandlers() {
439
263
  if (!webhookServer) return;
440
264
  webhookServer.setEventPipeline(eventPipeline);
441
- // Webhook fires run as sessions (schedules parity). Delivery mode:
442
- // 'app' (default) → the Automations session row IS the surface;
443
- // 'channel'/'both' → the run result also relays to the main channel.
265
+ // Webhook fires run as sessions (schedules parity); the Automations
266
+ // session row is the only surface (channel relay retired).
444
267
  webhookServer.setBridgeDispatch(async ({ prompt, model, cwd, workflow, attachments, delivery, context }) => {
445
268
  const { runWebhookSession } = await import("../../shared/webhook-session-run.mjs");
446
269
  const run = await runWebhookSession({
@@ -452,14 +275,6 @@ function wireWebhookHandlers() {
452
275
  delivery: delivery || null,
453
276
  prompt,
454
277
  });
455
- const mode = String(delivery || "app");
456
- if ((mode === "channel" || mode === "both") && run?.result && provider?.sendMessage) {
457
- const target = scheduler.resolveChannel("");
458
- if (target) {
459
- void bindingReady.then(() => provider.sendMessage(target, run.result))
460
- .catch((err) => dropTrace("send.webhook.err", { channelId: target, err: String(err) }));
461
- }
462
- }
463
278
  return run;
464
279
  });
465
280
  }
@@ -474,127 +289,37 @@ function wireEventQueueHandlers(eventQueue) {
474
289
  // bridgeRuntimeConnected is a fast-path AND; currentOwnerState().owned is
475
290
  // re-read at probe time so a stale-connected flag cannot mask a lost seat.
476
291
  eventQueue.setOwnerGetter(() => bridgeRuntimeConnected && currentOwnerState().owned);
477
- forwarder.setOwnerGetter(() => bridgeRuntimeConnected && currentOwnerState().owned);
478
292
  }
479
- createInteractionHandlers({
480
- getProvider: () => provider,
481
- getBridgeRuntimeConnected: () => bridgeRuntimeConnected,
482
- instanceId: INSTANCE_ID,
483
- getBridgeOwnershipSnapshot,
484
- refreshBridgeOwnershipSafe,
485
- pendingSetup,
486
- buildModalRequestSpec,
487
- loadProfileConfig,
488
- sendNotifyToParent,
489
- scheduler,
490
- controlClaudeSession,
491
- writeTextFile,
492
- TURN_END_FILE,
493
- });
494
- const { isVoiceAttachment, transcribeVoice } = createVoiceTranscription({
495
- getConfig: () => config,
496
- dataDir: DATA_DIR,
497
- });
293
+ // Inbound messaging, provider interaction handlers, and channel voice-message
294
+ // transcription are DELETED with Discord/Telegram (user decision: PWA replaces
295
+ // channels). The headless provider emits no events, so nothing wires here.
498
296
  import { TOOL_DEFS } from '../tool-defs.mjs';
499
297
  // Tool dispatch in worker mode goes through the IPC `call` handler at the
500
298
  // bottom of this file (parent's `callWorker` → `handleToolCall`). There is no
501
299
  // orphan worker-level MCP Server: the parent (server.mjs) owns the single
502
300
  // connected transport and routes CallTool through the IPC `call` path.
503
- // ── Inbound routing / dedup / ownership helpers ─────────────────────────────
504
- // Extracted → lib/inbound-routing.mjs. Bound to live config/identity getters.
505
- const {
506
- writeChannelOwner,
507
- shouldDropDuplicateInbound,
508
- resolveInboundRoute,
509
- } = createInboundRouting({
510
- getConfig: () => config,
511
- getInstanceId: () => INSTANCE_ID,
512
- getChannelOwnerPath,
513
- });
514
301
  // ── Worker/HTTP tool-call dispatch ──────────────────────────────────────────
515
- // handleToolCall switch + bridge auto-connect retry wrapper. Extracted →
516
- // lib/tool-dispatch.mjs. The switch is entangled with ~8 runtime-lifecycle
517
- // functions plus mutable owner state (channelBridgeActive/bridgeRuntimeConnected)
518
- // and the forwarder; those are threaded as a lifecycle bag of lazy getters so
519
- // the module reads live file-level references at call time (original closure
520
- // semantics preserved). Used by the HTTP MCP CallTool path and the worker IPC
521
- // `call` handler at the bottom of this file.
302
+ // Runtime lifecycle dispatch only (activate_channel_bridge / reload_config):
303
+ // threaded as a lifecycle bag of lazy getters so the module reads live
304
+ // file-level references at call time. Used by the HTTP MCP CallTool path and
305
+ // the worker IPC `call` handler at the bottom of this file.
522
306
  const {
523
307
  handleToolCall,
524
308
  handleToolCallWithBridgeRetry,
525
309
  } = createToolDispatch({
526
- getForwarder: () => forwarder,
527
310
  isChannelsDegraded,
528
311
  lifecycle: {
529
- getBridgeRuntimeConnected: () => bridgeRuntimeConnected,
530
312
  getChannelBridgeActive: () => channelBridgeActive,
531
313
  getOwned: () => getBridgeOwnershipSnapshot().owned,
532
314
  setChannelBridgeActive: (v) => { channelBridgeActive = v; },
533
315
  writeBridgeState,
534
- stopServerTyping,
535
316
  notifyRemoteAcquired,
536
317
  refreshBridgeOwnership,
537
- bindPersistedTranscriptIfAny,
538
- // Lead-pushed repoint: bind the exact transcript the lead just created
539
- // (auto-acquire / newSession / resume / clear) instead of waiting for the
540
- // next inbound parent-chain steal. Idempotent + best-effort: same binding
541
- // path as the inbound steal (rebindTranscriptContext -> applyTranscriptBinding).
542
- rebindCurrentTranscript: async (transcriptPath) => {
543
- const cleanPath = typeof transcriptPath === "string" ? transcriptPath.trim() : "";
544
- if (!cleanPath) return;
545
- const channelId = statusState.read().channelId || config.channelId;
546
- if (!channelId) return;
547
- // Fail-closed: a malformed / not-yet-on-disk path must NEVER fall through
548
- // to rebindTranscriptContext's discovery loop (which would mutate the
549
- // binding onto a different session). Only an existing regular file binds;
550
- // otherwise log + return so the current binding is left untouched.
551
- let exists = false;
552
- try { exists = fs.statSync(cleanPath).isFile(); } catch { exists = false; }
553
- if (!exists) {
554
- process.stderr.write(`mixdog: rebind_current_transcript: ignoring non-existent path ${cleanPath}\n`);
555
- return;
556
- }
557
- // Idempotent: already bound to this exact transcript => no-op. In
558
- // particular do NOT re-run recoverUnsyncedTail, which is only meaningful
559
- // when the binding actually changes.
560
- if (forwarder.hasBinding() && sameResolvedPath(forwarder.transcriptPath, cleanPath)) return;
561
- // Binding changed: same path as the inbound steal's applyTranscriptBinding.
562
- applyTranscriptBinding(channelId, cleanPath, {
563
- persistStatus: true,
564
- recoverUnsyncedTail: true,
565
- });
566
- },
567
318
  startChannelBridge: () => start({ messaging: true }),
568
319
  stopOwnedRuntime,
569
320
  reloadRuntimeConfig,
570
321
  },
571
322
  });
572
- createInboundHandler({
573
- getProvider: () => provider,
574
- getConfig: () => config,
575
- getBridgeRuntimeConnected: () => bridgeRuntimeConnected,
576
- getChannelBridgeActive: () => channelBridgeActive,
577
- instanceId: INSTANCE_ID,
578
- forwarder,
579
- scheduler,
580
- statusState,
581
- getBridgeOwnershipSnapshot,
582
- refreshBridgeOwnershipSafe,
583
- writeChannelOwner,
584
- shouldDropDuplicateInbound,
585
- resolveInboundRoute,
586
- isVoiceAttachment,
587
- transcribeVoice,
588
- getPersistedTranscriptPath,
589
- sessionIdFromTranscriptPath,
590
- pickUsableTranscriptPath,
591
- applyTranscriptBinding,
592
- rebindTranscriptContext,
593
- sendNotifyToParent,
594
- memoryAppendEntry,
595
- startServerTyping,
596
- stopServerTyping,
597
- });
598
323
  async function init(_sharedMcp) {
599
324
  // _sharedMcp is no longer used. Notifications now flow via IPC to the parent
600
325
  // (sendNotifyToParent above). The parameter is retained for backward
@@ -626,17 +351,11 @@ async function start(options = {}) {
626
351
  // Daemon model: this runtime is the machine-global singleton bridge owner
627
352
  // (enforced by the standalone daemon's singleton-owner lock), so there is no
628
353
  // seat to claim and no contender to make-before-break against. Just connect
629
- // the owned runtime and bind the persisted transcript.
630
- const _bindingReadyStart = Date.now();
354
+ // the owned runtime.
631
355
  try {
632
- await startOwnedRuntime({ restoreBinding: true });
633
- bindingReadyStatus = "resolved";
634
- dropTrace("bindingReady.resolve", { elapsedMs: Date.now() - _bindingReadyStart, status: bindingReadyStatus });
635
- _bindingReadyResolve(true);
636
- } catch (e) {
637
- bindingReadyStatus = "rejected";
638
- dropTrace("bindingReady.reject", { elapsedMs: Date.now() - _bindingReadyStart, status: bindingReadyStatus, err: String(e) });
639
- _bindingReadyResolve(e);
356
+ await startOwnedRuntime();
357
+ } catch {
358
+ // Non-fatal: owned-runtime start errors degrade to automation-only mode.
640
359
  }
641
360
  // No-op under the daemon model (kept for call-site stability): there is no
642
361
  // ownership timer — the singleton daemon guarantees exactly one owner.
@@ -668,10 +387,6 @@ async function stop() {
668
387
  clearBridgeOwnershipTimer();
669
388
  if (_reloadDebounce) { clearTimeout(_reloadDebounce); _reloadDebounce = null; }
670
389
  if (_configWatcher) { try { _configWatcher.close(); } catch {} _configWatcher = null; }
671
- if (turnEndWatcher) {
672
- try { turnEndWatcher.close(); } catch {}
673
- turnEndWatcher = null;
674
- }
675
390
  }
676
391
  // ── IPC worker mode ──────────────────────────────────────────────
677
392
  // Skipped under the machine-global host (MIXDOG_DAEMON_HOST=1): the daemon