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.
Files changed (73) hide show
  1. package/package.json +6 -4
  2. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +1 -6
  3. package/src/runtime/agent/orchestrator/context/collect.mjs +42 -27
  4. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +1 -1
  5. package/src/runtime/agent/orchestrator/providers/gemini.mjs +0 -6
  6. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +161 -0
  7. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +8 -204
  8. package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +26 -0
  9. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +25 -13
  10. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +3 -0
  11. package/src/runtime/agent/orchestrator/session/manager.mjs +0 -11
  12. package/src/runtime/agent/orchestrator/session/store/listing.mjs +613 -0
  13. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +1 -0
  14. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +10 -2
  15. package/src/runtime/agent/orchestrator/session/store.mjs +9 -575
  16. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +41 -0
  17. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-output.mjs +154 -0
  18. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +78 -18
  19. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +9 -144
  20. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +7 -3
  21. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +40 -3
  22. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +24 -8
  23. package/src/runtime/channels/lib/config.mjs +6 -5
  24. package/src/runtime/channels/lib/owned-runtime.mjs +65 -5
  25. package/src/runtime/channels/lib/scheduler.mjs +7 -15
  26. package/src/runtime/channels/lib/tool-dispatch.mjs +6 -1
  27. package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +6 -2
  28. package/src/runtime/channels/lib/webhook.mjs +36 -46
  29. package/src/runtime/channels/lib/worker-main.mjs +45 -92
  30. package/src/runtime/shared/automation-attachments.mjs +72 -0
  31. package/src/runtime/shared/automation-workflow.mjs +41 -0
  32. package/src/runtime/shared/schedule-session-run.mjs +10 -1
  33. package/src/runtime/shared/schedules-db.mjs +18 -3
  34. package/src/runtime/shared/webhook-session-run.mjs +57 -0
  35. package/src/runtime/shared/webhooks-db.mjs +19 -3
  36. package/src/session-runtime/channel-config-api.mjs +8 -1
  37. package/src/session-runtime/lifecycle-api.mjs +7 -0
  38. package/src/session-runtime/memory-daemon-probe.mjs +61 -0
  39. package/src/session-runtime/model-capabilities.mjs +6 -4
  40. package/src/session-runtime/notification-bus.mjs +82 -0
  41. package/src/session-runtime/provider-auth-api.mjs +16 -1
  42. package/src/session-runtime/provider-usage.mjs +3 -0
  43. package/src/session-runtime/remote-control.mjs +166 -0
  44. package/src/session-runtime/remote-transcript.mjs +126 -0
  45. package/src/session-runtime/remote-transition-queue.mjs +14 -0
  46. package/src/session-runtime/runtime-core.mjs +184 -660
  47. package/src/session-runtime/runtime-tunables.mjs +57 -0
  48. package/src/session-runtime/self-update.mjs +129 -0
  49. package/src/session-runtime/skills-api.mjs +77 -0
  50. package/src/session-runtime/tool-surface.mjs +83 -0
  51. package/src/session-runtime/workflow-agents-api.mjs +206 -3
  52. package/src/session-runtime/workflow.mjs +84 -17
  53. package/src/standalone/agent-tool/worker-index.mjs +287 -0
  54. package/src/standalone/agent-tool.mjs +22 -346
  55. package/src/standalone/agent-watchdog-registry.mjs +101 -0
  56. package/src/standalone/channel-admin.mjs +36 -1
  57. package/src/standalone/channel-daemon-client.mjs +5 -1
  58. package/src/standalone/channel-daemon-transport.mjs +112 -20
  59. package/src/standalone/channel-daemon.mjs +6 -4
  60. package/src/standalone/channel-worker.mjs +7 -13
  61. package/src/tui/App.jsx +2 -32
  62. package/src/tui/app/channel-pickers.mjs +2 -143
  63. package/src/tui/app/slash-commands.mjs +1 -3
  64. package/src/tui/app/slash-dispatch.mjs +3 -3
  65. package/src/tui/components/StatusLine.jsx +6 -5
  66. package/src/tui/dist/index.mjs +162 -259
  67. package/src/tui/engine/labels.mjs +0 -8
  68. package/src/tui/engine/session-api-ext.mjs +45 -10
  69. package/src/tui/engine/turn-watchdog.mjs +92 -0
  70. package/src/tui/engine/turn.mjs +18 -70
  71. package/src/tui/engine.mjs +14 -1
  72. package/src/workflows/default/WORKFLOW.md +1 -1
  73. package/src/workflows/solo/WORKFLOW.md +1 -1
@@ -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: 3, overflow: "hidden", justifyContent: "flex-start", paddingLeft: 2, backgroundColor: surfaceBackground(), children: [
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, marginRight: 1, children: /* @__PURE__ */ jsx2(Text2, { color: theme.statusText, wrap: "truncate", children: workflowLabel }) })
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, channels, schedules, webhooks" },
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
- void openChannelSetupPicker("webhooks");
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 clearWatchdog = () => {
26914
- if (watchdogTimer) {
26915
- clearTimeout(watchdogTimer);
26916
- watchdogTimer = null;
26917
- }
26918
- if (watchdogGraceTimer) {
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
- }, delay2);
27015
- watchdogTimer.unref?.();
27016
- };
27017
- const markTurnProgress = (label) => {
27018
- if (!isCurrentTurn()) return false;
27019
- if (watchdogTripped) return;
27020
- lastProgressAt = Date.now();
27021
- lastProgressLabel = String(label || "progress");
27022
- return true;
26875
+ }
26876
+ });
26877
+ const clearWatchdog = () => {
26878
+ watchdog.clear();
26879
+ if (watchdogGraceTimer) {
26880
+ clearTimeout(watchdogGraceTimer);
26881
+ watchdogGraceTimer = null;
26882
+ }
27023
26883
  };
27024
- armWatchdog();
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() || watchdogTripped) return;
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}${watchdogTripped ? " watchdogTripped=1" : ""} pending=${pending.length}`);
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 Discord remote mode for this session. Flips the runtime's
28282
- // remoteEnabled flag (booting/stopping the channel worker) and returns the
28283
- // NEW enabled getState() so the caller can render an ON/OFF notice.
28284
- toggleRemote: () => {
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
- if (enabled) runtime.stopRemote?.();
28287
- else runtime.startRemote?.();
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({ remoteEnabled: enabled === true });
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 Discord remote mode for this session. Flips the runtime's
407
- // remoteEnabled flag (booting/stopping the channel worker) and returns the
408
- // NEW enabled getState() so the caller can render an ON/OFF notice.
409
- toggleRemote: () => {
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
- if (enabled) runtime.stopRemote?.();
412
- else runtime.startRemote?.();
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