mixdog 0.9.68 → 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 (48) 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.mjs +0 -11
  11. package/src/runtime/agent/orchestrator/session/store/listing.mjs +613 -0
  12. package/src/runtime/agent/orchestrator/session/store.mjs +9 -575
  13. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +41 -0
  14. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-output.mjs +154 -0
  15. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +78 -18
  16. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +9 -144
  17. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +7 -3
  18. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +40 -3
  19. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +24 -8
  20. package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +6 -2
  21. package/src/runtime/channels/lib/webhook.mjs +13 -1
  22. package/src/session-runtime/memory-daemon-probe.mjs +61 -0
  23. package/src/session-runtime/model-capabilities.mjs +6 -4
  24. package/src/session-runtime/notification-bus.mjs +82 -0
  25. package/src/session-runtime/provider-auth-api.mjs +16 -1
  26. package/src/session-runtime/provider-usage.mjs +3 -0
  27. package/src/session-runtime/remote-control.mjs +166 -0
  28. package/src/session-runtime/remote-transcript.mjs +126 -0
  29. package/src/session-runtime/remote-transition-queue.mjs +14 -0
  30. package/src/session-runtime/runtime-core.mjs +143 -705
  31. package/src/session-runtime/runtime-tunables.mjs +57 -0
  32. package/src/session-runtime/self-update.mjs +129 -0
  33. package/src/session-runtime/skills-api.mjs +77 -0
  34. package/src/session-runtime/tool-surface.mjs +83 -0
  35. package/src/session-runtime/workflow-agents-api.mjs +206 -3
  36. package/src/session-runtime/workflow.mjs +84 -17
  37. package/src/standalone/agent-tool/worker-index.mjs +287 -0
  38. package/src/standalone/agent-tool.mjs +22 -346
  39. package/src/standalone/agent-watchdog-registry.mjs +101 -0
  40. package/src/standalone/channel-worker.mjs +7 -13
  41. package/src/tui/components/StatusLine.jsx +6 -5
  42. package/src/tui/dist/index.mjs +124 -94
  43. package/src/tui/engine/labels.mjs +0 -8
  44. package/src/tui/engine/session-api-ext.mjs +17 -8
  45. package/src/tui/engine/turn-watchdog.mjs +92 -0
  46. package/src/tui/engine/turn.mjs +18 -70
  47. package/src/workflows/default/WORKFLOW.md +1 -1
  48. 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
  ] });
