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.
- package/package.json +3 -1
- package/scripts/tool-stress.mjs +8 -1
- package/src/agents/maintainer/agent.json +1 -1
- package/src/defaults/skills/setup/SKILL.md +8 -43
- package/src/help.mjs +0 -2
- package/src/rules/lead/01-general.md +0 -4
- package/src/rules/shared/01-tool.md +10 -4
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +30 -4
- package/src/runtime/agent/orchestrator/agent-trace-timing.test.mjs +57 -0
- package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.test.mjs +40 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +7 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +0 -5
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +17 -4
- package/src/runtime/agent/orchestrator/tools/builtin/edit-sequential-occupation.test.mjs +40 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +150 -41
- package/src/runtime/agent/orchestrator/tools/builtin/local-search-telemetry.mjs +4 -0
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +34 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +18 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +10 -9
- package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +124 -18
- package/src/runtime/agent/orchestrator/tools/builtin/search-glob-top-k.test.mjs +39 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +7 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +82 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +78 -12
- package/src/runtime/agent/orchestrator/tools/lib/shell-warm-standby.mjs +34 -3
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +12 -5
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +87 -0
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
- package/src/runtime/channels/lib/config.mjs +14 -101
- package/src/runtime/channels/lib/index-drop-trace.mjs +2 -5
- package/src/runtime/channels/lib/owned-runtime.mjs +4 -80
- package/src/runtime/channels/lib/scheduler.mjs +75 -18
- package/src/runtime/channels/lib/status-snapshot.mjs +5 -68
- package/src/runtime/channels/lib/tool-dispatch.mjs +12 -55
- package/src/runtime/channels/lib/worker-ipc.mjs +1 -16
- package/src/runtime/channels/lib/worker-main.mjs +22 -307
- package/src/runtime/media/renditions.mjs +68 -5
- package/src/runtime/media/renditions.test.mjs +45 -0
- package/src/runtime/media/store.mjs +1 -0
- package/src/runtime/memory/index.mjs +4 -3
- package/src/runtime/memory/lib/cycle-scheduler.mjs +17 -6
- package/src/runtime/memory/lib/memory-cycle-packets.test.mjs +7 -1
- package/src/runtime/memory/lib/memory-cycle-requests.mjs +13 -0
- package/src/runtime/shared/config.mjs +8 -49
- package/src/runtime/shared/schedule-time.mjs +25 -0
- package/src/runtime/shared/schedule-time.test.mjs +29 -0
- package/src/runtime/shared/schedules-db.mjs +73 -1
- package/src/runtime/shared/tool-card-model.mjs +1 -1
- package/src/runtime/shared/transcript-writer.mjs +10 -17
- package/src/runtime/shared/update-checker.mjs +7 -2
- package/src/runtime/shared/update-checker.test.mjs +33 -0
- package/src/session-runtime/channel-config-api.mjs +3 -20
- package/src/session-runtime/config-lifecycle.mjs +1 -42
- package/src/session-runtime/lifecycle-api.mjs +1 -19
- package/src/session-runtime/model-route-api.mjs +0 -2
- package/src/session-runtime/model-settings-persist.test.mjs +0 -1
- package/src/session-runtime/prewarm.mjs +2 -7
- package/src/session-runtime/prewarm.test.mjs +0 -1
- package/src/session-runtime/runtime-core.mjs +6 -185
- package/src/session-runtime/session-lifecycle.mjs +4 -17
- package/src/session-runtime/session-transcript.mjs +54 -0
- package/src/session-runtime/session-turn-api.mjs +4 -44
- package/src/session-runtime/settings-api.mjs +0 -45
- package/src/session-runtime/tool-catalog-schema.mjs +1 -1
- package/src/standalone/channel-admin.mjs +8 -150
- package/src/standalone/daemon.mjs +5 -48
- package/src/standalone/seeds.mjs +18 -1
- package/src/standalone/session-protocol.mjs +0 -8
- package/src/standalone/session-service.mjs +1 -3
- package/src/tui/App.jsx +0 -5
- package/src/tui/app/app-view.jsx +1 -2
- package/src/tui/app/create-app-pickers.mjs +0 -20
- package/src/tui/app/doctor.mjs +2 -6
- package/src/tui/app/prompt-submit.mjs +2 -49
- package/src/tui/app/settings-picker.mjs +22 -96
- package/src/tui/app/slash-commands.mjs +0 -2
- package/src/tui/app/slash-dispatch.mjs +0 -17
- package/src/tui/app/use-prompt-handlers.mjs +4 -1
- package/src/tui/components/StatusLine.jsx +4 -4
- package/src/tui/dist/index.mjs +1003 -1461
- package/src/tui/session/context-state.mjs +0 -1
- package/src/tui/session/queue-helpers.mjs +1 -1
- package/src/tui/session/queue-helpers.test.mjs +10 -1
- package/src/tui/session/session-api-ext.mjs +0 -89
- package/src/tui/session/session-api.mjs +0 -2
- package/src/tui/session/tool-card-results.mjs +1 -1
- package/src/tui/session/tool-result-status.mjs +3 -3
- package/src/tui/session/tool-result-status.test.mjs +3 -3
- package/src/tui/session-local.mjs +0 -26
- package/src/workflows/default/WORKFLOW.md +13 -15
- package/src/workflows/solo/WORKFLOW.md +6 -7
- package/src/runtime/channels/lib/config-lock.mjs +0 -13
- package/src/runtime/channels/lib/drop-trace.mjs +0 -71
- package/src/runtime/channels/lib/format.mjs +0 -190
- package/src/runtime/channels/lib/inbound-handler.mjs +0 -387
- package/src/runtime/channels/lib/inbound-routing.mjs +0 -81
- package/src/runtime/channels/lib/interaction-handlers.mjs +0 -70
- package/src/runtime/channels/lib/interaction-workflows.mjs +0 -76
- package/src/runtime/channels/lib/memory-client.mjs +0 -289
- package/src/runtime/channels/lib/output-forwarder.mjs +0 -934
- package/src/runtime/channels/lib/session-control.mjs +0 -36
- package/src/runtime/channels/lib/session-discovery.mjs +0 -217
- package/src/runtime/channels/lib/session-discovery.test.mjs +0 -32
- package/src/runtime/channels/lib/telegram-format.mjs +0 -280
- package/src/runtime/channels/lib/tool-format.mjs +0 -125
- package/src/runtime/channels/lib/transcript-binding.mjs +0 -336
- package/src/runtime/channels/lib/transcript-discovery.mjs +0 -208
- package/src/runtime/channels/providers/discord-access.mjs +0 -30
- package/src/runtime/channels/providers/discord-attachments.mjs +0 -69
- package/src/runtime/channels/providers/discord-gateway.mjs +0 -230
- package/src/runtime/channels/providers/discord.mjs +0 -730
- package/src/runtime/channels/providers/telegram.mjs +0 -598
- package/src/runtime/channels/providers/telegram.test.mjs +0 -75
- package/src/session-runtime/remote-control.mjs +0 -126
- package/src/session-runtime/remote-transcript.mjs +0 -134
- package/src/session-runtime/remote-transition-queue.mjs +0 -14
- package/src/tui/app/channel-pickers.mjs +0 -384
|
@@ -1,29 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
103
|
-
//
|
|
104
|
-
|
|
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
|
-
//
|
|
129
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
386
|
-
//
|
|
387
|
-
|
|
388
|
-
|
|
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 (
|
|
427
|
-
|
|
428
|
-
|
|
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)
|
|
442
|
-
//
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
//
|
|
516
|
-
//
|
|
517
|
-
//
|
|
518
|
-
//
|
|
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
|
|
630
|
-
const _bindingReadyStart = Date.now();
|
|
354
|
+
// the owned runtime.
|
|
631
355
|
try {
|
|
632
|
-
await startOwnedRuntime(
|
|
633
|
-
|
|
634
|
-
|
|
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
|