mixdog 0.9.67 → 0.9.69
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 +6 -4
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +1 -6
- package/src/runtime/agent/orchestrator/context/collect.mjs +42 -27
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +0 -6
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +161 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +8 -204
- package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +25 -13
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +0 -11
- package/src/runtime/agent/orchestrator/session/store/listing.mjs +613 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +10 -2
- package/src/runtime/agent/orchestrator/session/store.mjs +9 -575
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +41 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-output.mjs +154 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +78 -18
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +9 -144
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +7 -3
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +40 -3
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +24 -8
- package/src/runtime/channels/lib/config.mjs +6 -5
- package/src/runtime/channels/lib/owned-runtime.mjs +65 -5
- package/src/runtime/channels/lib/scheduler.mjs +7 -15
- package/src/runtime/channels/lib/tool-dispatch.mjs +6 -1
- package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +6 -2
- package/src/runtime/channels/lib/webhook.mjs +36 -46
- package/src/runtime/channels/lib/worker-main.mjs +45 -92
- package/src/runtime/shared/automation-attachments.mjs +72 -0
- package/src/runtime/shared/automation-workflow.mjs +41 -0
- package/src/runtime/shared/schedule-session-run.mjs +10 -1
- package/src/runtime/shared/schedules-db.mjs +18 -3
- package/src/runtime/shared/webhook-session-run.mjs +57 -0
- package/src/runtime/shared/webhooks-db.mjs +19 -3
- package/src/session-runtime/channel-config-api.mjs +8 -1
- package/src/session-runtime/lifecycle-api.mjs +7 -0
- package/src/session-runtime/memory-daemon-probe.mjs +61 -0
- package/src/session-runtime/model-capabilities.mjs +6 -4
- package/src/session-runtime/notification-bus.mjs +82 -0
- package/src/session-runtime/provider-auth-api.mjs +16 -1
- package/src/session-runtime/provider-usage.mjs +3 -0
- package/src/session-runtime/remote-control.mjs +166 -0
- package/src/session-runtime/remote-transcript.mjs +126 -0
- package/src/session-runtime/remote-transition-queue.mjs +14 -0
- package/src/session-runtime/runtime-core.mjs +184 -660
- package/src/session-runtime/runtime-tunables.mjs +57 -0
- package/src/session-runtime/self-update.mjs +129 -0
- package/src/session-runtime/skills-api.mjs +77 -0
- package/src/session-runtime/tool-surface.mjs +83 -0
- package/src/session-runtime/workflow-agents-api.mjs +206 -3
- package/src/session-runtime/workflow.mjs +84 -17
- package/src/standalone/agent-tool/worker-index.mjs +287 -0
- package/src/standalone/agent-tool.mjs +22 -346
- package/src/standalone/agent-watchdog-registry.mjs +101 -0
- package/src/standalone/channel-admin.mjs +36 -1
- package/src/standalone/channel-daemon-client.mjs +5 -1
- package/src/standalone/channel-daemon-transport.mjs +112 -20
- package/src/standalone/channel-daemon.mjs +6 -4
- package/src/standalone/channel-worker.mjs +7 -13
- package/src/tui/App.jsx +2 -32
- package/src/tui/app/channel-pickers.mjs +2 -143
- package/src/tui/app/slash-commands.mjs +1 -3
- package/src/tui/app/slash-dispatch.mjs +3 -3
- package/src/tui/components/StatusLine.jsx +6 -5
- package/src/tui/dist/index.mjs +162 -259
- package/src/tui/engine/labels.mjs +0 -8
- package/src/tui/engine/session-api-ext.mjs +45 -10
- package/src/tui/engine/turn-watchdog.mjs +92 -0
- package/src/tui/engine/turn.mjs +18 -70
- package/src/tui/engine.mjs +14 -1
- package/src/workflows/default/WORKFLOW.md +1 -1
- package/src/workflows/solo/WORKFLOW.md +1 -1
package/src/tui/dist/index.mjs
CHANGED
|
@@ -6818,10 +6818,10 @@ ${grafted}` : cachedL1;
|
|
|
6818
6818
|
}, [sessionId, clientHostPid, provider, model, effort, fast, cwd, statsSignature, contextWindow, displayContextWindow, compactBoundaryTokens, autoCompactTokenLimit, rawContextWindow, resizeEpoch, agentRevision, agentWorkersSignature, agentJobsSignature, activeToolsSignature, refreshTick, themeEpoch]);
|
|
6819
6819
|
const lines = line ? line.split("\n").slice(0, 2) : [" ", " "];
|
|
6820
6820
|
const workflowLabel = workflowModeLabel(workflow, remoteEnabled);
|
|
6821
|
-
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: "100%", height:
|
|
6821
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: "100%", height: 2, overflow: "hidden", justifyContent: "flex-start", paddingLeft: 2, paddingRight: 2, backgroundColor: surfaceBackground(), children: [
|
|
6822
6822
|
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "row", width: "100%", overflow: "hidden", children: [
|
|
6823
6823
|
/* @__PURE__ */ jsx2(Box2, { flexGrow: 1, flexShrink: 1, flexBasis: 0, overflow: "hidden", children: /* @__PURE__ */ jsx2(Text2, { wrap: "truncate", children: lines[0] || " " }) }),
|
|
6824
|
-
/* @__PURE__ */ jsx2(Box2, { flexShrink: 0, marginLeft: 1,
|
|
6824
|
+
/* @__PURE__ */ jsx2(Box2, { flexShrink: 0, marginLeft: 1, children: /* @__PURE__ */ jsx2(Text2, { color: theme.statusText, wrap: "truncate", children: workflowLabel }) })
|
|
6825
6825
|
] }),
|
|
6826
6826
|
/* @__PURE__ */ jsx2(Box2, { flexDirection: "row", width: "100%", overflow: "hidden", children: /* @__PURE__ */ jsx2(Box2, { flexGrow: 1, flexShrink: 1, flexBasis: 0, overflow: "hidden", children: /* @__PURE__ */ jsx2(Text2, { wrap: "truncate", children: lines[1] || " " }) }) })
|
|
6827
6827
|
] });
|
|
@@ -10093,10 +10093,8 @@ var SLASH_COMMANDS = [
|
|
|
10093
10093
|
{ name: "plugins", usage: "/plugins", description: "Manage local plugin integrations" },
|
|
10094
10094
|
{ name: "hooks", usage: "/hooks", description: "Manage before-tool hook rules and events" },
|
|
10095
10095
|
{ name: "providers", usage: "/providers", description: "Manage auth, API keys, OAuth, and local endpoints" },
|
|
10096
|
-
{ name: "channels", usage: "/channels", description: "Manage Discord,
|
|
10096
|
+
{ name: "channels", usage: "/channels", description: "Manage Discord, Telegram, and voice" },
|
|
10097
10097
|
{ name: "remote", usage: "/remote", description: "Claim remote for this session (takes over from any other session)" },
|
|
10098
|
-
{ name: "schedules", usage: "/schedules", description: "Manage schedules" },
|
|
10099
|
-
{ name: "webhooks", usage: "/webhooks", description: "Manage inbound webhooks" },
|
|
10100
10098
|
{ name: "settings", usage: "/setting", aliases: ["setting", "config"], aliasUsage: ["settings", "config"], showAliasUsage: false, description: "Open runtime settings" },
|
|
10101
10099
|
{ name: "profile", usage: "/profile", description: "Set your title and response language" },
|
|
10102
10100
|
{ name: "update", usage: "/update", description: "Check version and update mixdog" },
|
|
@@ -17344,139 +17342,6 @@ function createChannelPickers({
|
|
|
17344
17342
|
setContextPanel(null);
|
|
17345
17343
|
setChannelPrompt(prompt);
|
|
17346
17344
|
};
|
|
17347
|
-
const channelRemoteEnabled = store.isRemoteEnabled?.() === true;
|
|
17348
|
-
if (focus === "schedules") {
|
|
17349
|
-
const schedules = setup.schedules || [];
|
|
17350
|
-
const items2 = [
|
|
17351
|
-
...schedules.length ? schedules.map((schedule) => {
|
|
17352
|
-
const enabled = schedule.enabled !== false;
|
|
17353
|
-
return {
|
|
17354
|
-
value: `schedule:${schedule.name}`,
|
|
17355
|
-
label: schedule.name,
|
|
17356
|
-
marker: enabled ? "\u25CF" : "\u25CB",
|
|
17357
|
-
markerColor: channelRemoteEnabled ? enabled ? theme.success : theme.inactive : theme.inactive,
|
|
17358
|
-
description: `${schedule.time || "(no cron)"} \xB7 ${schedule.route}${schedule.model ? ` \xB7 ${schedule.model}` : ""}${channelRemoteEnabled ? "" : " \xB7 channel off"}`,
|
|
17359
|
-
_action: "schedule-toggle",
|
|
17360
|
-
_name: schedule.name,
|
|
17361
|
-
_enabled: enabled
|
|
17362
|
-
};
|
|
17363
|
-
}) : [{
|
|
17364
|
-
value: "empty",
|
|
17365
|
-
label: "No schedules",
|
|
17366
|
-
description: "no schedules configured",
|
|
17367
|
-
_action: "noop"
|
|
17368
|
-
}]
|
|
17369
|
-
];
|
|
17370
|
-
const toggleSchedule = async (item) => {
|
|
17371
|
-
if (item._action !== "schedule-toggle") return;
|
|
17372
|
-
if (!channelRemoteEnabled) {
|
|
17373
|
-
store.pushNotice("enable channel first", "warn");
|
|
17374
|
-
return;
|
|
17375
|
-
}
|
|
17376
|
-
try {
|
|
17377
|
-
await store.setScheduleEnabled?.(item._name, !item._enabled);
|
|
17378
|
-
void openChannelSetupPicker("schedules", { highlightValue: `schedule:${item._name}` });
|
|
17379
|
-
} catch (e) {
|
|
17380
|
-
store.pushNotice(`schedule toggle failed: ${e?.message || e}`, "error");
|
|
17381
|
-
}
|
|
17382
|
-
};
|
|
17383
|
-
setPicker({
|
|
17384
|
-
title: "Schedules",
|
|
17385
|
-
description: channelRemoteEnabled ? "Enable or disable cron schedules." : "Enable channel to toggle schedules.",
|
|
17386
|
-
initialIndex: Math.max(0, items2.findIndex((entry) => entry.value === options.highlightValue)),
|
|
17387
|
-
items: items2,
|
|
17388
|
-
onSelect: (_value, item) => toggleSchedule(item),
|
|
17389
|
-
onLeft: (item) => toggleSchedule(item),
|
|
17390
|
-
onRight: (item) => toggleSchedule(item),
|
|
17391
|
-
onCancel: () => {
|
|
17392
|
-
setPicker(null);
|
|
17393
|
-
}
|
|
17394
|
-
});
|
|
17395
|
-
return;
|
|
17396
|
-
}
|
|
17397
|
-
if (focus === "webhook-endpoint") {
|
|
17398
|
-
const returnTo = typeof options.returnTo === "function" ? options.returnTo : () => setPicker(null);
|
|
17399
|
-
const publicUrl = setup.webhook?.publicUrl || "";
|
|
17400
|
-
const items2 = [
|
|
17401
|
-
{
|
|
17402
|
-
value: "endpoint-url",
|
|
17403
|
-
label: "Public URL",
|
|
17404
|
-
description: publicUrl ? `${publicUrl}/webhook/<name>` : "Assigned when the channel worker connects to the relay",
|
|
17405
|
-
_action: "endpoint-url"
|
|
17406
|
-
}
|
|
17407
|
-
];
|
|
17408
|
-
setPicker({
|
|
17409
|
-
title: "Webhook endpoint",
|
|
17410
|
-
description: "Served through the Mixdog relay \u2014 no tunnel setup needed. Toggle individual webhooks in /webhooks.",
|
|
17411
|
-
help: "\u2191/\u2193 Select \xB7 Enter Edit \xB7 Esc Back",
|
|
17412
|
-
indexMode: "always",
|
|
17413
|
-
labelWidth: 18,
|
|
17414
|
-
items: items2,
|
|
17415
|
-
onSelect: (_value, item) => {
|
|
17416
|
-
try {
|
|
17417
|
-
if (item._action === "endpoint-url") {
|
|
17418
|
-
store.pushNotice(publicUrl ? `Webhook base: ${publicUrl}/webhook/<name>` : "URL not assigned yet \u2014 start channels once and reopen.", "info");
|
|
17419
|
-
}
|
|
17420
|
-
} catch (e) {
|
|
17421
|
-
store.pushNotice(`webhook endpoint failed: ${e?.message || e}`, "error");
|
|
17422
|
-
}
|
|
17423
|
-
},
|
|
17424
|
-
onCancel: () => {
|
|
17425
|
-
setPicker(null);
|
|
17426
|
-
returnTo();
|
|
17427
|
-
}
|
|
17428
|
-
});
|
|
17429
|
-
return;
|
|
17430
|
-
}
|
|
17431
|
-
if (focus === "webhooks") {
|
|
17432
|
-
const hooks = setup.webhooks || [];
|
|
17433
|
-
const items2 = [
|
|
17434
|
-
...hooks.length ? hooks.map((hook) => {
|
|
17435
|
-
const enabled = hook.enabled !== false;
|
|
17436
|
-
return {
|
|
17437
|
-
value: `webhook:${hook.name}`,
|
|
17438
|
-
label: hook.name,
|
|
17439
|
-
marker: enabled ? "\u25CF" : "\u25CB",
|
|
17440
|
-
markerColor: channelRemoteEnabled ? enabled ? theme.success : theme.inactive : theme.inactive,
|
|
17441
|
-
description: `${hook.parser || "github"} \xB7 ${hook.route} \xB7 secret:${hook.secretSet ? "set" : "missing"}${channelRemoteEnabled ? "" : " \xB7 channel off"}`,
|
|
17442
|
-
_action: "webhook-toggle",
|
|
17443
|
-
_name: hook.name,
|
|
17444
|
-
_enabled: enabled
|
|
17445
|
-
};
|
|
17446
|
-
}) : [{
|
|
17447
|
-
value: "empty",
|
|
17448
|
-
label: "No webhooks",
|
|
17449
|
-
description: "no webhook endpoints configured",
|
|
17450
|
-
_action: "noop"
|
|
17451
|
-
}]
|
|
17452
|
-
];
|
|
17453
|
-
const toggleWebhook = async (item) => {
|
|
17454
|
-
if (item._action !== "webhook-toggle") return;
|
|
17455
|
-
if (!channelRemoteEnabled) {
|
|
17456
|
-
store.pushNotice("enable channel first", "warn");
|
|
17457
|
-
return;
|
|
17458
|
-
}
|
|
17459
|
-
try {
|
|
17460
|
-
await store.setWebhookEnabled?.(item._name, !item._enabled);
|
|
17461
|
-
void openChannelSetupPicker("webhooks", { highlightValue: `webhook:${item._name}` });
|
|
17462
|
-
} catch (e) {
|
|
17463
|
-
store.pushNotice(`webhook toggle failed: ${e?.message || e}`, "error");
|
|
17464
|
-
}
|
|
17465
|
-
};
|
|
17466
|
-
setPicker({
|
|
17467
|
-
title: "Webhooks",
|
|
17468
|
-
description: channelRemoteEnabled ? "Enable or disable inbound webhook endpoints." : "Enable channel to toggle webhooks.",
|
|
17469
|
-
initialIndex: Math.max(0, items2.findIndex((entry) => entry.value === options.highlightValue)),
|
|
17470
|
-
items: items2,
|
|
17471
|
-
onSelect: (_value, item) => toggleWebhook(item),
|
|
17472
|
-
onLeft: (item) => toggleWebhook(item),
|
|
17473
|
-
onRight: (item) => toggleWebhook(item),
|
|
17474
|
-
onCancel: () => {
|
|
17475
|
-
setPicker(null);
|
|
17476
|
-
}
|
|
17477
|
-
});
|
|
17478
|
-
return;
|
|
17479
|
-
}
|
|
17480
17345
|
const worker = store.getChannelWorkerStatus?.();
|
|
17481
17346
|
const activeBackend = setup.backend === "telegram" ? "telegram" : "discord";
|
|
17482
17347
|
const backendLabel = activeBackend === "telegram" ? "Telegram" : "Discord";
|
|
@@ -19574,10 +19439,8 @@ function createSlashDispatch({
|
|
|
19574
19439
|
void openChannelSetupPicker("all");
|
|
19575
19440
|
return true;
|
|
19576
19441
|
case "schedules":
|
|
19577
|
-
void openChannelSetupPicker("schedules");
|
|
19578
|
-
return true;
|
|
19579
19442
|
case "webhooks":
|
|
19580
|
-
|
|
19443
|
+
store.pushNotice("Schedules and webhooks are managed in the Mixdog desktop app", "info");
|
|
19581
19444
|
return true;
|
|
19582
19445
|
case "auth":
|
|
19583
19446
|
store.pushNotice("/auth moved to /providers", "info");
|
|
@@ -22830,29 +22693,6 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
|
|
|
22830
22693
|
});
|
|
22831
22694
|
return true;
|
|
22832
22695
|
}
|
|
22833
|
-
if (channelPrompt.kind === "schedule-add") {
|
|
22834
|
-
const [name, time, instructions, channel, model] = parts;
|
|
22835
|
-
Promise.resolve(store.saveSchedule({ name, time, instructions, channel, model })).then(() => {
|
|
22836
|
-
setChannelPrompt(null);
|
|
22837
|
-
void openChannelSetupPicker("schedules");
|
|
22838
|
-
}).catch((e) => {
|
|
22839
|
-
store.pushNotice(`schedule save failed: ${e?.message || e}`, "error");
|
|
22840
|
-
});
|
|
22841
|
-
return true;
|
|
22842
|
-
}
|
|
22843
|
-
if (channelPrompt.kind === "webhook-add") {
|
|
22844
|
-
const [name, instructions, channel, model, parser] = parts;
|
|
22845
|
-
Promise.resolve(store.saveWebhook({ name, instructions, channel, model, parser })).then((result) => {
|
|
22846
|
-
if (result?.secret) {
|
|
22847
|
-
store.pushNotice(`webhook secret for ${result.name}: ${result.secret}`, "info");
|
|
22848
|
-
}
|
|
22849
|
-
setChannelPrompt(null);
|
|
22850
|
-
void openChannelSetupPicker("webhooks");
|
|
22851
|
-
}).catch((e) => {
|
|
22852
|
-
store.pushNotice(`webhook save failed: ${e?.message || e}`, "error");
|
|
22853
|
-
});
|
|
22854
|
-
return true;
|
|
22855
|
-
}
|
|
22856
22696
|
} catch (e) {
|
|
22857
22697
|
store.pushNotice(`channels update failed: ${e?.message || e}`, "error");
|
|
22858
22698
|
return false;
|
|
@@ -24331,11 +24171,6 @@ function compactEventDetail(event = {}) {
|
|
|
24331
24171
|
const parts = [];
|
|
24332
24172
|
const elapsed = formatElapsedSeconds(Number(event.durationMs ?? event.elapsedMs ?? 0));
|
|
24333
24173
|
if (elapsed) parts.push(elapsed);
|
|
24334
|
-
const type = String(event.compactType || event.type || "").trim();
|
|
24335
|
-
if (type && type !== "semantic") parts.push(type);
|
|
24336
|
-
const trigger = String(event.trigger || "").toLowerCase();
|
|
24337
|
-
if (trigger === "reactive") parts.push("reactive");
|
|
24338
|
-
else if (trigger === "manual") parts.push("manual");
|
|
24339
24174
|
const before = Number(event.pressureTokens ?? event.beforeTokens ?? 0);
|
|
24340
24175
|
const after = Number(event.afterTokens ?? 0);
|
|
24341
24176
|
const fmtTok = (n) => {
|
|
@@ -26818,6 +26653,93 @@ function createSessionFlow(bag) {
|
|
|
26818
26653
|
return { leadSessionId, shouldMirrorSteeringEntry, commitSteeringQueueEntries, makeQueueEntry, removeQueuedEntries, requeueEntriesFront, dequeueQueueBatch, drain, enqueue, drainPendingSteering, restoreLeadSteeringFromDisk, autoClearBeforeSubmit, performSessionClear, restoreQueued, resetStats, clearUiActivityBeforeContextSync, resetTuiForPendingSessionReset, snapshotTuiBeforeSessionReset, restoreTuiAfterFailedSessionReset, commitTuiSessionReset, resetStatsAndSyncContext };
|
|
26819
26654
|
}
|
|
26820
26655
|
|
|
26656
|
+
// src/tui/engine/turn-watchdog.mjs
|
|
26657
|
+
function createTurnWatchdog({
|
|
26658
|
+
runtime,
|
|
26659
|
+
timeoutMs,
|
|
26660
|
+
toolMaxMs,
|
|
26661
|
+
isCurrentTurn,
|
|
26662
|
+
onTrip
|
|
26663
|
+
}) {
|
|
26664
|
+
let timer2 = null;
|
|
26665
|
+
let tripped = false;
|
|
26666
|
+
let lastProgressAt = Date.now();
|
|
26667
|
+
let lastProgressLabel = "start";
|
|
26668
|
+
let deferralCeilingAt = 0;
|
|
26669
|
+
function clear() {
|
|
26670
|
+
if (timer2) {
|
|
26671
|
+
clearTimeout(timer2);
|
|
26672
|
+
timer2 = null;
|
|
26673
|
+
}
|
|
26674
|
+
}
|
|
26675
|
+
function refreshFromRuntimeLiveness() {
|
|
26676
|
+
let liveness;
|
|
26677
|
+
try {
|
|
26678
|
+
liveness = runtime.getTurnLiveness?.();
|
|
26679
|
+
} catch {
|
|
26680
|
+
return false;
|
|
26681
|
+
}
|
|
26682
|
+
if (liveness?.stage !== "tool_running") deferralCeilingAt = 0;
|
|
26683
|
+
const progressAt = Number(liveness?.lastProgressAt);
|
|
26684
|
+
const now = Date.now();
|
|
26685
|
+
if (!liveness || !Number.isFinite(progressAt) || progressAt <= now - timeoutMs) return false;
|
|
26686
|
+
if (liveness.stage === "tool_running") {
|
|
26687
|
+
deferralCeilingAt = 0;
|
|
26688
|
+
const toolStartedAt = Number(liveness.toolStartedAt);
|
|
26689
|
+
if (!Number.isFinite(toolStartedAt) || toolStartedAt <= 0) return false;
|
|
26690
|
+
const toolSelfDeadlineMs = Number(liveness.toolSelfDeadlineMs);
|
|
26691
|
+
const toolCeilingMs = Math.max(
|
|
26692
|
+
Number.isFinite(toolSelfDeadlineMs) && toolSelfDeadlineMs > 0 ? toolSelfDeadlineMs + 6e4 : 0,
|
|
26693
|
+
toolMaxMs
|
|
26694
|
+
);
|
|
26695
|
+
if (now - toolStartedAt >= toolCeilingMs) return false;
|
|
26696
|
+
deferralCeilingAt = toolStartedAt + toolCeilingMs;
|
|
26697
|
+
}
|
|
26698
|
+
lastProgressAt = progressAt;
|
|
26699
|
+
lastProgressLabel = `orchestrator:${String(liveness.stage || "unknown")}`;
|
|
26700
|
+
arm();
|
|
26701
|
+
return true;
|
|
26702
|
+
}
|
|
26703
|
+
function arm() {
|
|
26704
|
+
clear();
|
|
26705
|
+
if (tripped) return;
|
|
26706
|
+
const now = Date.now();
|
|
26707
|
+
const remaining = Math.max(1, timeoutMs - Math.max(0, now - lastProgressAt));
|
|
26708
|
+
const ceilingRemaining = deferralCeilingAt > 0 ? Math.max(1, deferralCeilingAt - now) : Infinity;
|
|
26709
|
+
timer2 = setTimeout(() => {
|
|
26710
|
+
if (!isCurrentTurn() || tripped) return;
|
|
26711
|
+
const firedAt = Date.now();
|
|
26712
|
+
const idleMs = firedAt - lastProgressAt;
|
|
26713
|
+
if (idleMs < timeoutMs) {
|
|
26714
|
+
if (deferralCeilingAt > 0 && firedAt >= deferralCeilingAt) {
|
|
26715
|
+
if (refreshFromRuntimeLiveness()) return;
|
|
26716
|
+
} else {
|
|
26717
|
+
arm();
|
|
26718
|
+
return;
|
|
26719
|
+
}
|
|
26720
|
+
} else if (refreshFromRuntimeLiveness()) return;
|
|
26721
|
+
tripped = true;
|
|
26722
|
+
onTrip({ idleMs, lastProgressLabel });
|
|
26723
|
+
}, Math.min(remaining, ceilingRemaining));
|
|
26724
|
+
timer2.unref?.();
|
|
26725
|
+
}
|
|
26726
|
+
function markProgress(label) {
|
|
26727
|
+
if (!isCurrentTurn()) return false;
|
|
26728
|
+
if (tripped) return false;
|
|
26729
|
+
lastProgressAt = Date.now();
|
|
26730
|
+
lastProgressLabel = String(label || "progress");
|
|
26731
|
+
return true;
|
|
26732
|
+
}
|
|
26733
|
+
return {
|
|
26734
|
+
arm,
|
|
26735
|
+
clear,
|
|
26736
|
+
markProgress,
|
|
26737
|
+
get tripped() {
|
|
26738
|
+
return tripped;
|
|
26739
|
+
}
|
|
26740
|
+
};
|
|
26741
|
+
}
|
|
26742
|
+
|
|
26821
26743
|
// src/tui/engine/turn.mjs
|
|
26822
26744
|
function createRunTurn(bag) {
|
|
26823
26745
|
const {
|
|
@@ -26902,76 +26824,15 @@ function createRunTurn(bag) {
|
|
|
26902
26824
|
let assistantText = "";
|
|
26903
26825
|
let currentAssistantId = null;
|
|
26904
26826
|
tuiDebug2(`runTurn start turn=${turnIndex} pending=${pending.length} timeoutMs=${LEAD_TURN_TIMEOUT_MS2}`);
|
|
26905
|
-
let watchdogTripped = false;
|
|
26906
|
-
let watchdogTimer = null;
|
|
26907
26827
|
let watchdogGraceTimer = null;
|
|
26908
|
-
let lastProgressAt = startedAt;
|
|
26909
|
-
let lastProgressLabel = "start";
|
|
26910
|
-
let watchdogDeferralCeilingAt = 0;
|
|
26911
26828
|
const configuredLeadToolMaxMs = Number(process.env.MIXDOG_LEAD_TOOL_MAX_MS);
|
|
26912
26829
|
const leadToolMaxMs = Number.isFinite(configuredLeadToolMaxMs) && configuredLeadToolMaxMs > 0 ? configuredLeadToolMaxMs : 30 * 60 * 1e3;
|
|
26913
|
-
const
|
|
26914
|
-
|
|
26915
|
-
|
|
26916
|
-
|
|
26917
|
-
|
|
26918
|
-
|
|
26919
|
-
clearTimeout(watchdogGraceTimer);
|
|
26920
|
-
watchdogGraceTimer = null;
|
|
26921
|
-
}
|
|
26922
|
-
};
|
|
26923
|
-
const refreshWatchdogFromRuntimeLiveness = () => {
|
|
26924
|
-
let liveness;
|
|
26925
|
-
try {
|
|
26926
|
-
liveness = runtime.getTurnLiveness?.();
|
|
26927
|
-
} catch {
|
|
26928
|
-
return false;
|
|
26929
|
-
}
|
|
26930
|
-
if (liveness?.stage !== "tool_running") watchdogDeferralCeilingAt = 0;
|
|
26931
|
-
const progressAt = Number(liveness?.lastProgressAt);
|
|
26932
|
-
const now = Date.now();
|
|
26933
|
-
if (!liveness || !Number.isFinite(progressAt) || progressAt <= now - LEAD_TURN_TIMEOUT_MS2) return false;
|
|
26934
|
-
if (liveness.stage === "tool_running") {
|
|
26935
|
-
watchdogDeferralCeilingAt = 0;
|
|
26936
|
-
const toolStartedAt = Number(liveness.toolStartedAt);
|
|
26937
|
-
if (!Number.isFinite(toolStartedAt) || toolStartedAt <= 0) return false;
|
|
26938
|
-
const toolSelfDeadlineMs = Number(liveness.toolSelfDeadlineMs);
|
|
26939
|
-
const toolCeilingMs = Math.max(
|
|
26940
|
-
Number.isFinite(toolSelfDeadlineMs) && toolSelfDeadlineMs > 0 ? toolSelfDeadlineMs + 6e4 : 0,
|
|
26941
|
-
leadToolMaxMs
|
|
26942
|
-
);
|
|
26943
|
-
if (now - toolStartedAt >= toolCeilingMs) return false;
|
|
26944
|
-
watchdogDeferralCeilingAt = toolStartedAt + toolCeilingMs;
|
|
26945
|
-
}
|
|
26946
|
-
lastProgressAt = progressAt;
|
|
26947
|
-
lastProgressLabel = `orchestrator:${String(liveness.stage || "unknown")}`;
|
|
26948
|
-
armWatchdog();
|
|
26949
|
-
return true;
|
|
26950
|
-
};
|
|
26951
|
-
const armWatchdog = () => {
|
|
26952
|
-
if (watchdogTimer) {
|
|
26953
|
-
clearTimeout(watchdogTimer);
|
|
26954
|
-
watchdogTimer = null;
|
|
26955
|
-
}
|
|
26956
|
-
if (watchdogTripped) return;
|
|
26957
|
-
const now = Date.now();
|
|
26958
|
-
const remaining = Math.max(1, LEAD_TURN_TIMEOUT_MS2 - Math.max(0, now - lastProgressAt));
|
|
26959
|
-
const ceilingRemaining = watchdogDeferralCeilingAt > 0 ? Math.max(1, watchdogDeferralCeilingAt - now) : Infinity;
|
|
26960
|
-
const delay2 = Math.min(remaining, ceilingRemaining);
|
|
26961
|
-
watchdogTimer = setTimeout(() => {
|
|
26962
|
-
if (!isCurrentTurn()) return;
|
|
26963
|
-
if (watchdogTripped) return;
|
|
26964
|
-
const now2 = Date.now();
|
|
26965
|
-
const idleMs = now2 - lastProgressAt;
|
|
26966
|
-
if (idleMs < LEAD_TURN_TIMEOUT_MS2) {
|
|
26967
|
-
if (watchdogDeferralCeilingAt > 0 && now2 >= watchdogDeferralCeilingAt) {
|
|
26968
|
-
if (refreshWatchdogFromRuntimeLiveness()) return;
|
|
26969
|
-
} else {
|
|
26970
|
-
armWatchdog();
|
|
26971
|
-
return;
|
|
26972
|
-
}
|
|
26973
|
-
} else if (refreshWatchdogFromRuntimeLiveness()) return;
|
|
26974
|
-
watchdogTripped = true;
|
|
26830
|
+
const watchdog = createTurnWatchdog({
|
|
26831
|
+
runtime,
|
|
26832
|
+
timeoutMs: LEAD_TURN_TIMEOUT_MS2,
|
|
26833
|
+
toolMaxMs: leadToolMaxMs,
|
|
26834
|
+
isCurrentTurn,
|
|
26835
|
+
onTrip: ({ idleMs, lastProgressLabel }) => {
|
|
26975
26836
|
if (_batchTimer !== null) {
|
|
26976
26837
|
clearTimeout(_batchTimer);
|
|
26977
26838
|
_batchTimer = null;
|
|
@@ -27011,17 +26872,17 @@ function createRunTurn(bag) {
|
|
|
27011
26872
|
flushEmitImmediate?.();
|
|
27012
26873
|
}, 5e3);
|
|
27013
26874
|
watchdogGraceTimer.unref?.();
|
|
27014
|
-
}
|
|
27015
|
-
|
|
27016
|
-
|
|
27017
|
-
|
|
27018
|
-
if (
|
|
27019
|
-
|
|
27020
|
-
|
|
27021
|
-
|
|
27022
|
-
return true;
|
|
26875
|
+
}
|
|
26876
|
+
});
|
|
26877
|
+
const clearWatchdog = () => {
|
|
26878
|
+
watchdog.clear();
|
|
26879
|
+
if (watchdogGraceTimer) {
|
|
26880
|
+
clearTimeout(watchdogGraceTimer);
|
|
26881
|
+
watchdogGraceTimer = null;
|
|
26882
|
+
}
|
|
27023
26883
|
};
|
|
27024
|
-
|
|
26884
|
+
const markTurnProgress = (label) => watchdog.markProgress(label);
|
|
26885
|
+
watchdog.arm();
|
|
27025
26886
|
let currentAssistantText = "";
|
|
27026
26887
|
const committedSegments = [];
|
|
27027
26888
|
let thinkingText = "";
|
|
@@ -27767,7 +27628,7 @@ function createRunTurn(bag) {
|
|
|
27767
27628
|
flushStreamBatch();
|
|
27768
27629
|
},
|
|
27769
27630
|
onReasoningDelta: (chunk) => {
|
|
27770
|
-
if (!isCurrentTurn() ||
|
|
27631
|
+
if (!isCurrentTurn() || watchdog.tripped) return;
|
|
27771
27632
|
if (String(chunk ?? "")) {
|
|
27772
27633
|
if (!markTurnProgress("reasoning-delta")) return;
|
|
27773
27634
|
markPromptCommitted();
|
|
@@ -27907,7 +27768,7 @@ function createRunTurn(bag) {
|
|
|
27907
27768
|
flushEmit?.();
|
|
27908
27769
|
_publishedThinkingActive = false;
|
|
27909
27770
|
const finalStatus = cancelled ? "cancelled" : failed ? "failed" : "done";
|
|
27910
|
-
tuiDebug2(`runTurn end turn=${turnIndex} status=${finalStatus} elapsedMs=${Date.now() - startedAt}${
|
|
27771
|
+
tuiDebug2(`runTurn end turn=${turnIndex} status=${finalStatus} elapsedMs=${Date.now() - startedAt}${watchdog.tripped ? " watchdogTripped=1" : ""} pending=${pending.length}`);
|
|
27911
27772
|
return finalStatus;
|
|
27912
27773
|
}
|
|
27913
27774
|
return runTurn;
|
|
@@ -28245,6 +28106,15 @@ function createEngineApiB(bag) {
|
|
|
28245
28106
|
listWorkflows: () => {
|
|
28246
28107
|
return runtime.listWorkflows?.() || [];
|
|
28247
28108
|
},
|
|
28109
|
+
// Workflow pack editing (desktop Workflows page): direct passthroughs —
|
|
28110
|
+
// the runtime owns validation, user-pack persistence, and delete guards.
|
|
28111
|
+
getWorkflowPack: (workflowId) => runtime.getWorkflowPack?.(workflowId) ?? null,
|
|
28112
|
+
saveWorkflowPack: async (payload) => runtime.saveWorkflowPack?.(payload) ?? null,
|
|
28113
|
+
createWorkflow: async (payload) => runtime.createWorkflow?.(payload) ?? null,
|
|
28114
|
+
deleteWorkflow: async (workflowId) => runtime.deleteWorkflow?.(workflowId) ?? null,
|
|
28115
|
+
getAgentDefinition: (agentId) => runtime.getAgentDefinition?.(agentId) ?? null,
|
|
28116
|
+
saveAgentDefinition: async (payload) => runtime.saveAgentDefinition?.(payload) ?? null,
|
|
28117
|
+
deleteAgentDefinition: async (agentId) => runtime.deleteAgentDefinition?.(agentId) ?? null,
|
|
28248
28118
|
getOutputStyle: () => {
|
|
28249
28119
|
return runtime.getOutputStyle?.() || runtime.listOutputStyles?.() || null;
|
|
28250
28120
|
},
|
|
@@ -28278,27 +28148,49 @@ function createEngineApiB(bag) {
|
|
|
28278
28148
|
set({ commandBusy: false });
|
|
28279
28149
|
}
|
|
28280
28150
|
},
|
|
28281
|
-
// Toggle
|
|
28282
|
-
//
|
|
28283
|
-
//
|
|
28284
|
-
|
|
28151
|
+
// Toggle channel remote mode for the CURRENT session (session-scoped,
|
|
28152
|
+
// user decision). Off → on claims for this session; on + this session
|
|
28153
|
+
// owns → off; on + ANOTHER session owns → move the relay seat here
|
|
28154
|
+
// (last-wins) without restarting the worker.
|
|
28155
|
+
toggleRemote: async () => {
|
|
28285
28156
|
const enabled = runtime.isRemoteEnabled?.() === true;
|
|
28286
|
-
|
|
28287
|
-
|
|
28157
|
+
const owner = String(runtime.getRemoteSessionId?.() || "");
|
|
28158
|
+
const currentId = String(getState().sessionId || "");
|
|
28159
|
+
const workerRunning = runtime.getChannelWorkerStatus?.()?.running === true;
|
|
28160
|
+
if (enabled && !workerRunning) {
|
|
28161
|
+
await runtime.startRemote?.();
|
|
28162
|
+
const next2 = runtime.isRemoteEnabled?.() === true;
|
|
28163
|
+
set({ remoteEnabled: next2, remoteSessionId: runtime.getRemoteSessionId?.() || null });
|
|
28164
|
+
return next2;
|
|
28165
|
+
}
|
|
28166
|
+
if (enabled && owner && currentId && owner !== currentId) {
|
|
28167
|
+
runtime.claimRemoteForCurrentSession?.();
|
|
28168
|
+
set({ remoteEnabled: true, remoteSessionId: runtime.getRemoteSessionId?.() || null });
|
|
28169
|
+
return true;
|
|
28170
|
+
}
|
|
28171
|
+
if (enabled) await runtime.releaseRemote?.();
|
|
28172
|
+
else await runtime.startRemote?.();
|
|
28288
28173
|
const next = runtime.isRemoteEnabled?.() === true;
|
|
28289
|
-
set({ remoteEnabled: next });
|
|
28174
|
+
set({ remoteEnabled: next, remoteSessionId: runtime.getRemoteSessionId?.() || null });
|
|
28290
28175
|
return next;
|
|
28291
28176
|
},
|
|
28292
28177
|
// Force-claim remote for this session (single-holder, last-wins). Always
|
|
28293
28178
|
// turns remote ON here and steals the bridge seat; the previous holder is
|
|
28294
28179
|
// superseded and flips itself OFF via onRemoteStateChange. Used by the
|
|
28295
28180
|
// `/remote` slash command — repeated /remote just re-claims (idempotent).
|
|
28296
|
-
claimRemote: () => {
|
|
28297
|
-
runtime.startRemote?.();
|
|
28181
|
+
claimRemote: async () => {
|
|
28182
|
+
await runtime.startRemote?.();
|
|
28298
28183
|
const next = runtime.isRemoteEnabled?.() === true;
|
|
28299
|
-
set({ remoteEnabled: next });
|
|
28184
|
+
set({ remoteEnabled: next, remoteSessionId: runtime.getRemoteSessionId?.() || null });
|
|
28300
28185
|
return next;
|
|
28301
28186
|
},
|
|
28187
|
+
// Idempotent desired-state OFF for desktop controls. Unlike toggleRemote,
|
|
28188
|
+
// a delayed/replayed request can never turn remote back on.
|
|
28189
|
+
releaseRemote: async () => {
|
|
28190
|
+
await runtime.releaseRemote?.();
|
|
28191
|
+
set({ remoteEnabled: false, remoteSessionId: null });
|
|
28192
|
+
return false;
|
|
28193
|
+
},
|
|
28302
28194
|
isRemoteEnabled: () => runtime.isRemoteEnabled?.() === true,
|
|
28303
28195
|
getVoiceStatus: () => getVoiceStatus(),
|
|
28304
28196
|
// Desktop push-to-talk dictation: accept a recorded audio payload
|
|
@@ -31163,12 +31055,23 @@ async function createEngineSession({
|
|
|
31163
31055
|
if (typeof runtime.onRemoteStateChange === "function") {
|
|
31164
31056
|
lifecycle.unsubscribeRemoteState = runtime.onRemoteStateChange(({ enabled, reason }) => {
|
|
31165
31057
|
if (flags.disposed) return;
|
|
31166
|
-
set({
|
|
31058
|
+
set({
|
|
31059
|
+
remoteEnabled: enabled === true,
|
|
31060
|
+
remoteSessionId: runtime.getRemoteSessionId?.() || null
|
|
31061
|
+
});
|
|
31167
31062
|
if (reason === "superseded") {
|
|
31168
31063
|
pushNotice("Remote mode OFF \u2014 another session took over remote.", "warn");
|
|
31064
|
+
} else if (reason === "claim-failed") {
|
|
31065
|
+
pushNotice("Remote mode could not start. Check the channel connection and try again.", "error");
|
|
31066
|
+
} else if (reason === "release-failed") {
|
|
31067
|
+
pushNotice("Remote mode could not be stopped cleanly. The shared state will reconcile automatically.", "error");
|
|
31169
31068
|
}
|
|
31170
31069
|
});
|
|
31171
31070
|
}
|
|
31071
|
+
set({
|
|
31072
|
+
remoteEnabled: runtime.isRemoteEnabled?.() === true,
|
|
31073
|
+
remoteSessionId: runtime.getRemoteSessionId?.() || null
|
|
31074
|
+
});
|
|
31172
31075
|
const { patchToolCardResult, flushToolResults } = createToolCardResults({
|
|
31173
31076
|
getState: () => state,
|
|
31174
31077
|
set,
|
|
@@ -39,17 +39,9 @@ export function compactEventLabel(event = {}) {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export function compactEventDetail(event = {}) {
|
|
42
|
-
// Keep the elapsed time as the lead detail, but no longer discard the rest of
|
|
43
|
-
// the compact metadata. Surface type/trigger and the boundary/pressure so the
|
|
44
|
-
// statusdone marker reflects what actually fired.
|
|
45
42
|
const parts = [];
|
|
46
43
|
const elapsed = formatElapsedSeconds(Number(event.durationMs ?? event.elapsedMs ?? 0));
|
|
47
44
|
if (elapsed) parts.push(elapsed);
|
|
48
|
-
const type = String(event.compactType || event.type || '').trim();
|
|
49
|
-
if (type && type !== 'semantic') parts.push(type);
|
|
50
|
-
const trigger = String(event.trigger || '').toLowerCase();
|
|
51
|
-
if (trigger === 'reactive') parts.push('reactive');
|
|
52
|
-
else if (trigger === 'manual') parts.push('manual');
|
|
53
45
|
const before = Number(event.pressureTokens ?? event.beforeTokens ?? 0);
|
|
54
46
|
const after = Number(event.afterTokens ?? 0);
|
|
55
47
|
const fmtTok = (n) => {
|
|
@@ -368,6 +368,15 @@ export function createEngineApiB(bag) {
|
|
|
368
368
|
listWorkflows: () => {
|
|
369
369
|
return runtime.listWorkflows?.() || [];
|
|
370
370
|
},
|
|
371
|
+
// Workflow pack editing (desktop Workflows page): direct passthroughs —
|
|
372
|
+
// the runtime owns validation, user-pack persistence, and delete guards.
|
|
373
|
+
getWorkflowPack: (workflowId) => runtime.getWorkflowPack?.(workflowId) ?? null,
|
|
374
|
+
saveWorkflowPack: async (payload) => runtime.saveWorkflowPack?.(payload) ?? null,
|
|
375
|
+
createWorkflow: async (payload) => runtime.createWorkflow?.(payload) ?? null,
|
|
376
|
+
deleteWorkflow: async (workflowId) => runtime.deleteWorkflow?.(workflowId) ?? null,
|
|
377
|
+
getAgentDefinition: (agentId) => runtime.getAgentDefinition?.(agentId) ?? null,
|
|
378
|
+
saveAgentDefinition: async (payload) => runtime.saveAgentDefinition?.(payload) ?? null,
|
|
379
|
+
deleteAgentDefinition: async (agentId) => runtime.deleteAgentDefinition?.(agentId) ?? null,
|
|
371
380
|
getOutputStyle: () => {
|
|
372
381
|
return runtime.getOutputStyle?.() || runtime.listOutputStyles?.() || null;
|
|
373
382
|
},
|
|
@@ -403,27 +412,53 @@ export function createEngineApiB(bag) {
|
|
|
403
412
|
set({ commandBusy: false });
|
|
404
413
|
}
|
|
405
414
|
},
|
|
406
|
-
// Toggle
|
|
407
|
-
//
|
|
408
|
-
//
|
|
409
|
-
|
|
415
|
+
// Toggle channel remote mode for the CURRENT session (session-scoped,
|
|
416
|
+
// user decision). Off → on claims for this session; on + this session
|
|
417
|
+
// owns → off; on + ANOTHER session owns → move the relay seat here
|
|
418
|
+
// (last-wins) without restarting the worker.
|
|
419
|
+
toggleRemote: async () => {
|
|
410
420
|
const enabled = runtime.isRemoteEnabled?.() === true;
|
|
411
|
-
|
|
412
|
-
|
|
421
|
+
const owner = String(runtime.getRemoteSessionId?.() || '');
|
|
422
|
+
const currentId = String(getState().sessionId || '');
|
|
423
|
+
// Stale-enabled guard: boot auto-acquire can leave remote reading ON
|
|
424
|
+
// while the channels worker already self-shut (no live clients). A
|
|
425
|
+
// toggle in that state must BOOT the worker for this session, not
|
|
426
|
+
// no-op into a claim or flip a dead relay "off".
|
|
427
|
+
const workerRunning = runtime.getChannelWorkerStatus?.()?.running === true;
|
|
428
|
+
if (enabled && !workerRunning) {
|
|
429
|
+
await runtime.startRemote?.();
|
|
430
|
+
const next = runtime.isRemoteEnabled?.() === true;
|
|
431
|
+
set({ remoteEnabled: next, remoteSessionId: runtime.getRemoteSessionId?.() || null });
|
|
432
|
+
return next;
|
|
433
|
+
}
|
|
434
|
+
if (enabled && owner && currentId && owner !== currentId) {
|
|
435
|
+
runtime.claimRemoteForCurrentSession?.();
|
|
436
|
+
set({ remoteEnabled: true, remoteSessionId: runtime.getRemoteSessionId?.() || null });
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
if (enabled) await runtime.releaseRemote?.();
|
|
440
|
+
else await runtime.startRemote?.();
|
|
413
441
|
const next = runtime.isRemoteEnabled?.() === true;
|
|
414
|
-
set({ remoteEnabled: next });
|
|
442
|
+
set({ remoteEnabled: next, remoteSessionId: runtime.getRemoteSessionId?.() || null });
|
|
415
443
|
return next;
|
|
416
444
|
},
|
|
417
445
|
// Force-claim remote for this session (single-holder, last-wins). Always
|
|
418
446
|
// turns remote ON here and steals the bridge seat; the previous holder is
|
|
419
447
|
// superseded and flips itself OFF via onRemoteStateChange. Used by the
|
|
420
448
|
// `/remote` slash command — repeated /remote just re-claims (idempotent).
|
|
421
|
-
claimRemote: () => {
|
|
422
|
-
runtime.startRemote?.();
|
|
449
|
+
claimRemote: async () => {
|
|
450
|
+
await runtime.startRemote?.();
|
|
423
451
|
const next = runtime.isRemoteEnabled?.() === true;
|
|
424
|
-
set({ remoteEnabled: next });
|
|
452
|
+
set({ remoteEnabled: next, remoteSessionId: runtime.getRemoteSessionId?.() || null });
|
|
425
453
|
return next;
|
|
426
454
|
},
|
|
455
|
+
// Idempotent desired-state OFF for desktop controls. Unlike toggleRemote,
|
|
456
|
+
// a delayed/replayed request can never turn remote back on.
|
|
457
|
+
releaseRemote: async () => {
|
|
458
|
+
await runtime.releaseRemote?.();
|
|
459
|
+
set({ remoteEnabled: false, remoteSessionId: null });
|
|
460
|
+
return false;
|
|
461
|
+
},
|
|
427
462
|
isRemoteEnabled: () => runtime.isRemoteEnabled?.() === true,
|
|
428
463
|
getVoiceStatus: () => getVoiceStatus(),
|
|
429
464
|
// Desktop push-to-talk dictation: accept a recorded audio payload
|