@@ -24171,11 +24171,6 @@ function compactEventDetail(event = {}) {
24171
24171
  const parts = [];
24172
24172
  const elapsed = formatElapsedSeconds(Number(event.durationMs ?? event.elapsedMs ?? 0));
24173
24173
  if (elapsed) parts.push(elapsed);
24174
- const type = String(event.compactType || event.type || "").trim();
24175
- if (type && type !== "semantic") parts.push(type);
24176
- const trigger = String(event.trigger || "").toLowerCase();
24177
- if (trigger === "reactive") parts.push("reactive");
24178
- else if (trigger === "manual") parts.push("manual");
24179
24174
  const before = Number(event.pressureTokens ?? event.beforeTokens ?? 0);
24180
24175
  const after = Number(event.afterTokens ?? 0);
24181
24176
  const fmtTok = (n) => {
@@ -26658,6 +26653,93 @@ function createSessionFlow(bag) {
26658
26653
  return { leadSessionId, shouldMirrorSteeringEntry, commitSteeringQueueEntries, makeQueueEntry, removeQueuedEntries, requeueEntriesFront, dequeueQueueBatch, drain, enqueue, drainPendingSteering, restoreLeadSteeringFromDisk, autoClearBeforeSubmit, performSessionClear, restoreQueued, resetStats, clearUiActivityBeforeContextSync, resetTuiForPendingSessionReset, snapshotTuiBeforeSessionReset, restoreTuiAfterFailedSessionReset, commitTuiSessionReset, resetStatsAndSyncContext };
26659
26654
  }
26660
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
+
26661
26743
  // src/tui/engine/turn.mjs
26662
26744
  function createRunTurn(bag) {
26663
26745
  const {
@@ -26742,76 +26824,15 @@ function createRunTurn(bag) {
26742
26824
  let assistantText = "";
26743
26825
  let currentAssistantId = null;
26744
26826
  tuiDebug2(`runTurn start turn=${turnIndex} pending=${pending.length} timeoutMs=${LEAD_TURN_TIMEOUT_MS2}`);
26745
- let watchdogTripped = false;
26746
- let watchdogTimer = null;
26747
26827
  let watchdogGraceTimer = null;
26748
- let lastProgressAt = startedAt;
26749
- let lastProgressLabel = "start";
26750
- let watchdogDeferralCeilingAt = 0;
26751
26828
  const configuredLeadToolMaxMs = Number(process.env.MIXDOG_LEAD_TOOL_MAX_MS);
26752
26829
  const leadToolMaxMs = Number.isFinite(configuredLeadToolMaxMs) && configuredLeadToolMaxMs > 0 ? configuredLeadToolMaxMs : 30 * 60 * 1e3;
26753
- const clearWatchdog = () => {
26754
- if (watchdogTimer) {
26755
- clearTimeout(watchdogTimer);
26756
- watchdogTimer = null;
26757
- }
26758
- if (watchdogGraceTimer) {
26759
- clearTimeout(watchdogGraceTimer);
26760
- watchdogGraceTimer = null;
26761
- }
26762
- };
26763
- const refreshWatchdogFromRuntimeLiveness = () => {
26764
- let liveness;
26765
- try {
26766
- liveness = runtime.getTurnLiveness?.();
26767
- } catch {
26768
- return false;
26769
- }
26770
- if (liveness?.stage !== "tool_running") watchdogDeferralCeilingAt = 0;
26771
- const progressAt = Number(liveness?.lastProgressAt);
26772
- const now = Date.now();
26773
- if (!liveness || !Number.isFinite(progressAt) || progressAt <= now - LEAD_TURN_TIMEOUT_MS2) return false;
26774
- if (liveness.stage === "tool_running") {
26775
- watchdogDeferralCeilingAt = 0;
26776
- const toolStartedAt = Number(liveness.toolStartedAt);
26777
- if (!Number.isFinite(toolStartedAt) || toolStartedAt <= 0) return false;
26778
- const toolSelfDeadlineMs = Number(liveness.toolSelfDeadlineMs);
26779
- const toolCeilingMs = Math.max(
26780
- Number.isFinite(toolSelfDeadlineMs) && toolSelfDeadlineMs > 0 ? toolSelfDeadlineMs + 6e4 : 0,
26781
- leadToolMaxMs
26782
- );
26783
- if (now - toolStartedAt >= toolCeilingMs) return false;
26784
- watchdogDeferralCeilingAt = toolStartedAt + toolCeilingMs;
26785
- }
26786
- lastProgressAt = progressAt;
26787
- lastProgressLabel = `orchestrator:${String(liveness.stage || "unknown")}`;
26788
- armWatchdog();
26789
- return true;
26790
- };
26791
- const armWatchdog = () => {
26792
- if (watchdogTimer) {
26793
- clearTimeout(watchdogTimer);
26794
- watchdogTimer = null;
26795
- }
26796
- if (watchdogTripped) return;
26797
- const now = Date.now();
26798
- const remaining = Math.max(1, LEAD_TURN_TIMEOUT_MS2 - Math.max(0, now - lastProgressAt));
26799
- const ceilingRemaining = watchdogDeferralCeilingAt > 0 ? Math.max(1, watchdogDeferralCeilingAt - now) : Infinity;
26800
- const delay2 = Math.min(remaining, ceilingRemaining);
26801
- watchdogTimer = setTimeout(() => {
26802
- if (!isCurrentTurn()) return;
26803
- if (watchdogTripped) return;
26804
- const now2 = Date.now();
26805
- const idleMs = now2 - lastProgressAt;
26806
- if (idleMs < LEAD_TURN_TIMEOUT_MS2) {
26807
- if (watchdogDeferralCeilingAt > 0 && now2 >= watchdogDeferralCeilingAt) {
26808
- if (refreshWatchdogFromRuntimeLiveness()) return;
26809
- } else {
26810
- armWatchdog();
26811
- return;
26812
- }
26813
- } else if (refreshWatchdogFromRuntimeLiveness()) return;
26814
- watchdogTripped = true;
26830
+ const watchdog = createTurnWatchdog({
26831
+ runtime,
26832
+ timeoutMs: LEAD_TURN_TIMEOUT_MS2,
26833
+ toolMaxMs: leadToolMaxMs,
26834
+ isCurrentTurn,
26835
+ onTrip: ({ idleMs, lastProgressLabel }) => {
26815
26836
  if (_batchTimer !== null) {
26816
26837
  clearTimeout(_batchTimer);
26817
26838
  _batchTimer = null;
@@ -26851,17 +26872,17 @@ function createRunTurn(bag) {
26851
26872
  flushEmitImmediate?.();
26852
26873
  }, 5e3);
26853
26874
  watchdogGraceTimer.unref?.();
26854
- }, delay2);
26855
- watchdogTimer.unref?.();
26856
- };
26857
- const markTurnProgress = (label) => {
26858
- if (!isCurrentTurn()) return false;
26859
- if (watchdogTripped) return;
26860
- lastProgressAt = Date.now();
26861
- lastProgressLabel = String(label || "progress");
26862
- return true;
26875
+ }
26876
+ });
26877
+ const clearWatchdog = () => {
26878
+ watchdog.clear();
26879
+ if (watchdogGraceTimer) {
26880
+ clearTimeout(watchdogGraceTimer);
26881
+ watchdogGraceTimer = null;
26882
+ }
26863
26883
  };
26864
- armWatchdog();
26884
+ const markTurnProgress = (label) => watchdog.markProgress(label);
26885
+ watchdog.arm();
26865
26886
  let currentAssistantText = "";
26866
26887
  const committedSegments = [];
26867
26888
  let thinkingText = "";
@@ -27607,7 +27628,7 @@ function createRunTurn(bag) {
27607
27628
  flushStreamBatch();
27608
27629
  },
27609
27630
  onReasoningDelta: (chunk) => {
27610
- if (!isCurrentTurn() || watchdogTripped) return;
27631
+ if (!isCurrentTurn() || watchdog.tripped) return;
27611
27632
  if (String(chunk ?? "")) {
27612
27633
  if (!markTurnProgress("reasoning-delta")) return;
27613
27634
  markPromptCommitted();
@@ -27747,7 +27768,7 @@ function createRunTurn(bag) {
27747
27768
  flushEmit?.();
27748
27769
  _publishedThinkingActive = false;
27749
27770
  const finalStatus = cancelled ? "cancelled" : failed ? "failed" : "done";
27750
- 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}`);
27751
27772
  return finalStatus;
27752
27773
  }
27753
27774
  return runTurn;
@@ -28085,6 +28106,15 @@ function createEngineApiB(bag) {
28085
28106
  listWorkflows: () => {
28086
28107
  return runtime.listWorkflows?.() || [];
28087
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,
28088
28118
  getOutputStyle: () => {
28089
28119
  return runtime.getOutputStyle?.() || runtime.listOutputStyles?.() || null;
28090
28120
  },
@@ -28122,13 +28152,13 @@ function createEngineApiB(bag) {
28122
28152
  // user decision). Off → on claims for this session; on + this session
28123
28153
  // owns → off; on + ANOTHER session owns → move the relay seat here
28124
28154
  // (last-wins) without restarting the worker.
28125
- toggleRemote: () => {
28155
+ toggleRemote: async () => {
28126
28156
  const enabled = runtime.isRemoteEnabled?.() === true;
28127
28157
  const owner = String(runtime.getRemoteSessionId?.() || "");
28128
28158
  const currentId = String(getState().sessionId || "");
28129
28159
  const workerRunning = runtime.getChannelWorkerStatus?.()?.running === true;
28130
28160
  if (enabled && !workerRunning) {
28131
- runtime.startRemote?.();
28161
+ await runtime.startRemote?.();
28132
28162
  const next2 = runtime.isRemoteEnabled?.() === true;
28133
28163
  set({ remoteEnabled: next2, remoteSessionId: runtime.getRemoteSessionId?.() || null });
28134
28164
  return next2;
@@ -28138,8 +28168,8 @@ function createEngineApiB(bag) {
28138
28168
  set({ remoteEnabled: true, remoteSessionId: runtime.getRemoteSessionId?.() || null });
28139
28169
  return true;
28140
28170
  }
28141
- if (enabled) runtime.releaseRemote?.();
28142
- else runtime.startRemote?.();
28171
+ if (enabled) await runtime.releaseRemote?.();
28172
+ else await runtime.startRemote?.();
28143
28173
  const next = runtime.isRemoteEnabled?.() === true;
28144
28174
  set({ remoteEnabled: next, remoteSessionId: runtime.getRemoteSessionId?.() || null });
28145
28175
  return next;
@@ -28148,16 +28178,16 @@ function createEngineApiB(bag) {
28148
28178
  // turns remote ON here and steals the bridge seat; the previous holder is
28149
28179
  // superseded and flips itself OFF via onRemoteStateChange. Used by the
28150
28180
  // `/remote` slash command — repeated /remote just re-claims (idempotent).
28151
- claimRemote: () => {
28152
- runtime.startRemote?.();
28181
+ claimRemote: async () => {
28182
+ await runtime.startRemote?.();
28153
28183
  const next = runtime.isRemoteEnabled?.() === true;
28154
28184
  set({ remoteEnabled: next, remoteSessionId: runtime.getRemoteSessionId?.() || null });
28155
28185
  return next;
28156
28186
  },
28157
28187
  // Idempotent desired-state OFF for desktop controls. Unlike toggleRemote,
28158
28188
  // a delayed/replayed request can never turn remote back on.
28159
- releaseRemote: () => {
28160
- runtime.releaseRemote?.();
28189
+ releaseRemote: async () => {
28190
+ await runtime.releaseRemote?.();
28161
28191
  set({ remoteEnabled: false, remoteSessionId: null });
28162
28192
  return false;
28163
28193
  },
@@ -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
  },
@@ -407,7 +416,7 @@ export function createEngineApiB(bag) {
407
416
  // user decision). Off → on claims for this session; on + this session
408
417
  // owns → off; on + ANOTHER session owns → move the relay seat here
409
418
  // (last-wins) without restarting the worker.
410
- toggleRemote: () => {
419
+ toggleRemote: async () => {
411
420
  const enabled = runtime.isRemoteEnabled?.() === true;
412
421
  const owner = String(runtime.getRemoteSessionId?.() || '');
413
422
  const currentId = String(getState().sessionId || '');
@@ -417,7 +426,7 @@ export function createEngineApiB(bag) {
417
426
  // no-op into a claim or flip a dead relay "off".
418
427
  const workerRunning = runtime.getChannelWorkerStatus?.()?.running === true;
419
428
  if (enabled && !workerRunning) {
420
- runtime.startRemote?.();
429
+ await runtime.startRemote?.();
421
430
  const next = runtime.isRemoteEnabled?.() === true;
422
431
  set({ remoteEnabled: next, remoteSessionId: runtime.getRemoteSessionId?.() || null });
423
432
  return next;
@@ -427,8 +436,8 @@ export function createEngineApiB(bag) {
427
436
  set({ remoteEnabled: true, remoteSessionId: runtime.getRemoteSessionId?.() || null });
428
437
  return true;
429
438
  }
430
- if (enabled) runtime.releaseRemote?.();
431
- else runtime.startRemote?.();
439
+ if (enabled) await runtime.releaseRemote?.();
440
+ else await runtime.startRemote?.();
432
441
  const next = runtime.isRemoteEnabled?.() === true;
433
442
  set({ remoteEnabled: next, remoteSessionId: runtime.getRemoteSessionId?.() || null });
434
443
  return next;
@@ -437,16 +446,16 @@ export function createEngineApiB(bag) {
437
446
  // turns remote ON here and steals the bridge seat; the previous holder is
438
447
  // superseded and flips itself OFF via onRemoteStateChange. Used by the
439
448
  // `/remote` slash command — repeated /remote just re-claims (idempotent).
440
- claimRemote: () => {
441
- runtime.startRemote?.();
449
+ claimRemote: async () => {
450
+ await runtime.startRemote?.();
442
451
  const next = runtime.isRemoteEnabled?.() === true;
443
452
  set({ remoteEnabled: next, remoteSessionId: runtime.getRemoteSessionId?.() || null });
444
453
  return next;
445
454
  },
446
455
  // Idempotent desired-state OFF for desktop controls. Unlike toggleRemote,
447
456
  // a delayed/replayed request can never turn remote back on.
448
- releaseRemote: () => {
449
- runtime.releaseRemote?.();
457
+ releaseRemote: async () => {
458
+ await runtime.releaseRemote?.();
450
459
  set({ remoteEnabled: false, remoteSessionId: null });
451
460
  return false;
452
461
  },
@@ -0,0 +1,92 @@
1
+ // Lead-turn stall watchdog. A turn is declared stuck only when NOTHING has
2
+ // progressed for the timeout: local deltas mark progress directly, and before
3
+ // tripping we re-read the orchestrator's own liveness so a long-but-alive tool
4
+ // (its self-deadline plus a grace ceiling) defers the trip instead of aborting
5
+ // real work. Extracted from turn.mjs; the trip ACTION stays there because it
6
+ // unwinds turn-scoped UI state.
7
+ export function createTurnWatchdog({
8
+ runtime,
9
+ timeoutMs,
10
+ toolMaxMs,
11
+ isCurrentTurn,
12
+ onTrip,
13
+ }) {
14
+ let timer = null;
15
+ let tripped = false;
16
+ let lastProgressAt = Date.now();
17
+ let lastProgressLabel = 'start';
18
+ // While a tool runs, the trip is deferred until this wall-clock ceiling.
19
+ let deferralCeilingAt = 0;
20
+
21
+ function clear() {
22
+ if (timer) { clearTimeout(timer); timer = null; }
23
+ }
24
+
25
+ // Adopt the orchestrator's liveness as progress when it is fresher than the
26
+ // local view; returns true when the trip was deferred.
27
+ function refreshFromRuntimeLiveness() {
28
+ let liveness;
29
+ try { liveness = runtime.getTurnLiveness?.(); } catch { return false; }
30
+ if (liveness?.stage !== 'tool_running') deferralCeilingAt = 0;
31
+ const progressAt = Number(liveness?.lastProgressAt);
32
+ const now = Date.now();
33
+ if (!liveness || !Number.isFinite(progressAt) || progressAt <= now - timeoutMs) return false;
34
+
35
+ if (liveness.stage === 'tool_running') {
36
+ deferralCeilingAt = 0;
37
+ const toolStartedAt = Number(liveness.toolStartedAt);
38
+ if (!Number.isFinite(toolStartedAt) || toolStartedAt <= 0) return false;
39
+ const toolSelfDeadlineMs = Number(liveness.toolSelfDeadlineMs);
40
+ const toolCeilingMs = Math.max(
41
+ Number.isFinite(toolSelfDeadlineMs) && toolSelfDeadlineMs > 0 ? toolSelfDeadlineMs + 60_000 : 0,
42
+ toolMaxMs,
43
+ );
44
+ if (now - toolStartedAt >= toolCeilingMs) return false;
45
+ deferralCeilingAt = toolStartedAt + toolCeilingMs;
46
+ }
47
+
48
+ lastProgressAt = progressAt;
49
+ lastProgressLabel = `orchestrator:${String(liveness.stage || 'unknown')}`;
50
+ arm();
51
+ return true;
52
+ }
53
+
54
+ function arm() {
55
+ clear();
56
+ if (tripped) return;
57
+ const now = Date.now();
58
+ const remaining = Math.max(1, timeoutMs - Math.max(0, now - lastProgressAt));
59
+ const ceilingRemaining = deferralCeilingAt > 0 ? Math.max(1, deferralCeilingAt - now) : Infinity;
60
+ timer = setTimeout(() => {
61
+ if (!isCurrentTurn() || tripped) return;
62
+ const firedAt = Date.now();
63
+ const idleMs = firedAt - lastProgressAt;
64
+ if (idleMs < timeoutMs) {
65
+ if (deferralCeilingAt > 0 && firedAt >= deferralCeilingAt) {
66
+ if (refreshFromRuntimeLiveness()) return;
67
+ } else {
68
+ arm();
69
+ return;
70
+ }
71
+ } else if (refreshFromRuntimeLiveness()) return;
72
+ tripped = true;
73
+ onTrip({ idleMs, lastProgressLabel });
74
+ }, Math.min(remaining, ceilingRemaining));
75
+ timer.unref?.();
76
+ }
77
+
78
+ function markProgress(label) {
79
+ if (!isCurrentTurn()) return false;
80
+ if (tripped) return false;
81
+ lastProgressAt = Date.now();
82
+ lastProgressLabel = String(label || 'progress');
83
+ return true;
84
+ }
85
+
86
+ return {
87
+ arm,
88
+ clear,
89
+ markProgress,
90
+ get tripped() { return tripped; },
91
+ };
92
+ }
@@ -8,6 +8,7 @@ import { toolResultText, toolErrorDisplay } from './tool-result-text.mjs';
8
8
  import { toolCallId, toolResultCallId, toolCallName, toolCallArgs } from './tool-call-fields.mjs';
9
9
  import { promptDisplayText, STEERING_SUPPRESSED_DISPLAY } from './queue-helpers.mjs';
10
10
  import { yieldToRenderer } from './render-timing.mjs';
11
+ import { createTurnWatchdog } from './turn-watchdog.mjs';
11
12
  import { aggregateRawResult, aggregateBucketForCategory, aggregateSummaries, assignAggregateSummaryOrder, failureDetailText, toolCallOutcome } from './tool-result-status.mjs';
12
13
 
13
14
  export function createRunTurn(bag) {
@@ -82,69 +83,19 @@ export function createRunTurn(bag) {
82
83
  // teardown runs. Keep this progress-based rather than total wall-clock:
83
84
  // long multi-tool turns can legitimately exceed 5m while still producing
84
85
  // model/tool events.
85
- let watchdogTripped = false;
86
- let watchdogTimer = null;
87
86
  let watchdogGraceTimer = null;
88
- let lastProgressAt = startedAt;
89
- let lastProgressLabel = 'start';
90
- let watchdogDeferralCeilingAt = 0;
91
87
  const configuredLeadToolMaxMs = Number(process.env.MIXDOG_LEAD_TOOL_MAX_MS);
92
88
  const leadToolMaxMs = Number.isFinite(configuredLeadToolMaxMs) && configuredLeadToolMaxMs > 0
93
89
  ? configuredLeadToolMaxMs
94
90
  : 30 * 60 * 1000;
95
- const clearWatchdog = () => {
96
- if (watchdogTimer) { clearTimeout(watchdogTimer); watchdogTimer = null; }
97
- if (watchdogGraceTimer) { clearTimeout(watchdogGraceTimer); watchdogGraceTimer = null; }
98
- };
99
- const refreshWatchdogFromRuntimeLiveness = () => {
100
- let liveness;
101
- try { liveness = runtime.getTurnLiveness?.(); } catch { return false; }
102
- if (liveness?.stage !== 'tool_running') watchdogDeferralCeilingAt = 0;
103
- const progressAt = Number(liveness?.lastProgressAt);
104
- const now = Date.now();
105
- if (!liveness || !Number.isFinite(progressAt) || progressAt <= now - LEAD_TURN_TIMEOUT_MS) return false;
106
-
107
- if (liveness.stage === 'tool_running') {
108
- watchdogDeferralCeilingAt = 0;
109
- const toolStartedAt = Number(liveness.toolStartedAt);
110
- if (!Number.isFinite(toolStartedAt) || toolStartedAt <= 0) return false;
111
- const toolSelfDeadlineMs = Number(liveness.toolSelfDeadlineMs);
112
- const toolCeilingMs = Math.max(
113
- Number.isFinite(toolSelfDeadlineMs) && toolSelfDeadlineMs > 0 ? toolSelfDeadlineMs + 60_000 : 0,
114
- leadToolMaxMs,
115
- );
116
- if (now - toolStartedAt >= toolCeilingMs) return false;
117
- watchdogDeferralCeilingAt = toolStartedAt + toolCeilingMs;
118
- }
119
-
120
- lastProgressAt = progressAt;
121
- lastProgressLabel = `orchestrator:${String(liveness.stage || 'unknown')}`;
122
- armWatchdog();
123
- return true;
124
- };
125
- const armWatchdog = () => {
126
- if (watchdogTimer) { clearTimeout(watchdogTimer); watchdogTimer = null; }
127
- if (watchdogTripped) return;
128
- const now = Date.now();
129
- const remaining = Math.max(1, LEAD_TURN_TIMEOUT_MS - Math.max(0, now - lastProgressAt));
130
- const ceilingRemaining = watchdogDeferralCeilingAt > 0
131
- ? Math.max(1, watchdogDeferralCeilingAt - now)
132
- : Infinity;
133
- const delay = Math.min(remaining, ceilingRemaining);
134
- watchdogTimer = setTimeout(() => {
135
- if (!isCurrentTurn()) return;
136
- if (watchdogTripped) return;
137
- const now = Date.now();
138
- const idleMs = now - lastProgressAt;
139
- if (idleMs < LEAD_TURN_TIMEOUT_MS) {
140
- if (watchdogDeferralCeilingAt > 0 && now >= watchdogDeferralCeilingAt) {
141
- if (refreshWatchdogFromRuntimeLiveness()) return;
142
- } else {
143
- armWatchdog();
144
- return;
145
- }
146
- } else if (refreshWatchdogFromRuntimeLiveness()) return;
147
- watchdogTripped = true;
91
+ // Scheduling/liveness lives in turn-watchdog.mjs; the trip ACTION stays here
92
+ // because it unwinds this turn's UI state.
93
+ const watchdog = createTurnWatchdog({
94
+ runtime,
95
+ timeoutMs: LEAD_TURN_TIMEOUT_MS,
96
+ toolMaxMs: leadToolMaxMs,
97
+ isCurrentTurn,
98
+ onTrip: ({ idleMs, lastProgressLabel }) => {
148
99
  if (_batchTimer !== null) {
149
100
  clearTimeout(_batchTimer);
150
101
  _batchTimer = null;
@@ -189,17 +140,14 @@ export function createRunTurn(bag) {
189
140
  flushEmitImmediate?.();
190
141
  }, 5_000);
191
142
  watchdogGraceTimer.unref?.();
192
- }, delay);
193
- watchdogTimer.unref?.();
194
- };
195
- const markTurnProgress = (label) => {
196
- if (!isCurrentTurn()) return false;
197
- if (watchdogTripped) return;
198
- lastProgressAt = Date.now();
199
- lastProgressLabel = String(label || 'progress');
200
- return true;
143
+ },
144
+ });
145
+ const clearWatchdog = () => {
146
+ watchdog.clear();
147
+ if (watchdogGraceTimer) { clearTimeout(watchdogGraceTimer); watchdogGraceTimer = null; }
201
148
  };
202
- armWatchdog();
149
+ const markTurnProgress = (label) => watchdog.markProgress(label);
150
+ watchdog.arm();
203
151
  let currentAssistantText = '';
204
152
  // Segments sealed as their own assistant item(s) this turn via
205
153
  // commitAssistantSegment (e.g. a tool preamble, then a no-newline tail
@@ -1188,7 +1136,7 @@ export function createRunTurn(bag) {
1188
1136
  flushStreamBatch();
1189
1137
  },
1190
1138
  onReasoningDelta: (chunk) => {
1191
- if (!isCurrentTurn() || watchdogTripped) return;
1139
+ if (!isCurrentTurn() || watchdog.tripped) return;
1192
1140
  if (String(chunk ?? '')) {
1193
1141
  if (!markTurnProgress('reasoning-delta')) return;
1194
1142
  markPromptCommitted();
@@ -1395,7 +1343,7 @@ export function createRunTurn(bag) {
1395
1343
  flushEmit?.();
1396
1344
  _publishedThinkingActive = false; // turn teardown cleared getState().thinking
1397
1345
  const finalStatus = cancelled ? 'cancelled' : (failed ? 'failed' : 'done');
1398
- tuiDebug(`runTurn end turn=${turnIndex} status=${finalStatus} elapsedMs=${Date.now() - startedAt}${watchdogTripped ? ' watchdogTripped=1' : ''} pending=${pending.length}`);
1346
+ tuiDebug(`runTurn end turn=${turnIndex} status=${finalStatus} elapsedMs=${Date.now() - startedAt}${watchdog.tripped ? ' watchdogTripped=1' : ''} pending=${pending.length}`);
1399
1347
  return finalStatus;
1400
1348
  }
1401
1349
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  id: default
3
3
  name: Default
4
- description: "Default agent workflow — fan-out parallel delegation across independent scopes."
4
+ description: "Fan-out parallel delegation."
5
5
  agents: worker, heavy-worker, reviewer, debugger, maintainer
6
6
  ---
7
7
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  id: solo
3
3
  name: Solo
4
- description: "Solo workflow — Lead handles everything directly."
4
+ description: "Lead handles everything directly."
5
5
  agents:
6
6
  ---
7
7