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.
- package/README.md +0 -17
- package/package.json +8 -7
- package/scripts/build-tui.mjs +6 -6
- package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
- package/src/defaults/skills/setup/SKILL.md +1 -1
- package/src/lib/keychain-cjs.cjs +1 -1
- package/src/lib/mixdog-debug.cjs +2 -3
- package/src/rules/lead/01-general.md +1 -2
- package/src/rules/lead/lead-tool.md +0 -3
- package/src/rules/shared/01-tool.md +11 -10
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
- package/src/runtime/agent/orchestrator/mcp/client.mjs +81 -33
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +8 -6
- package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -4
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +20 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +4 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +40 -25
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
- package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +59 -42
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +12 -22
- package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
- package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +80 -30
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +41 -22
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +55 -11
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +55 -18
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +67 -21
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -1
- package/src/runtime/channels/lib/config.mjs +33 -33
- package/src/runtime/channels/lib/inbound-handler.mjs +7 -7
- package/src/runtime/channels/lib/interaction-handlers.mjs +6 -6
- package/src/runtime/channels/lib/memory-client.mjs +31 -157
- package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
- package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
- package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
- package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
- package/src/runtime/channels/lib/runtime-paths.mjs +4 -30
- package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
- package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
- package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
- package/src/runtime/channels/lib/worker-ipc.mjs +7 -7
- package/src/runtime/channels/lib/worker-main.mjs +38 -38
- package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
- package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
- package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
- package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/index.mjs +72 -211
- package/src/runtime/memory/lib/agent-ipc.mjs +9 -8
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +2 -2
- package/src/runtime/memory/lib/http-router.mjs +1 -6
- package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
- package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
- package/src/runtime/memory/lib/memory-log.mjs +1 -1
- package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
- package/src/runtime/memory/lib/memory.mjs +1 -1
- package/src/runtime/memory/lib/session-ingest.mjs +1 -1
- package/src/runtime/search/index.mjs +2 -2
- package/src/runtime/shared/abort-race.mjs +70 -0
- package/src/runtime/shared/child-spawn-gate.mjs +101 -63
- package/src/runtime/shared/config.mjs +11 -24
- package/src/runtime/shared/llm/index.mjs +1 -1
- package/src/runtime/shared/owner-fair-gate.mjs +134 -0
- package/src/runtime/shared/resource-admission.mjs +6 -5
- package/src/runtime/shared/tool-execution-owner.mjs +12 -0
- package/src/runtime/shared/tool-result-summary.mjs +1 -1
- package/src/runtime/shared/tool-surface.mjs +91 -22
- package/src/runtime/shared/tool-workload-gates.mjs +29 -0
- package/src/runtime/shared/turn-snapshot.mjs +17 -2
- package/src/session-runtime/channel-config-api.mjs +5 -5
- package/src/session-runtime/config-lifecycle.mjs +27 -27
- package/src/session-runtime/lifecycle-api.mjs +22 -3
- package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
- package/src/session-runtime/prewarm.mjs +1 -1
- package/src/session-runtime/provider-models.mjs +1 -1
- package/src/session-runtime/remote-control.mjs +6 -7
- package/src/session-runtime/runtime-core.mjs +52 -28
- package/src/session-runtime/self-update.mjs +1 -1
- package/src/session-runtime/session-lifecycle.mjs +36 -15
- package/src/session-runtime/session-title.mjs +1 -1
- package/src/session-runtime/session-turn-api.mjs +55 -15
- package/src/session-runtime/settings-api.mjs +7 -7
- package/src/standalone/agent-dispatch-broker.mjs +1 -1
- package/src/standalone/channel-admin.mjs +42 -53
- package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +16 -18
- package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +45 -46
- package/src/standalone/channel-worker.mjs +124 -647
- package/src/standalone/{backend-daemon.mjs → daemon.mjs} +187 -132
- package/src/standalone/memory-runtime-proxy.mjs +58 -1
- package/src/standalone/{engine-daemon-client.mjs → session-client.mjs} +151 -86
- package/src/standalone/session-protocol.mjs +8 -4
- package/src/standalone/session-runtime-pool.mjs +393 -0
- package/src/standalone/session-runtime-record.mjs +20 -0
- package/src/standalone/session-runtime-worker.mjs +203 -0
- package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +170 -157
- package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +139 -61
- package/src/standalone/session-wire.mjs +55 -0
- package/src/tui/App.jsx +4 -4
- package/src/tui/app/app-format.mjs +1 -1
- package/src/tui/app/channel-pickers.mjs +41 -41
- package/src/tui/app/input-parsers.mjs +2 -2
- package/src/tui/app/onboarding-steps.mjs +2 -2
- package/src/tui/app/project-picker.mjs +8 -8
- package/src/tui/app/prompt-submit.mjs +8 -8
- package/src/tui/app/settings-picker.mjs +25 -25
- package/src/tui/app/use-prompt-queue-history.mjs +1 -1
- package/src/tui/app/use-transcript-scroll.mjs +1 -1
- package/src/tui/components/ToolExecution.jsx +1 -1
- package/src/tui/components/prompt-input/restore-policy.mjs +1 -1
- package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
- package/src/tui/dist/index.mjs +154 -127
- package/src/tui/hooks/useSession.mjs +16 -0
- package/src/tui/index.jsx +11 -11
- package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
- package/src/tui/{engine → session}/agent-job-feed.mjs +4 -4
- package/src/tui/{engine → session}/boot-profile.mjs +2 -2
- package/src/tui/{engine → session}/context-state.mjs +3 -3
- package/src/tui/{engine → session}/labels.mjs +2 -2
- package/src/tui/{engine → session}/live-share.mjs +7 -7
- package/src/tui/{engine → session}/notice-text.mjs +2 -2
- package/src/tui/{engine → session}/notification-plan.mjs +3 -3
- package/src/tui/session/oauth-flows.mjs +140 -0
- package/src/tui/{engine → session}/prompt-history.mjs +3 -3
- package/src/tui/{engine → session}/queue-helpers.mjs +2 -2
- package/src/tui/{engine → session}/render-timing.mjs +2 -2
- package/src/tui/{engine → session}/session-api-ext.mjs +30 -17
- package/src/tui/{engine → session}/session-api.mjs +7 -60
- package/src/tui/{engine → session}/session-flow.mjs +1 -1
- package/src/tui/{engine → session}/session-stats.mjs +3 -3
- package/src/tui/{engine → session}/tool-approval.mjs +3 -3
- package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
- package/src/tui/{engine → session}/tool-card-results.mjs +4 -4
- package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
- package/src/tui/{engine → session}/tool-result-text.mjs +3 -3
- package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
- package/src/tui/{engine → session}/turn.mjs +29 -107
- package/src/tui/{engine-local-session.mjs → session-local.mjs} +59 -75
- package/src/tui/session.mjs +22 -0
- package/src/ui/statusline-segments.mjs +1 -1
- package/src/ui/statusline.mjs +1 -1
- package/vendor/ink/build/components/App.js +10 -1
- package/src/session-runtime/memory-daemon-probe.mjs +0 -61
- package/src/standalone/engine-daemon-protocol.mjs +0 -32
- package/src/tui/engine/turn-watchdog.mjs +0 -92
- package/src/tui/engine.mjs +0 -22
- package/src/tui/hooks/useEngine.mjs +0 -16
- /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
- /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
- /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
- /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
- /package/src/tui/{engine → session}/tui-steering-persist.mjs +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
|
-
import { loadConfig,
|
|
2
|
+
import { loadConfig, createProvider } from "./config.mjs";
|
|
3
3
|
import { WebhookServer } from "./webhook.mjs";
|
|
4
4
|
import { EventPipeline } from "./event-pipeline.mjs";
|
|
5
5
|
import { startSnapshotWriter, stopSnapshotWriter } from "./status-snapshot.mjs";
|
|
@@ -12,16 +12,16 @@ import {
|
|
|
12
12
|
clearActiveInstance,
|
|
13
13
|
} from "./runtime-paths.mjs";
|
|
14
14
|
// Owned-runtime lifecycle extracted from channels/index.mjs (behavior-
|
|
15
|
-
// preserving): bridge-ownership claim/refresh/loss,
|
|
15
|
+
// preserving): bridge-ownership claim/refresh/loss, provider connect/disconnect,
|
|
16
16
|
// scheduler + webhook/event runtime, owner heartbeat gating, and config
|
|
17
|
-
// hot-reload. Owns its own in-flight flags + timers; shares config /
|
|
17
|
+
// hot-reload. Owns its own in-flight flags + timers; shares config / provider /
|
|
18
18
|
// bridgeRuntimeConnected / webhookServer / eventPipeline with the worker via
|
|
19
19
|
// get/set so file-level reference semantics are preserved.
|
|
20
20
|
export function createOwnedRuntime({
|
|
21
21
|
getConfig,
|
|
22
22
|
setConfig,
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
getProvider,
|
|
24
|
+
setProvider,
|
|
25
25
|
getBridgeRuntimeConnected,
|
|
26
26
|
setBridgeRuntimeConnected,
|
|
27
27
|
getWebhookServer,
|
|
@@ -50,16 +50,16 @@ export function createOwnedRuntime({
|
|
|
50
50
|
let bridgeOwnershipRefreshInFlight = null;
|
|
51
51
|
let _memoryDrainTimer = null;
|
|
52
52
|
// Automation (scheduler + webhook server + relay tunnel) can outlive a
|
|
53
|
-
// failed/absent messaging
|
|
53
|
+
// failed/absent messaging provider: it is tracked separately so teardown and
|
|
54
54
|
// reload know it is running even while bridgeRuntimeConnected stays false.
|
|
55
55
|
let automationRunning = false;
|
|
56
56
|
let automationStartPromise = null;
|
|
57
57
|
// Promise that resolves when the current startOwnedRuntime() run fully
|
|
58
58
|
// settles (bridgeRuntimeStarting -> false). reloadRuntimeConfig awaits this
|
|
59
|
-
// before issuing a restart so a
|
|
59
|
+
// before issuing a restart so a provider swap that lands mid-start is not
|
|
60
60
|
// dropped by startOwnedRuntime's in-flight guard (lost-restart race).
|
|
61
61
|
let _inFlightStart = null;
|
|
62
|
-
// Daemon model: the machine-global
|
|
62
|
+
// Daemon model: the machine-global service (singleton-owner lock in
|
|
63
63
|
// src/standalone) guarantees exactly one runtime per machine, so this process
|
|
64
64
|
// is the unconditional bridge owner. The OS seat lock is retired — no takeover
|
|
65
65
|
// handler, no vacant-reacquire poll, no cross-process ownership-loss detection.
|
|
@@ -158,7 +158,7 @@ async function startAutomationRuntime() {
|
|
|
158
158
|
startSnapshotWriter(scheduler);
|
|
159
159
|
syncOwnedWebhookAndEventRuntime();
|
|
160
160
|
automationRunning = true;
|
|
161
|
-
process.stderr.write('mixdog: automation runtime up without messaging
|
|
161
|
+
process.stderr.write('mixdog: automation runtime up without messaging provider\n');
|
|
162
162
|
})();
|
|
163
163
|
automationStartPromise = run;
|
|
164
164
|
try {
|
|
@@ -175,8 +175,8 @@ let _ownedRuntimeSelfHealing = false;
|
|
|
175
175
|
// activation/reload recovery only.
|
|
176
176
|
async function selfHealOwnedRuntime(options = {}) {
|
|
177
177
|
const shouldRestoreBinding = options.restoreBinding === true;
|
|
178
|
-
const
|
|
179
|
-
if (!shouldRestoreBinding && !
|
|
178
|
+
const shouldResetProvider = options.resetProvider === true;
|
|
179
|
+
if (!shouldRestoreBinding && !shouldResetProvider) return;
|
|
180
180
|
if (_ownedRuntimeSelfHealing) return;
|
|
181
181
|
_ownedRuntimeSelfHealing = true;
|
|
182
182
|
try {
|
|
@@ -185,9 +185,9 @@ async function selfHealOwnedRuntime(options = {}) {
|
|
|
185
185
|
process.stderr.write(`mixdog: self-heal bindPersistedTranscriptIfAny failed (non-fatal): ${e instanceof Error ? e.message : String(e)}\n`);
|
|
186
186
|
});
|
|
187
187
|
}
|
|
188
|
-
if (
|
|
189
|
-
await
|
|
190
|
-
process.stderr.write(`mixdog: self-heal
|
|
188
|
+
if (shouldResetProvider && typeof getProvider()?._resetClient === "function") {
|
|
189
|
+
await getProvider()._resetClient().catch((e) => {
|
|
190
|
+
process.stderr.write(`mixdog: self-heal getProvider() reset failed (non-fatal): ${e instanceof Error ? e.message : String(e)}\n`);
|
|
191
191
|
});
|
|
192
192
|
}
|
|
193
193
|
// Do NOT nudge forwardNewText() here. bindPersistedTranscriptIfAny() above
|
|
@@ -217,28 +217,28 @@ async function startOwnedRuntime(options = {}) {
|
|
|
217
217
|
_settleStart = null;
|
|
218
218
|
done?.();
|
|
219
219
|
};
|
|
220
|
-
// Capture the
|
|
221
|
-
// reloadRuntimeConfig() hot-swap can replace the global `
|
|
220
|
+
// Capture the getProvider() instance that THIS start operation will connect. A
|
|
221
|
+
// reloadRuntimeConfig() hot-swap can replace the global `getProvider()` while this
|
|
222
222
|
// start is still awaiting connect(); using the captured instance for both
|
|
223
223
|
// connect() and the bail-path disconnect() guarantees we tear down the
|
|
224
|
-
//
|
|
225
|
-
// both-
|
|
226
|
-
const
|
|
224
|
+
// getProvider() WE started (not the freshly-swapped one), closing the
|
|
225
|
+
// both-providers-live window.
|
|
226
|
+
const startingProvider = getProvider();
|
|
227
227
|
// Daemon model: this runtime is the singleton bridge owner (enforced by the
|
|
228
228
|
// standalone daemon's singleton-owner lock), so there is no seat to claim and
|
|
229
229
|
// no cross-process contender to back off from. Just advertise metadata
|
|
230
|
-
// (
|
|
230
|
+
// (pg_*/channelId/... preserved inside refreshActiveInstance);
|
|
231
231
|
// active-instance.json is a pure advert. Wrapped so any throw resets
|
|
232
232
|
// bridgeRuntimeStarting.
|
|
233
233
|
try {
|
|
234
|
-
refreshActiveInstance(instanceId, {
|
|
234
|
+
refreshActiveInstance(instanceId, { providerReady: false });
|
|
235
235
|
} catch (e) {
|
|
236
236
|
settleInFlightStart();
|
|
237
237
|
process.stderr.write(`mixdog: pre-connect metadata advert aborted (${e instanceof Error ? e.message : String(e)})\n`);
|
|
238
238
|
return;
|
|
239
239
|
}
|
|
240
240
|
// Periodic buffer drain: replays memory-buffer/entry-*/ingest-*.json once the
|
|
241
|
-
// memory
|
|
241
|
+
// daemon-hosted memory runtime is ready. Idempotent + reentrancy-guarded inside
|
|
242
242
|
// drainBuffer(); unref'd so it never holds the worker open.
|
|
243
243
|
if (!_memoryDrainTimer) {
|
|
244
244
|
_memoryDrainTimer = setInterval(() => { void memoryDrainBuffer().catch(() => {}); }, 5e3);
|
|
@@ -249,11 +249,11 @@ async function startOwnedRuntime(options = {}) {
|
|
|
249
249
|
// webhook/binding launches below would revive owner state after stop).
|
|
250
250
|
// Idempotent: stop's sync teardown already ran; re-running disconnect +
|
|
251
251
|
// teardown is safe and covers both the pre-connected window (stop could
|
|
252
|
-
// not disconnect an in-flight
|
|
252
|
+
// not disconnect an in-flight getProvider()) and the post-connected window
|
|
253
253
|
// (stop did disconnect; redo to be defensive).
|
|
254
254
|
const bailIfStopRequested = async () => {
|
|
255
255
|
if (!_ownedRuntimeStopRequested) return false;
|
|
256
|
-
try { await
|
|
256
|
+
try { await startingProvider.disconnect(); } catch {}
|
|
257
257
|
try { releaseOwnedChannelLocks(instanceId); } catch {}
|
|
258
258
|
try { clearActiveInstance(instanceId); } catch {}
|
|
259
259
|
setBridgeRuntimeConnected(false);
|
|
@@ -266,20 +266,20 @@ async function startOwnedRuntime(options = {}) {
|
|
|
266
266
|
process.stderr.write(`mixdog: bindPersistedTranscriptIfAny failed (non-fatal): ${e instanceof Error ? e.message : String(e)}\n`);
|
|
267
267
|
})
|
|
268
268
|
: null;
|
|
269
|
-
// Await
|
|
269
|
+
// Await getProvider().connect() so callers (and bindingReady) only resolve after
|
|
270
270
|
// the Discord binding is real. Previously this was fire-and-forget and
|
|
271
271
|
// refreshBridgeOwnership returned immediately, letting bindingReady fire
|
|
272
|
-
// before
|
|
272
|
+
// before getProvider() listeners were attached.
|
|
273
273
|
try {
|
|
274
|
-
await
|
|
274
|
+
await startingProvider.connect();
|
|
275
275
|
if (await bailIfStopRequested()) {
|
|
276
276
|
cancelPendingTranscriptRearm();
|
|
277
277
|
try { forwarder.stopWatch(); } catch {}
|
|
278
278
|
if (bindPersistedTranscriptTask) await bindPersistedTranscriptTask;
|
|
279
279
|
return;
|
|
280
280
|
}
|
|
281
|
-
// Advertise
|
|
282
|
-
try { refreshActiveInstance(instanceId, {
|
|
281
|
+
// Advertise provider readiness (metadata advert).
|
|
282
|
+
try { refreshActiveInstance(instanceId, { providerReady: true }); } catch {}
|
|
283
283
|
setBridgeRuntimeConnected(true);
|
|
284
284
|
// Fresh confirmed connection — tell the parent to flip remote ON. Reached
|
|
285
285
|
// ONLY on a not-connected -> connected transition (the top-of-fn
|
|
@@ -327,18 +327,18 @@ async function startOwnedRuntime(options = {}) {
|
|
|
327
327
|
});
|
|
328
328
|
}
|
|
329
329
|
}
|
|
330
|
-
process.stderr.write(`mixdog: running with ${
|
|
330
|
+
process.stderr.write(`mixdog: running with ${getProvider().name} getProvider()\n`);
|
|
331
331
|
logOwnership(`active owner lead=${TERMINAL_LEAD_PID} pid=${process.pid}`);
|
|
332
332
|
} catch (e) {
|
|
333
|
-
process.stderr.write(`mixdog:
|
|
333
|
+
process.stderr.write(`mixdog: getProvider() connect failed (non-fatal, cycle1/MCP still up): ${e instanceof Error ? e.message : String(e)}\n`);
|
|
334
334
|
cancelPendingTranscriptRearm();
|
|
335
335
|
try { forwarder.stopWatch(); } catch {}
|
|
336
336
|
if (bindPersistedTranscriptTask) await bindPersistedTranscriptTask;
|
|
337
337
|
// Roll back partial owner-side state advertised before connect() ran:
|
|
338
|
-
// disconnect the
|
|
338
|
+
// disconnect the provider WE started (a post-connect startup step may have
|
|
339
339
|
// thrown while the gateway is live), then release the channel locks + clear
|
|
340
340
|
// the active-instance advert.
|
|
341
|
-
try { await
|
|
341
|
+
try { await startingProvider.disconnect(); } catch {}
|
|
342
342
|
try { releaseOwnedChannelLocks(instanceId); } catch {}
|
|
343
343
|
try { clearActiveInstance(instanceId); } catch {}
|
|
344
344
|
if (_memoryDrainTimer) { clearInterval(_memoryDrainTimer); _memoryDrainTimer = null; }
|
|
@@ -370,13 +370,13 @@ async function stopOwnedRuntime(reason) {
|
|
|
370
370
|
// (startWatch is not owner-gated), leaking a live watcher past shutdown.
|
|
371
371
|
cancelPendingTranscriptRearm();
|
|
372
372
|
// startOwnedRuntime() advertises owner HTTP/heartbeat/active-instance and
|
|
373
|
-
// claims channel locks BEFORE awaiting
|
|
373
|
+
// claims channel locks BEFORE awaiting getProvider().connect(). If shutdown lands
|
|
374
374
|
// during that window (bridgeRuntimeStarting=true, getBridgeRuntimeConnected()
|
|
375
375
|
// still false) we still need to tear that partial state down — otherwise
|
|
376
376
|
// the port stays bound and active-instance.json stays stale.
|
|
377
377
|
if (!getBridgeRuntimeConnected() && !bridgeRuntimeStarting && !automationRunning && !automationStartPromise) return;
|
|
378
378
|
// If a start is in flight (bridgeRuntimeStarting=true), signal the in-flight
|
|
379
|
-
// startOwnedRuntime() to abort right after its
|
|
379
|
+
// startOwnedRuntime() to abort right after its getProvider().connect() resolves.
|
|
380
380
|
// Without this the in-flight start re-marks connected and re-launches
|
|
381
381
|
// scheduler/webhook/heartbeat after we tear them down here.
|
|
382
382
|
if (bridgeRuntimeStarting || automationStartPromise) _ownedRuntimeStopRequested = true;
|
|
@@ -398,15 +398,15 @@ async function stopOwnedRuntime(reason) {
|
|
|
398
398
|
releaseOwnedChannelLocks(instanceId);
|
|
399
399
|
clearActiveInstance(instanceId);
|
|
400
400
|
try {
|
|
401
|
-
// Only disconnect the
|
|
401
|
+
// Only disconnect the getProvider() when connect() actually completed; calling
|
|
402
402
|
// disconnect() mid-connect races the connect promise.
|
|
403
403
|
if (wasConnected) {
|
|
404
404
|
// Drain in-flight outbound sends before disconnecting so a handoff
|
|
405
405
|
// (owned=false observed → ownership lost) never cuts off a reply
|
|
406
406
|
// mid-delivery. Bounded inside drainPendingSends so a wedged send can
|
|
407
407
|
// not stall teardown — we still disconnect promptly.
|
|
408
|
-
try { await
|
|
409
|
-
await
|
|
408
|
+
try { await getProvider().drainPendingSends?.(); } catch {}
|
|
409
|
+
await getProvider().disconnect();
|
|
410
410
|
}
|
|
411
411
|
} finally {
|
|
412
412
|
setBridgeRuntimeConnected(false);
|
|
@@ -442,7 +442,7 @@ function notifyRemoteAcquired() {
|
|
|
442
442
|
sendNotifyToParent('notifications/mixdog/remote', { state: 'acquired' });
|
|
443
443
|
}
|
|
444
444
|
async function refreshBridgeOwnership(options = {}) {
|
|
445
|
-
// Coalesce concurrent callers onto the in-flight refresh so
|
|
445
|
+
// Coalesce concurrent callers onto the in-flight refresh so getProvider() tool
|
|
446
446
|
// calls landing during normal login wait for the same connect attempt
|
|
447
447
|
// instead of returning early and observing spurious auto-connect failure.
|
|
448
448
|
if (bridgeOwnershipRefreshInFlight) return bridgeOwnershipRefreshInFlight;
|
|
@@ -465,8 +465,8 @@ async function refreshBridgeOwnership(options = {}) {
|
|
|
465
465
|
}
|
|
466
466
|
|
|
467
467
|
async function reloadRuntimeConfig() {
|
|
468
|
-
const
|
|
469
|
-
const
|
|
468
|
+
const previousProvider = getProvider();
|
|
469
|
+
const previousProviderName = previousProvider?.name || "";
|
|
470
470
|
// File-watch/tool-triggered reloads must bypass the short keychain hit cache:
|
|
471
471
|
// another process may have just saved or rotated the channel credential.
|
|
472
472
|
setConfig(await loadConfig({ freshSecrets: true }));
|
|
@@ -478,37 +478,37 @@ async function reloadRuntimeConfig() {
|
|
|
478
478
|
// The scheduler must be RE-ARMED by the reload whenever it is supposed to
|
|
479
479
|
// be running. `restart: false` only destroys the cron/one-shot bindings and
|
|
480
480
|
// hands lifecycle back to the caller — but on an automation-only install
|
|
481
|
-
// (no messaging
|
|
481
|
+
// (no messaging provider) startAutomationRuntime() is already past its
|
|
482
482
|
// `automationRunning` guard, so nobody would ever call scheduler.start()
|
|
483
483
|
// again and every saved/edited schedule stayed silently disarmed until the
|
|
484
484
|
// daemon restarted.
|
|
485
485
|
{ restart: automationRunning || getBridgeRuntimeConnected() }
|
|
486
486
|
);
|
|
487
|
-
const
|
|
488
|
-
const
|
|
489
|
-
const credentialsChanged = !
|
|
490
|
-
&& String(
|
|
491
|
-
const
|
|
492
|
-
if (
|
|
487
|
+
const nextProvider = createProvider(getConfig());
|
|
488
|
+
const providerTypeChanged = (nextProvider?.name || "") !== previousProviderName;
|
|
489
|
+
const credentialsChanged = !providerTypeChanged
|
|
490
|
+
&& String(nextProvider?.token || "") !== String(previousProvider?.token || "");
|
|
491
|
+
const providerChanged = providerTypeChanged || credentialsChanged;
|
|
492
|
+
if (providerChanged) {
|
|
493
493
|
const shouldRestart = getBridgeRuntimeConnected() || bridgeRuntimeStarting;
|
|
494
|
-
if (shouldRestart) await stopOwnedRuntime("
|
|
494
|
+
if (shouldRestart) await stopOwnedRuntime("getProvider() getConfig() changed");
|
|
495
495
|
// A start that was in flight when stopOwnedRuntime landed was signalled to
|
|
496
|
-
// bail (and disconnects the OLD
|
|
496
|
+
// bail (and disconnects the OLD provider it was connecting). Wait for it to
|
|
497
497
|
// FULLY settle before issuing the fresh start below — otherwise
|
|
498
498
|
// startOwnedRuntime's `if (bridgeRuntimeStarting) return` guard would drop
|
|
499
|
-
// the restart and the NEW
|
|
499
|
+
// the restart and the NEW provider would never connect (lost-restart race).
|
|
500
500
|
if (_inFlightStart) { try { await _inFlightStart; } catch {} }
|
|
501
|
-
|
|
502
|
-
// The persisted routing channelId belongs to the OLD
|
|
501
|
+
setProvider(nextProvider);
|
|
502
|
+
// The persisted routing channelId belongs to the OLD getProvider() (e.g. a
|
|
503
503
|
// Discord snowflake) and is meaningless for the new one — sending to it
|
|
504
504
|
// would 400 "chat not found". There is no id mapping between platforms, so
|
|
505
505
|
// CLEAR the stale binding: drop the forwarder's context + watcher and wipe
|
|
506
|
-
// status.channelId/transcriptPath. The next inbound from the new
|
|
506
|
+
// status.channelId/transcriptPath. The next inbound from the new getProvider()
|
|
507
507
|
// rebinds the correct chat via applyTranscriptBinding(). Only done on
|
|
508
|
-
//
|
|
508
|
+
// providerChanged — same-getProvider() reloads keep their binding untouched.
|
|
509
509
|
// (active-instance is cleared by stopOwnedRuntime on the restart path; we
|
|
510
510
|
// don't re-advertise here to avoid resurrecting a just-cleared entry.)
|
|
511
|
-
if (
|
|
511
|
+
if (providerTypeChanged) {
|
|
512
512
|
try { forwarder.stopWatch(); } catch {}
|
|
513
513
|
forwarder.channelId = "";
|
|
514
514
|
forwarder.transcriptPath = "";
|
|
@@ -520,11 +520,11 @@ async function reloadRuntimeConfig() {
|
|
|
520
520
|
} catch {}
|
|
521
521
|
}
|
|
522
522
|
// stopOwnedRuntime above tore the owned runtime down; a same-session reload
|
|
523
|
-
// reconnects the NEW
|
|
523
|
+
// reconnects the NEW provider here. The in-flight start (if any) has already
|
|
524
524
|
// settled above, so this start is not dropped by the in-flight guard.
|
|
525
|
-
if (shouldRestart) refreshBridgeOwnershipSafe({ restoreBinding: !
|
|
526
|
-
} else if (
|
|
527
|
-
try { await
|
|
525
|
+
if (shouldRestart) refreshBridgeOwnershipSafe({ restoreBinding: !providerTypeChanged });
|
|
526
|
+
} else if (nextProvider !== previousProvider) {
|
|
527
|
+
try { await nextProvider.disconnect?.(); } catch {}
|
|
528
528
|
}
|
|
529
529
|
if (getBridgeRuntimeConnected() || automationRunning) {
|
|
530
530
|
syncOwnedWebhookAndEventRuntime({ reload: true });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Bridge ownership snapshot + ownership logging. Under the machine-global
|
|
2
|
-
//
|
|
2
|
+
// unified daemon (singleton-owner lock in src/standalone) has exactly one
|
|
3
3
|
// runtime per machine, so this process is the unconditional bridge owner — the
|
|
4
4
|
// OS seat lock and its file heartbeat / last-wins CAS are retired.
|
|
5
5
|
// active-instance.json is now a pure metadata advert; it is no longer read to
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { recordFetchedMessages } from "./status-snapshot.mjs";
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
// (behavior-preserving). Bound to live getters so runtime config/
|
|
3
|
+
// Provider-tool dispatch helpers. Extracted verbatim from channels/index.mjs
|
|
4
|
+
// (behavior-preserving). Bound to live getters so runtime config/provider
|
|
5
5
|
// reloads keep the original file-level reference semantics.
|
|
6
|
-
function
|
|
6
|
+
function createProviderDispatch({ getConfig, getProvider, scheduler }) {
|
|
7
7
|
async function dispatchReply(args) {
|
|
8
8
|
const config = getConfig();
|
|
9
9
|
const channelId = String(args?.chat_id || args?.channel_id || args?.channel || config.channelId || '').trim();
|
|
@@ -29,7 +29,7 @@ function createBackendDispatch({ getConfig, getBackend, scheduler }) {
|
|
|
29
29
|
let ids;
|
|
30
30
|
// Pre-send activity bump keeps idle gating consistent during the await.
|
|
31
31
|
scheduler.noteActivity();
|
|
32
|
-
const sendResult = await
|
|
32
|
+
const sendResult = await getProvider().sendMessage(channelId, message, sendOpts);
|
|
33
33
|
scheduler.noteActivity();
|
|
34
34
|
ids = sendResult.sentIds;
|
|
35
35
|
const text = ids.length === 1 ? `sent (id: ${ids[0]})` : `sent ${ids.length} parts (ids: ${ids.join(", ")})`;
|
|
@@ -43,7 +43,7 @@ function createBackendDispatch({ getConfig, getBackend, scheduler }) {
|
|
|
43
43
|
const channelId = args.channel || config.channelId || "";
|
|
44
44
|
const limit = args.limit ?? 20;
|
|
45
45
|
let msgs;
|
|
46
|
-
msgs = await
|
|
46
|
+
msgs = await getProvider().fetchMessages(channelId, limit);
|
|
47
47
|
recordFetchedMessages(channelId, channelId, msgs);
|
|
48
48
|
const text = msgs.length === 0 ? "(no messages)" : msgs.map((m) => {
|
|
49
49
|
const atts = m.attachmentCount > 0 ? ` +${m.attachmentCount}att` : "";
|
|
@@ -58,4 +58,4 @@ function createBackendDispatch({ getConfig, getBackend, scheduler }) {
|
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
export {
|
|
61
|
+
export { createProviderDispatch };
|
|
@@ -189,9 +189,8 @@ function buildActiveInstanceState(instanceId, meta) {
|
|
|
189
189
|
...meta?.channelId ? { channelId: meta.channelId } : {},
|
|
190
190
|
...meta?.transcriptPath ? { transcriptPath: meta.transcriptPath } : {},
|
|
191
191
|
...meta?.httpPort ? { httpPort: meta.httpPort } : {},
|
|
192
|
-
...meta?.memory_port ? { memory_port: meta.memory_port } : {},
|
|
193
192
|
...gatewayMeta,
|
|
194
|
-
...typeof meta?.
|
|
193
|
+
...typeof meta?.providerReady === "boolean" ? { providerReady: meta.providerReady } : {}
|
|
195
194
|
};
|
|
196
195
|
}
|
|
197
196
|
function refreshActiveInstance(instanceId, meta, options) {
|
|
@@ -208,7 +207,7 @@ function refreshActiveInstance(instanceId, meta, options) {
|
|
|
208
207
|
// check ownership OUTSIDE this lock, so a newer session can claim the
|
|
209
208
|
// seat between that check and this locked update. Without this re-read,
|
|
210
209
|
// the tick would overwrite the newer owner's instanceId (TOCTOU
|
|
211
|
-
// re-steal -> ownership ping-pong / double
|
|
210
|
+
// re-steal -> ownership ping-pong / double provider connections).
|
|
212
211
|
// Returning undefined aborts the update with no write. Explicit claims
|
|
213
212
|
// (boot, claimBridgeOwnership) omit the option and stay last-wins.
|
|
214
213
|
if (options?.onlyIfOwned && (!prev?.instanceId || prev.instanceId !== instanceId)) {
|
|
@@ -249,9 +248,8 @@ function refreshActiveInstance(instanceId, meta, options) {
|
|
|
249
248
|
...meta?.channelId ? { channelId: meta.channelId } : {},
|
|
250
249
|
...meta?.transcriptPath ? { transcriptPath: meta.transcriptPath } : {},
|
|
251
250
|
...meta?.httpPort ? { httpPort: meta.httpPort } : {},
|
|
252
|
-
...meta?.memory_port ? { memory_port: meta.memory_port } : {},
|
|
253
251
|
...gatewayMeta,
|
|
254
|
-
...typeof meta?.
|
|
252
|
+
...typeof meta?.providerReady === "boolean" ? { providerReady: meta.providerReady } : {},
|
|
255
253
|
};
|
|
256
254
|
if (typeof meta?.transcriptPath === "string" && meta.transcriptPath) {
|
|
257
255
|
const outgoing = prevForPreserve?.transcriptPath;
|
|
@@ -267,34 +265,10 @@ function refreshActiveInstance(instanceId, meta, options) {
|
|
|
267
265
|
delete next.pinned;
|
|
268
266
|
// I1: pg_* spreads FIRST so newFields above win on conflict.
|
|
269
267
|
// prev.pg_port='A', meta.httpPort-adjacent pg_port='B' → result.pg_port='B'.
|
|
270
|
-
// memory_port: preserve when the advertising memory worker is still alive
|
|
271
|
-
// (sync process.kill(pid,0); ESRCH=dead). Same server_pid restart still
|
|
272
|
-
// preserves; a live owner from another session must not be dropped on handoff.
|
|
273
268
|
const preservedExtra = Object.fromEntries(
|
|
274
269
|
Object.entries(prevForPreserve ?? {}).filter(([k]) => k.startsWith('pg_'))
|
|
275
270
|
);
|
|
276
|
-
|
|
277
|
-
const prevMemoryOwnerAlive = (() => {
|
|
278
|
-
if (prevMemoryServerPid === null) return false;
|
|
279
|
-
try {
|
|
280
|
-
process.kill(prevMemoryServerPid, 0);
|
|
281
|
-
return true;
|
|
282
|
-
} catch (e) {
|
|
283
|
-
if (e && e.code === "ESRCH") return false;
|
|
284
|
-
return true;
|
|
285
|
-
}
|
|
286
|
-
})();
|
|
287
|
-
const sameMemoryAdvertiser =
|
|
288
|
-
prevMemoryServerPid !== null &&
|
|
289
|
-
identity.server_pid !== null &&
|
|
290
|
-
prevMemoryServerPid === identity.server_pid;
|
|
291
|
-
if (sameMemoryAdvertiser || prevMemoryOwnerAlive) {
|
|
292
|
-
if (prevForPreserve && Object.prototype.hasOwnProperty.call(prevForPreserve, 'memory_port')) {
|
|
293
|
-
preservedExtra.memory_port = prevForPreserve.memory_port;
|
|
294
|
-
preservedExtra.memory_server_pid = prevMemoryServerPid;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
// gateway_port follows the same preservation rule as memory_port: the
|
|
271
|
+
// gateway_port is preserved while the gateway owner is alive: the
|
|
298
272
|
// gateway child advertises itself independently, and active-owner
|
|
299
273
|
// heartbeats must not erase that discovery record while its owning
|
|
300
274
|
// server-main process is still alive.
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Usage (from channels/index.mjs):
|
|
11
11
|
* import { startSnapshotWriter } from './lib/status-snapshot.mjs';
|
|
12
|
-
* startSnapshotWriter(scheduler,
|
|
12
|
+
* startSnapshotWriter(scheduler, provider, webhookServer);
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import * as fs from 'fs';
|
|
@@ -38,8 +38,8 @@ const _discordUnread = new Map();
|
|
|
38
38
|
const DISCORD_UNREAD_MAX_CHANNELS = 500;
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
|
-
* Called whenever the
|
|
42
|
-
* `messages` is the array returned by
|
|
41
|
+
* Called whenever the provider observes messages for a channelId.
|
|
42
|
+
* `messages` is the array returned by provider.fetchMessages().
|
|
43
43
|
* We record the most-recently-seen message id and count messages
|
|
44
44
|
* received since the last call as "unread since last fetch".
|
|
45
45
|
* Pass { markRead: true } for live gateway messages that were already
|
|
@@ -10,7 +10,7 @@ import { OutputForwarder } from "./output-forwarder.mjs";
|
|
|
10
10
|
// call time — matching the original in-file closure semantics.
|
|
11
11
|
function createToolDispatch({
|
|
12
12
|
getForwarder,
|
|
13
|
-
|
|
13
|
+
PROVIDER_TOOLS,
|
|
14
14
|
isChannelsDegraded,
|
|
15
15
|
dispatchReply,
|
|
16
16
|
dispatchFetch,
|
|
@@ -152,11 +152,11 @@ function createToolDispatch({
|
|
|
152
152
|
_lastForwardMs = now;
|
|
153
153
|
await forwarder.forwardNewText();
|
|
154
154
|
}
|
|
155
|
-
if (
|
|
156
|
-
// Remote-owner startup: ensure this owner's
|
|
155
|
+
if (PROVIDER_TOOLS.has(toolName) && !getBridgeRuntimeConnected()) {
|
|
156
|
+
// Remote-owner startup: ensure this owner's provider is connected.
|
|
157
157
|
for (let i = 0; i < 2 && !getBridgeRuntimeConnected(); i++) {
|
|
158
158
|
try {
|
|
159
|
-
// Auto-connect this owner's
|
|
159
|
+
// Auto-connect this owner's provider (daemon singleton — no seat claim).
|
|
160
160
|
await refreshBridgeOwnership();
|
|
161
161
|
} catch {
|
|
162
162
|
}
|
|
@@ -78,7 +78,7 @@ if (!isWorkerMode) {
|
|
|
78
78
|
// Publish owner identity immediately so the SessionStart shim's
|
|
79
79
|
// owner_lead_alive() sees a live owner and uses the full connect budget
|
|
80
80
|
// instead of the 5s no-owner grace (fixes missing recap/core on restart).
|
|
81
|
-
//
|
|
81
|
+
// providerReady intentionally omitted — readiness stays gated until connect.
|
|
82
82
|
try {
|
|
83
83
|
refreshActiveInstance(instanceId);
|
|
84
84
|
} catch (e) {
|
|
@@ -12,7 +12,7 @@ export function runWorkerIpc({
|
|
|
12
12
|
clearServerPid,
|
|
13
13
|
instanceId,
|
|
14
14
|
statusState,
|
|
15
|
-
|
|
15
|
+
getProvider,
|
|
16
16
|
getConfig,
|
|
17
17
|
pendingPermRequests,
|
|
18
18
|
refreshToolExecConsumerMarker,
|
|
@@ -72,14 +72,14 @@ export function runWorkerIpc({
|
|
|
72
72
|
// Silent-to-agent lifecycle forward — parent (server.mjs) asks the
|
|
73
73
|
// channels worker to post status pings to the active bridge Discord
|
|
74
74
|
// channel without the Lead-notify hop. Best-effort: unknown channel or
|
|
75
|
-
//
|
|
75
|
+
// getProvider() failure is swallowed; lifecycle pings are non-critical.
|
|
76
76
|
if (msg && msg.type === 'forward_to_discord') {
|
|
77
77
|
try {
|
|
78
78
|
const target = msg.channelId
|
|
79
79
|
|| (statusState?.read?.().channelId)
|
|
80
80
|
|| null;
|
|
81
|
-
if (target &&
|
|
82
|
-
await
|
|
81
|
+
if (target && getProvider()?.sendMessage && typeof msg.content === 'string' && msg.content) {
|
|
82
|
+
await getProvider().sendMessage(target, msg.content).catch(() => {});
|
|
83
83
|
}
|
|
84
84
|
} catch { /* best-effort */ }
|
|
85
85
|
return;
|
|
@@ -87,7 +87,7 @@ export function runWorkerIpc({
|
|
|
87
87
|
// Host permission request → Discord Allow/Deny prompt.
|
|
88
88
|
// Parent (server.mjs) receives notifications/claude/channel/permission_request
|
|
89
89
|
// from the MCP host and forwards the params here. We post a buttoned message;
|
|
90
|
-
// button clicks are handled in
|
|
90
|
+
// button clicks are handled in getProvider().onInteraction and sent back to CC as
|
|
91
91
|
// notifications/claude/channel/permission via sendNotifyToParent.
|
|
92
92
|
if (msg && msg.type === 'permission_request_inbound') {
|
|
93
93
|
try {
|
|
@@ -110,7 +110,7 @@ export function runWorkerIpc({
|
|
|
110
110
|
const target = (statusState?.read?.().channelId)
|
|
111
111
|
|| getConfig()?.channelId
|
|
112
112
|
|| null;
|
|
113
|
-
if (!target || !
|
|
113
|
+
if (!target || !getProvider()?.sendMessage) {
|
|
114
114
|
process.stderr.write(`mixdog channels: permission_request dropped, no target channel (request_id=${request_id})\n`);
|
|
115
115
|
return;
|
|
116
116
|
}
|
|
@@ -128,7 +128,7 @@ export function runWorkerIpc({
|
|
|
128
128
|
}];
|
|
129
129
|
let sentIds = null;
|
|
130
130
|
try {
|
|
131
|
-
const sendResult = await
|
|
131
|
+
const sendResult = await getProvider().sendMessage(target, content, { components });
|
|
132
132
|
sentIds = sendResult?.sentIds;
|
|
133
133
|
} catch (err) {
|
|
134
134
|
process.stderr.write(`mixdog channels: permission_request Discord send failed: ${err && err.message || err}\n`);
|