mixdog 0.9.94 → 0.9.95

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 (161) hide show
  1. package/NOTICE.md +72 -0
  2. package/package.json +16 -6
  3. package/scripts/lib/isolated-root-cleanup.mjs +19 -0
  4. package/scripts/run-suite.mjs +100 -0
  5. package/src/lib/rules-builder.cjs +4 -3
  6. package/src/output-styles/detailed.md +13 -15
  7. package/src/output-styles/extreme-minimal.md +7 -11
  8. package/src/output-styles/minimal.md +4 -7
  9. package/src/output-styles/simple.md +11 -13
  10. package/src/rules/lead/01-general.md +1 -2
  11. package/src/rules/lead/lead-brief.md +4 -5
  12. package/src/rules/lead/lead-tool.md +3 -2
  13. package/src/rules/shared/01-tool.md +28 -34
  14. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -2
  15. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +1 -1
  16. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -0
  17. package/src/runtime/agent/orchestrator/agent-trace.mjs +29 -0
  18. package/src/runtime/agent/orchestrator/mcp/client.mjs +3 -3
  19. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +97 -7
  20. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +11 -4
  21. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +2 -3
  22. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -1
  23. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +4 -6
  24. package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +1 -1
  25. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +8 -8
  26. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +7 -8
  27. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +2 -2
  28. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +14 -16
  29. package/src/runtime/agent/orchestrator/providers/openai-ws-headers.mjs +5 -4
  30. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +10 -10
  31. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -3
  32. package/src/runtime/agent/orchestrator/providers/registry.mjs +51 -4
  33. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +15 -15
  34. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +12 -32
  35. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +7 -0
  36. package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +10 -3
  37. package/src/runtime/agent/orchestrator/session/context-utils.mjs +10 -11
  38. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +1 -0
  39. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -3
  40. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +0 -14
  41. package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +9 -8
  42. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +7 -4
  43. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +1 -1
  44. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +1 -1
  45. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +40 -13
  46. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +3 -0
  47. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +8 -1
  48. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +5 -9
  49. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +3 -3
  50. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +3 -6
  51. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +1 -1
  52. package/src/runtime/agent/orchestrator/stall-policy.mjs +2 -3
  53. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +22 -0
  54. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +6 -1
  55. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -1
  56. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +1 -1
  57. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +14 -0
  58. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +4 -4
  59. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +2 -2
  60. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +2 -1
  61. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +3 -3
  62. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +1 -1
  63. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +3 -4
  64. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +3 -3
  65. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +12 -0
  66. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +10 -3
  67. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +35 -5
  68. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +98 -0
  69. package/src/runtime/agent/orchestrator/tools/builtin.mjs +2 -2
  70. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +3 -1
  71. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +53 -12
  72. package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +47 -2
  73. package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +3 -1
  74. package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +9 -2
  75. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +2 -2
  76. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +1 -1
  77. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +57 -2
  78. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +148 -15
  79. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +5 -1
  80. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +109 -13
  81. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +10 -10
  82. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +4 -5
  83. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +4 -0
  84. package/src/runtime/channels/backends/discord.mjs +5 -14
  85. package/src/runtime/channels/backends/telegram.mjs +0 -5
  86. package/src/runtime/channels/lib/inbound-handler.mjs +0 -1
  87. package/src/runtime/channels/lib/output-forwarder.mjs +24 -5
  88. package/src/runtime/channels/lib/scheduler.mjs +1 -1
  89. package/src/runtime/channels/lib/worker-main.mjs +1 -1
  90. package/src/runtime/media/renditions.mjs +21 -2
  91. package/src/runtime/memory/tool-defs.mjs +6 -6
  92. package/src/runtime/shared/atomic-file.mjs +53 -0
  93. package/src/runtime/shared/background-tasks.mjs +6 -3
  94. package/src/runtime/shared/resource-admission.mjs +40 -1
  95. package/src/runtime/shared/tool-status.mjs +10 -1
  96. package/src/runtime/shared/tool-surface.mjs +7 -7
  97. package/src/runtime/shared/turn-snapshot.mjs +385 -21
  98. package/src/runtime/shared/turn-worktree-snapshot.mjs +540 -0
  99. package/src/session-runtime/context-status.mjs +9 -3
  100. package/src/session-runtime/lifecycle-api.mjs +35 -5
  101. package/src/session-runtime/mcp-glue.mjs +11 -6
  102. package/src/session-runtime/provider-models.mjs +94 -43
  103. package/src/session-runtime/runtime-core.mjs +46 -8
  104. package/src/session-runtime/runtime-tunables.mjs +4 -0
  105. package/src/session-runtime/self-update.mjs +33 -4
  106. package/src/session-runtime/session-lifecycle.mjs +2 -0
  107. package/src/session-runtime/session-text.mjs +2 -1
  108. package/src/session-runtime/session-turn-api.mjs +106 -22
  109. package/src/session-runtime/workflow.mjs +11 -4
  110. package/src/standalone/agent-tool.mjs +4 -4
  111. package/src/standalone/backend-daemon.mjs +570 -0
  112. package/src/standalone/channel-daemon-transport.mjs +141 -1
  113. package/src/standalone/channel-worker.mjs +3 -2
  114. package/src/standalone/engine-daemon-client.mjs +894 -0
  115. package/src/standalone/engine-daemon-local-bridge.mjs +20 -0
  116. package/src/standalone/engine-daemon-protocol.mjs +33 -0
  117. package/src/standalone/engine-daemon-service.mjs +864 -0
  118. package/src/standalone/engine-daemon-transport.mjs +603 -0
  119. package/src/tui/App.jsx +62 -47
  120. package/src/tui/app/app-format.mjs +4 -2
  121. package/src/tui/app/app-view.jsx +5 -0
  122. package/src/tui/app/channel-pickers.mjs +7 -6
  123. package/src/tui/app/core-memory-picker.mjs +4 -4
  124. package/src/tui/app/extension-pickers.mjs +20 -18
  125. package/src/tui/app/maintenance-pickers.mjs +27 -27
  126. package/src/tui/app/onboarding-steps.mjs +23 -18
  127. package/src/tui/app/prompt-submit.mjs +13 -2
  128. package/src/tui/app/route-pickers.mjs +13 -8
  129. package/src/tui/app/settings-picker.mjs +55 -58
  130. package/src/tui/app/slash-dispatch.mjs +32 -28
  131. package/src/tui/app/transcript-window.mjs +19 -0
  132. package/src/tui/app/usage-context-panels.mjs +22 -7
  133. package/src/tui/app/use-mouse-input.mjs +25 -3
  134. package/src/tui/app/use-prompt-queue-history.mjs +31 -16
  135. package/src/tui/app/use-transcript-scroll.mjs +30 -8
  136. package/src/tui/app/use-transcript-window.mjs +15 -0
  137. package/src/tui/app/use-welcome-prompt-hint.mjs +2 -2
  138. package/src/tui/components/PromptInput.jsx +10 -0
  139. package/src/tui/components/Spinner.jsx +89 -95
  140. package/src/tui/components/TextEntryPanel.jsx +14 -0
  141. package/src/tui/components/ToolExecution.jsx +2 -2
  142. package/src/tui/dist/index.mjs +1534 -9801
  143. package/src/tui/engine/agent-job-feed.mjs +2 -2
  144. package/src/tui/engine/live-share.mjs +76 -3
  145. package/src/tui/engine/session-api-ext.mjs +23 -1
  146. package/src/tui/engine/session-api.mjs +81 -41
  147. package/src/tui/engine/session-flow.mjs +43 -4
  148. package/src/tui/engine/tool-card-results.mjs +6 -0
  149. package/src/tui/engine/turn.mjs +8 -1
  150. package/src/tui/engine-local-session.mjs +1108 -0
  151. package/src/tui/engine.mjs +16 -1065
  152. package/src/tui/index.jsx +40 -2
  153. package/src/tui/markdown/stream-fence.mjs +1 -1
  154. package/src/tui/spinner-meta.mjs +80 -0
  155. package/src/tui/spinner-verbs.mjs +35 -0
  156. package/src/ui/statusline-segments.mjs +43 -10
  157. package/src/ui/statusline.mjs +10 -1
  158. package/scripts/tmp-cdp-errors.mjs +0 -41
  159. package/scripts/tmp-cdp-inspect.mjs +0 -41
  160. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +0 -176
  161. package/src/standalone/channel-daemon.mjs +0 -226
@@ -4,7 +4,7 @@ import { join } from 'path';
4
4
  import { readFileSync, statSync } from 'fs';
5
5
  import { createHash, randomBytes } from 'crypto';
6
6
  import { resolvePluginData } from '../../../../shared/plugin-paths.mjs';
7
- import { updateJsonAtomicSync, updateJsonAtomic } from '../../../../shared/atomic-file.mjs';
7
+ import { updateJsonAtomic } from '../../../../shared/atomic-file.mjs';
8
8
  import { promptContentText, isInternalRuntimeNotificationText } from './prompt-utils.mjs';
9
9
  import { loadSession, readSessionLifecycleStateFromDisk, saveSessionAsync } from '../store.mjs';
10
10
  import { isDeliveredCompletion, logDuplicateSkip } from './delivered-completions.mjs';
@@ -942,11 +942,11 @@ function shouldEvictPendingSession(sessionId, ttlMs, entryTouchedAt, now = Date.
942
942
  return entryTouch > 0 && (now - entryTouch) > PENDING_ORPHAN_GRACE_MS;
943
943
  }
944
944
 
945
- export function sweepOrphanedPendingMessages({ ttlMs = PENDING_ORPHAN_TTL_MS } = {}) {
945
+ export async function sweepOrphanedPendingMessages({ ttlMs = PENDING_ORPHAN_TTL_MS } = {}) {
946
946
  const now = Date.now();
947
947
  const removed = [];
948
948
  try {
949
- updateJsonAtomicSync(pendingMessagesPath(), (raw) => {
949
+ await updateJsonAtomic(pendingMessagesPath(), (raw) => {
950
950
  const next = normalizePendingStore(raw);
951
951
  const ids = Object.keys(next.sessions);
952
952
  if (ids.length === 0) return undefined;
@@ -988,9 +988,9 @@ function modelVisiblePendingMessages(messages) {
988
988
  }
989
989
 
990
990
  export function _mergePendingMessageEntries(entries) {
991
- // CC-parity delivery priority (reference messageQueueManager.ts: user
992
- // input enqueues at 'next', task notifications at 'later', and dequeue
993
- // always serves 'next' first). Our single merged turn message is the
991
+ // Delivery priority: user input enqueues at 'next', task notifications at
992
+ // 'later', and dequeue always serves 'next' first. Our single merged turn
993
+ // message is the
994
994
  // analogue of that dequeue order: genuine user/steering entries are
995
995
  // merged BEFORE deferred completion notifications so queued user input
996
996
  // is never buried under system notification text. FIFO is preserved
@@ -1074,7 +1074,16 @@ export function enqueueRemotePendingMessage(sessionId, message) {
1074
1074
  // spool lock (see persistPendingMessages).
1075
1075
  runPendingTestHook('enqueue:beforePublish', { sessionId });
1076
1076
  if (pendingLifecycleInvalidated(sessionId, token)) return 0;
1077
- const remoteEntry = stampLifecycleToken({ ...normalized, id: newPendingMessageId() }, token);
1077
+ // Preserve the viewer's submission id end-to-end. Minting a fresh id here
1078
+ // made pipe-delivered + spool-fallback twins look like two messages
1079
+ // (owner accepted the pipe copy, then re-took the spool copy).
1080
+ const preservedId = pendingMessageId(normalized) || pendingMessageId(message);
1081
+ const remoteEntry = stampLifecycleToken({
1082
+ ...normalized,
1083
+ id: (typeof preservedId === 'string' && preservedId.trim())
1084
+ ? preservedId.trim()
1085
+ : newPendingMessageId(),
1086
+ }, token);
1078
1087
  return persistPendingMessages(sessionId, [remoteEntry]);
1079
1088
  }
1080
1089
 
@@ -1104,7 +1113,7 @@ function _rememberForeignSpoolScan(sessionId, mtime) {
1104
1113
  * internal-notification entries are left untouched for the normal
1105
1114
  * askSession hydrate path.
1106
1115
  */
1107
- export function drainForeignUserInjections(sessionId) {
1116
+ export async function drainForeignUserInjections(sessionId) {
1108
1117
  if (!isValidPendingSessionId(sessionId)) return [];
1109
1118
  // DELIVERY path, not cleanup: this takes rows out of the spool and hands
1110
1119
  // them to a live turn. A tombstoned (or otherwise invalidated) session may
@@ -1147,7 +1156,13 @@ export function drainForeignUserInjections(sessionId) {
1147
1156
  // spool immediately pollable again.
1148
1157
  let lifecycleDecided = false;
1149
1158
  try {
1150
- updateJsonAtomicSync(pendingMessagesPath(), (raw) => {
1159
+ // ASYNC spool lock. The sync variant parked the owning process's event
1160
+ // loop in Atomics.wait for the whole timeout: in the daemon (PTYs,
1161
+ // engines, IPC and SSE fan-out share one loop) every poll that lost the
1162
+ // race froze the entire app, and it could never win against an async
1163
+ // holder in the SAME process. Waiting off the loop also lets the
1164
+ // in-process lock queue serialize sibling sessions for free.
1165
+ await updateJsonAtomic(pendingMessagesPath(), (raw) => {
1151
1166
  // Same authority re-read INSIDE the spool lock: a close/detach
1152
1167
  // landing in this window must neither deliver to the old owner nor
1153
1168
  // remove the reopened owner's rows.
@@ -1172,8 +1187,10 @@ export function drainForeignUserInjections(sessionId) {
1172
1187
  // late-delivery header instead of being silently removed
1173
1188
  // (user report: remote sends silently discarded around owner
1174
1189
  // restarts).
1175
- if (foreignUser && isStaleUserInjection(entry)) taken.push(lateDeliveryText(text, entry));
1176
- else if (foreignUser) taken.push(text);
1190
+ if (foreignUser && isStaleUserInjection(entry)) {
1191
+ taken.push({ text: lateDeliveryText(text, entry), id });
1192
+ }
1193
+ else if (foreignUser) taken.push({ text, id });
1177
1194
  else kept.push(entry);
1178
1195
  }
1179
1196
  if (taken.length === 0) return undefined;
@@ -1184,8 +1201,18 @@ export function drainForeignUserInjections(sessionId) {
1184
1201
  }
1185
1202
  next.updatedAt = Date.now();
1186
1203
  return next;
1187
- }, { compact: true, lock: true, mode: PENDING_MESSAGES_MODE, fsync: false });
1204
+ }, {
1205
+ compact: true,
1206
+ lock: true,
1207
+ mode: PENDING_MESSAGES_MODE,
1208
+ fsync: false,
1209
+ // Delivery polling must never wait on an async spool writer in the
1210
+ // same daemon process: waiting synchronously prevents that writer
1211
+ // from ever reaching its finally/release path.
1212
+ timeoutMs: 0,
1213
+ });
1188
1214
  } catch (err) {
1215
+ if (err?.code === 'ELOCKCONTENDED') return [];
1189
1216
  try { process.stderr.write(`[session] foreign-injection drain failed sessionId=${sessionId}: ${err?.message || err}\n`); } catch {}
1190
1217
  return [];
1191
1218
  }
@@ -1422,5 +1449,5 @@ setImmediate(() => {
1422
1449
  // the cross-process lock against the lead's writes for zero
1423
1450
  // benefit — the lead already sweeps.
1424
1451
  if (process.env.MIXDOG_WORKER_MODE === '1') return;
1425
- try { sweepOrphanedPendingMessages(); } catch { /* ignore */ }
1452
+ void sweepOrphanedPendingMessages().catch(() => {});
1426
1453
  });
@@ -12,6 +12,7 @@ import { _dropPendingMessageState } from './pending-messages.mjs';
12
12
  import { _stopToolActivityHeartbeat, _getRuntimeEntry, _clearSessionRuntime } from './runtime-liveness.mjs';
13
13
  import { _closeBashSessionLazy } from './runtime-loaders.mjs';
14
14
  import { clearTurnCheckpoint } from './turn-checkpoint.mjs';
15
+ import { releaseReadSnapshotScope } from '../../tools/builtin/snapshot-store.mjs';
15
16
 
16
17
  /**
17
18
  * Close a session. Plants a `closed=true` tombstone on disk with a bumped
@@ -137,6 +138,7 @@ export function closeSession(id, reason = 'manual', opts = {}) {
137
138
  // Drop session-scoped read dedup cache so the Map doesn't accumulate
138
139
  // entries across mcp-server lifetime.
139
140
  try { clearReadDedupSession(id); } catch { /* ignore */ }
141
+ try { releaseReadSnapshotScope(id); } catch { /* ignore */ }
140
142
  // Drop offload sidecars + module-level counter for this session so a
141
143
  // long-running mcp-server doesn't leak disk (tool-results/<id>/*.txt)
142
144
  // or Map entries across session lifetime. Fire-and-forget — close path
@@ -226,6 +228,7 @@ export function unloadSessionRuntime(id, reason = 'runtime-unload') {
226
228
  try { globalThis.__mixdogCloseProviderConnectionsForSession?.(id, `runtime-unload:${reason}`); } catch { /* ignore */ }
227
229
  // Pure caches — a resumed turn rebuilds them from the transcript/disk.
228
230
  try { clearReadDedupSession(id); } catch { /* ignore */ }
231
+ try { releaseReadSnapshotScope(id); } catch { /* ignore */ }
229
232
  _clearSessionRuntime(id);
230
233
  if (process.env.MIXDOG_DEBUG_SESSION_LOG) {
231
234
  try { process.stderr.write(`[agent-unload] session=${id} reason=${reason} shells=${allBashIds.length}\n`); } catch { /* best-effort */ }
@@ -169,7 +169,14 @@ export function createSession(opts) {
169
169
  const provider = getProvider(providerName);
170
170
  if (!provider)
171
171
  throw new Error(`Provider "${providerName}" not found or not enabled`);
172
- const id = mintSessionId();
172
+ const requestedId = String(opts.id || '').trim();
173
+ if (requestedId && !/^[A-Za-z0-9_-]+$/.test(requestedId)) {
174
+ throw new Error('createSession: id is invalid');
175
+ }
176
+ // The daemon may reserve the durable address before provider/session
177
+ // materialization. Supplying that reservation here keeps the address
178
+ // stable across intake -> queued turn -> provider execution.
179
+ const id = requestedId || mintSessionId();
173
180
  // Provider cache strategy — agentRuntime.resolveSync() above is a
174
181
  // best-effort injection point (setAgentRuntime() has no live caller
175
182
  // today, so that branch never fires); build it directly here so every
@@ -7,10 +7,10 @@ import { filterModelVisibleSessionMessages } from './message-sanitize.mjs';
7
7
 
8
8
  const INTERRUPT_MESSAGE = '[Request interrupted by user]';
9
9
  const INTERRUPT_MESSAGE_FOR_TOOL_USE = '[Request interrupted by user for tool use]';
10
- const STREAMING_INTERRUPTED_TOOL_RESULT = 'Interrupted by user';
11
10
  const PROCESS_RESTART_INTERRUPT_MESSAGE = '[Request interrupted by process restart]';
12
- const PROCESS_RESTART_TOOL_RESULT = 'Interrupted by process restart';
13
- const TOOL_USE_REJECT_RESULT = "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.";
11
+ // Short tool_result body for any unfinished call closed by
12
+ // cancel/crash. UI maps this (and legacy long reject bodies) to Cancelled.
13
+ const INTERRUPTED_TOOL_RESULT = 'Cancelled';
14
14
 
15
15
  // Abort reasons that represent an EXPLICIT user cancellation of this turn.
16
16
  // Only these rewind a not-yet-answered user turn out of history — the TUI/
@@ -136,11 +136,7 @@ function finalizeInterruptedTurn({
136
136
  const executionStarted = phase === 'tools' || observed?.eagerStarted === true;
137
137
  messages.push({
138
138
  role: 'tool',
139
- content: abortReason === 'process-crash'
140
- ? PROCESS_RESTART_TOOL_RESULT
141
- : (executionStarted
142
- ? TOOL_USE_REJECT_RESULT
143
- : STREAMING_INTERRUPTED_TOOL_RESULT),
139
+ content: INTERRUPTED_TOOL_RESULT,
144
140
  toolCallId: callId,
145
141
  toolKind: 'error',
146
142
  });
@@ -148,7 +144,7 @@ function finalizeInterruptedTurn({
148
144
  }
149
145
 
150
146
  const pairedMessages = sanitizeToolPairs(messages);
151
- // Claude Code omits the synthetic marker when a queued user submission
147
+ // The synthetic marker is omitted when a queued user submission
152
148
  // interrupted the active request; that queued message is the boundary.
153
149
  if (abortReason !== 'interrupt' && abortReason !== 'provider-error') {
154
150
  pairedMessages.push({
@@ -36,7 +36,7 @@ function normalizedIncompleteUsage(raw) {
36
36
  };
37
37
  }
38
38
 
39
- // Loop-level transport replay (codex retry_transport parity). The provider
39
+ // Loop-level transport replay. The provider
40
40
  // layer already retries transient failures with a ~15s total envelope
41
41
  // (PROVIDER_RETRY_BACKOFF_MS); a real network blip (router/VPN flap — the
42
42
  // 2026-08-02 17:37 incident also dropped the Discord gateway) outlasts it and
@@ -192,8 +192,8 @@ export async function sendWithRecovery(ctx) {
192
192
  // have been mid-sentence, and there is no completion signal that
193
193
  // says the work is done. Promoting it to a normal terminal response
194
194
  // silently reported truncated/aborted turns as finished. Keep it an
195
- // explicit failure (like codex's no-completed stream handling and
196
- // opencode's persisted partial-failure): the already-streamed text
195
+ // explicit failure (no completion signal means no completed turn,
196
+ // and the partial is persisted): the already-streamed text
197
197
  // is preserved by the interruption/error persistence path
198
198
  // (turn-interruption.mjs records the deltas and commits them as the
199
199
  // partial assistant message alongside the error), so nothing visible
@@ -48,7 +48,7 @@ export async function processToolBatch(ctx) {
48
48
  const {
49
49
  calls, messages, tools, cwd, sessionId, sessionRef, signal, opts,
50
50
  iterations, assistantTurnMsg, pending, epoch, startEagerRun,
51
- crossTurnCalls, crossTurnCap, sessionAgent, steeringLadder,
51
+ crossTurnCalls, crossTurnCap, sessionAgent,
52
52
  pushToolResultMessage, throwIfAborted, repeatFailLimit,
53
53
  } = ctx;
54
54
  const executeToolFn = typeof ctx.executeToolFn === 'function' ? ctx.executeToolFn : executeTool;
@@ -323,6 +323,7 @@ export async function processToolBatch(ctx) {
323
323
  toolEndedAt = Date.now();
324
324
  _resultKind = 'error';
325
325
  } else {
326
+ await opts.beforeToolExecution?.();
326
327
  result = await executeToolFn(call.name, call.arguments, cwd, sessionId, sessionRef, { toolCallId: call.id, signal, notifyFn: opts.notifyFn, toolApprovalHook: opts.onToolApproval, iteration: iterations, deferShellCwdCommit: true });
327
328
  toolEndedAt = Date.now();
328
329
  // Boundary: tool-return string convention → structural kind.
@@ -632,7 +633,7 @@ export async function processToolBatch(ctx) {
632
633
  toolCallId: call.id,
633
634
  toolKind: _resultKind,
634
635
  ...(_nativeToolSearch ? { nativeToolSearch: _nativeToolSearch } : {}),
635
- ...(_applyPatchUiDiff ? { uiDiff: _applyPatchUiDiff } : {}),
636
+ ...(_applyPatchUiDiff !== null ? { uiDiff: _applyPatchUiDiff } : {}),
636
637
  });
637
638
  if (_executeOk) {
638
639
  const _isEager = isEagerDispatchable(call.name, tools);
@@ -730,10 +731,6 @@ export async function processToolBatch(ctx) {
730
731
  } catch { /* best-effort: PostToolBatch hook must never break the loop */ }
731
732
  }
732
733
  }
733
- // Completion-first steering hints (missed-parallelism / all-read-only /
734
- // serial-rewording). At most ONE hint per turn. The ladder controller
735
- // owns the cumulative counters and streaks.
736
- steeringLadder.emitPostBatchSteering(calls, false);
737
734
  // Mid-turn steering is drained at the next loop's pre-send point,
738
735
  // AFTER any auto-compact pass. Draining here would put the steering
739
736
  // user turn after the fresh tool results before compaction runs; then
@@ -202,7 +202,7 @@ export async function maybeOffloadToolResult(sessionId, toolCallId, toolName, re
202
202
  return `${header}\n\n${preview}${suffix}`;
203
203
  }
204
204
 
205
- // Apply per-tool persistence first, then enforce the Claude Code message-level
205
+ // Apply per-tool persistence first, then enforce the message-level
206
206
  // budget across the remaining non-Read text results. Selection is
207
207
  // largest-first; ties prefer the latest result in the assistant tool batch.
208
208
  export async function maybeOffloadToolResultBatch(sessionId, entries, options = {}) {
@@ -144,9 +144,8 @@ export const PROVIDER_SSE_IDLE_TIMEOUT_MS = resolveTimeoutMs(
144
144
  // to 300s silently collapsed it to 135s, which deterministically beheaded
145
145
  // live long-thinking turns (regex-chess / circuit-fibsqrt: ttft ~1.5s, then
146
146
  // killed at exactly 135s of silent thinking, 4 attempts ≈ 552s per turn).
147
- // Reference parity: Codex aborts only after a 300s single-gap silence
148
- // (DEFAULT_STREAM_IDLE_TIMEOUT_MS); Claude Code ships its watchdog OFF by
149
- // default and falls back to non-streaming when it does abort. The default is
147
+ // Reference behavior: abort only after a 300s single-gap silence, and fall
148
+ // back to non-streaming instead of killing the turn. The default is
150
149
  // therefore a FIXED 300s, decoupled from the warn math. The only remaining
151
150
  // coupling is the ordering guarantee: cap at (abort − tick) so the provider
152
151
  // layer — which can retry or fall back non-streaming — always fires strictly
@@ -726,7 +726,19 @@ function guardFind(a) {
726
726
  if (!queryOk) {
727
727
  return `Error: find requires non-empty string (or string[]) "query" (got ${describeType(a.query)})`;
728
728
  }
729
+ if (hasOwn(a, 'path')) {
730
+ // Absorb: a JSON-stringified array ('["apps/desktop"]') or a
731
+ // single-entry batch (['apps/desktop']). find takes ONE base
732
+ // directory, and a lone entry IS that directory — no ambiguity.
733
+ const coerced = coerceShapeFlex(a.path);
734
+ a.path = (Array.isArray(coerced) && coerced.length === 1 && isString(coerced[0]))
735
+ ? coerced[0]
736
+ : coerced;
737
+ }
729
738
  if (hasOwn(a, 'path') && !isString(a.path)) {
739
+ if (Array.isArray(a.path)) {
740
+ return `Error: find arg "path" must be a single base directory (got ${a.path.length}) — run find once per base, or widen "query".`;
741
+ }
730
742
  return `Error: find arg "path" must be a string (got ${describeType(a.path)})`;
731
743
  }
732
744
  if (hasOwn(a, 'head_limit') && a.head_limit !== undefined && a.head_limit !== null) {
@@ -832,6 +844,15 @@ const GUARDS = {
832
844
  code_graph: guardCodeGraph,
833
845
  };
834
846
 
847
+ // A bridge/provider can materialize an optional schema field as an explicit
848
+ // `undefined`. JSON cannot carry undefined, so it means "not sent" — drop the
849
+ // key instead of failing the call on a field the caller never set.
850
+ function dropUndefinedArgs(args) {
851
+ for (const key of Object.keys(args)) {
852
+ if (args[key] === undefined) delete args[key];
853
+ }
854
+ }
855
+
835
856
  export function validateBuiltinArgs(toolName, args) {
836
857
  const guard = GUARDS[toolName];
837
858
  if (!guard) return null;
@@ -841,6 +862,7 @@ export function validateBuiltinArgs(toolName, args) {
841
862
  if (typeof args !== 'object' || Array.isArray(args)) {
842
863
  return `Error: ${toolName} arguments must be an object (got ${describeType(args)})`;
843
864
  }
865
+ dropUndefinedArgs(args);
844
866
  if (toolName === 'grep') applyGrepContextLeadPolicy(args);
845
867
  try {
846
868
  return guard(args) || null;
@@ -256,7 +256,7 @@ export async function executeBashTool(args, workDir, options = {}) {
256
256
  // decode + rm token guard; calling it here applies the same allowlist
257
257
  // to both persistent and stateless paths.
258
258
  const _rawCmd = String(args && args.command != null ? args.command : '');
259
- // Codex-parity: `apply_patch` typed into the shell (heredoc/argument/bare
259
+ // `apply_patch` typed into the shell (heredoc/argument/bare
260
260
  // patch forms) routes to the internal patch engine instead of failing as
261
261
  // an unknown binary. Runs BEFORE the exec-policy scan so patch BODY lines
262
262
  // (e.g. `+ rm -rf …`) are never misread as shell commands. Dynamic import
@@ -524,6 +524,9 @@ export async function executeBashTool(args, workDir, options = {}) {
524
524
  // Per-terminal session stamp: the dispatching terminal's
525
525
  // claude.exe pid (server-main threads callerSession.clientHostPid).
526
526
  clientHostPid: options?.clientHostPid,
527
+ // Dispatching session: hosts that pool many sessions in one
528
+ // process (desktop) scope the job to its own pane with this.
529
+ ownerSessionId: options?.callerSessionId || options?.sessionId || null,
527
530
  ...(options?.shellJobRuntime || {}),
528
531
  });
529
532
  if (job && job.error) {
@@ -634,6 +637,8 @@ export async function executeBashTool(args, workDir, options = {}) {
634
637
  // Threaded so an auto-backgrounded foreground job is stamped with
635
638
  // the dispatching terminal's claude.exe pid (per-terminal scope).
636
639
  clientHostPid: options?.clientHostPid,
640
+ // …and with the dispatching session, for per-pane scoping.
641
+ ownerSessionId: options?.callerSessionId || options?.sessionId || null,
637
642
  // MCP live-progress reporter (null unless the client subscribed via
638
643
  // callTool onprogress). execShellCommand emits throttled "running
639
644
  // Ns" frames while the foreground command runs.
@@ -104,7 +104,7 @@ export const BUILTIN_TOOLS = [
104
104
  type: 'object',
105
105
  properties: {
106
106
  task_id: { type: 'string', description: 'shell async task_id.' },
107
- action: { type: 'string', enum: ['list', 'status', 'read', 'wait', 'cancel'], description: 'Action; avoid polling loops.' },
107
+ action: { type: 'string', enum: ['list', 'status', 'read', 'wait', 'cancel'], description: 'Defaults to list, or wait when task_id is set; avoid polling loops.' },
108
108
  timeout_ms: { type: 'number', description: 'Wait timeout ms.' },
109
109
  },
110
110
  required: [],
@@ -334,7 +334,7 @@ function renderAtRadius(selected, sources, radius, _budget, notice) {
334
334
  )).join('\n');
335
335
  const sourceComplete = blocks.every((block) => block.sourceComplete);
336
336
  // Header is a format cue only — internal radius/char-budget numbers are
337
- // harness noise (neither Claude Code nor Codex surface theirs).
337
+ // harness noise and stay out of the model-visible text.
338
338
  const text = `[Raw source spans; apply_patch context may be copied verbatim]\n${body}${notice}`;
339
339
  return { text, sourceComplete, blockCount: blocks.length };
340
340
  }
@@ -289,6 +289,20 @@ export function coerceReadFamilyPathArg(path, workDir = null) {
289
289
  } catch { /* not a literal bracket path — allow JSON coercion */ }
290
290
  }
291
291
  }
292
+ // Absorb: a truncated JSON path array ('["C:\\a\\b.mjs' — a streamed
293
+ // argument cut mid-emit). Repaired only when exactly ONE path survives,
294
+ // so a real batch can never be silently narrowed to its first entry.
295
+ if (typeof path === 'string') {
296
+ const trimmed = path.trim();
297
+ if (trimmed.startsWith('["') && !trimmed.endsWith(']')) {
298
+ try {
299
+ const repaired = JSON.parse(`${trimmed}"]`);
300
+ if (Array.isArray(repaired) && repaired.length === 1 && typeof repaired[0] === 'string') {
301
+ return repaired[0];
302
+ }
303
+ } catch { /* not a repairable truncation — fall through */ }
304
+ }
305
+ }
292
306
  const coerced = coerceShapeFlex(path);
293
307
  if (!Array.isArray(coerced)) return coerced;
294
308
  const list = coerced
@@ -18,11 +18,11 @@ import { TOOL_OUTPUT_MAX_BYTES } from './tool-output-limit.mjs';
18
18
  // truncation (proceed with cap) rather than refusing. Hard in-memory cap
19
19
  // for loading a full file remains READ_MAX_SIZE_BYTES (10 MiB).
20
20
  //
21
- // READ_MAX_OUTPUT_BYTES (30 KB) — output-truncation cap. Lead-facing default tightened from 50k. Mirrors CC's
22
- // MAX_OUTPUT_SIZE intent (utils/file.ts:48) and the throw-vs-truncate
21
+ // READ_MAX_OUTPUT_BYTES (30 KB) — output-truncation cap. Lead-facing default tightened from 50k. Follows the
22
+ // common max-output-size intent and the throw-vs-truncate
23
23
  // trade-off Anthropic chose in #21841 (throw is more token-efficient).
24
- // NOTE: CC's parallel output budget is maxTokens = 25,000 tokens
25
- // (limits.ts:18), enforced post-read via a tokenizer throw. mixdog
24
+ // NOTE: the usual alternative is a ~25,000-token output budget
25
+ // enforced post-read via a tokenizer throw. mixdog
26
26
  // caps on BYTES at read time instead (no tokenizer in the hot path);
27
27
  // 30 KB ≈ well under a 25k-token budget, so the byte cap is left as-is
28
28
  // rather than converted to a token count that can't be expressed
@@ -15,8 +15,8 @@ export const API_IMAGE_MAX_BASE64_SIZE = 5 * 1024 * 1024; // 5 MB
15
15
  export const IMAGE_TARGET_RAW_SIZE = (API_IMAGE_MAX_BASE64_SIZE * 3) / 4; // 3.75 MB
16
16
  const IMAGE_MAX_WIDTH = 2000;
17
17
  const IMAGE_MAX_HEIGHT = 2000;
18
- // Token budget for a single image. est tokens = base64.length * 0.125 (CC's
19
- // per-image heuristic). Default aligns to the 5MB base64 API ceiling so the
18
+ // Token budget for a single image. est tokens = base64.length * 0.125 (the
19
+ // common per-image heuristic). Default aligns to the 5MB base64 API ceiling so the
20
20
  // dimension/raw-size resize governs the common case and the token gate only
21
21
  // fires on pathologically dense images.
22
22
  const DEFAULT_IMAGE_MAX_TOKENS = Math.ceil(API_IMAGE_MAX_BASE64_SIZE * 0.125);
@@ -12,6 +12,7 @@ import {
12
12
  rawContentCacheSet,
13
13
  } from './cache-layers.mjs';
14
14
  import {
15
+ rememberReadSnapshot,
15
16
  readFilesForScope,
16
17
  readScopeKey,
17
18
  scheduleScopePersist,
@@ -146,7 +147,7 @@ export function recordReadSnapshot(fullPath, st, scope = null, meta = {}) {
146
147
  } else if (!batchSliced && sameFile && Number.isFinite(existing?.fileLineCount) && existing.fileLineCount >= 0) {
147
148
  next.fileLineCount = Math.trunc(existing.fileLineCount);
148
149
  }
149
- readFiles.set(fullPath, next);
150
+ rememberReadSnapshot(fullPath, next, scope, readFiles);
150
151
  scheduleScopePersist(readScopeKey(scope));
151
152
  }
152
153
 
@@ -8,8 +8,8 @@ const requireCjs = createRequire(import.meta.url);
8
8
  const DEFAULT_READ_MAX_OUTPUT_BYTES = 100 * 1024;
9
9
 
10
10
  // PDFs at or under this size are emitted as an Anthropic base64 document
11
- // block (the model reads the rendered PDF directly). Mirrors CC's
12
- // PDF_TARGET_RAW_SIZE (20MB raw → ~27MB base64, under the 32MB request cap).
11
+ // block (the model reads the rendered PDF directly): 20MB raw → ~27MB
12
+ // base64, which stays under the 32MB request cap.
13
13
  // Larger PDFs fall back to pdf-parse TEXT extraction.
14
14
  const PDF_DOCUMENT_MAX_BYTES = 20 * 1024 * 1024;
15
15
 
@@ -19,7 +19,7 @@ const PDF_DOCUMENT_MAX_BYTES = 20 * 1024 * 1024;
19
19
  const PDF_MAGIC = Buffer.from('%PDF-', 'latin1');
20
20
 
21
21
  // Per-output text ceiling inside a notebook. A single cell output larger than
22
- // this is replaced with a jq hint (port of CC's large-notebook guidance) so a
22
+ // this is replaced with a jq hint (large-notebook guidance) so a
23
23
  // runaway stdout / data dump can't blow the read budget.
24
24
  const IPYNB_OUTPUT_MAX_CHARS = 10_000;
25
25
 
@@ -302,7 +302,7 @@ export async function rgSupportsPcre2() {
302
302
  }
303
303
 
304
304
  // Cap rg stdout accumulation so a runaway producer (huge repo, accidental
305
- // match-all) cannot balloon the JS string heap. Mirrors CC's ripgrep.ts cap.
305
+ // match-all) cannot balloon the JS string heap.
306
306
  const MAX_RG_STDOUT_BYTES = 20 * 1024 * 1024; // 20MB
307
307
 
308
308
  // SIGTERM → grace → force kill; hard Promise settle if 'close' never fires
@@ -101,10 +101,9 @@ import {
101
101
  } from './lib/grep-context-expander.mjs';
102
102
 
103
103
 
104
- // Default grep result cap when head_limit is unspecified. 250 matches the
105
- // Claude Code default (GrepTool DEFAULT_HEAD_LIMIT); the tool-result offload
106
- // layer still bounds oversized results. MIXDOG_GREP_DEFAULT_HEAD_LIMIT
107
- // overrides for A/B runs.
104
+ // Default grep result cap when head_limit is unspecified. 250 is the common
105
+ // harness default; the tool-result offload layer still bounds oversized
106
+ // results. MIXDOG_GREP_DEFAULT_HEAD_LIMIT overrides for A/B runs.
108
107
  function _grepDefaultHeadLimit() {
109
108
  const parsed = parseInt(process.env.MIXDOG_GREP_DEFAULT_HEAD_LIMIT ?? '', 10);
110
109
  return parsed > 0 ? parsed : 250;
@@ -790,8 +790,8 @@ export function detectBlockedSleepPattern(command, minSecs = 2) {
790
790
  return rest ? `sleep ${secs} followed by: ${rest.slice(0, 80)}` : `standalone sleep ${secs}`;
791
791
  }
792
792
 
793
- // Codex-parity shell interception (core maybe_parse_apply_patch_verified):
794
- // codex-trained models type `apply_patch <<'EOF' … EOF` INTO THE SHELL. No
793
+ // Shell interception: patch-trained models type `apply_patch <<'EOF' … EOF`
794
+ // INTO THE SHELL. No
795
795
  // such binary exists here, so the invocation is extracted and routed to the
796
796
  // internal apply_patch engine instead of dying as "command not found".
797
797
  // Returns null when the command is not an apply_patch invocation, { patch }
@@ -802,7 +802,7 @@ export function extractShellApplyPatchInvocation(command) {
802
802
  // Unwrap one `bash|sh|zsh -lc '<script>'` / `-c "<script>"` layer.
803
803
  const wrap = /^(?:bash|sh|zsh)\s+-l?c\s+(['"])([\s\S]*)\1\s*$/.exec(cmd);
804
804
  if (wrap) cmd = wrap[2].trim();
805
- // A bare patch pasted into the shell (codex ImplicitInvocation case) is
805
+ // A bare patch pasted into the shell (implicit invocation) is
806
806
  // unambiguous — route it to the engine directly.
807
807
  if (cmd.startsWith('*** Begin Patch')) return { patch: cmd };
808
808
  if (!/^apply_patch(?:\s|$)/.test(cmd)) return null;
@@ -236,6 +236,18 @@ export function resolveJobOwnerHostPid(clientHostPid) {
236
236
  return null;
237
237
  }
238
238
 
239
+ // Resolve the SESSION that owns a freshly-spawned job. The owner pid alone is
240
+ // not an ownership answer where one host process multiplexes many sessions
241
+ // (the desktop pools every pane's engine in one process): without this stamp a
242
+ // blank New task pane displayed another session's running shell. Agent
243
+ // sub-sessions stamp their OWNER session id — tool-exec threads callerSessionId
244
+ // as `notifySessionId || sessionRef.ownerSessionId` — because that is the
245
+ // session a pane actually renders.
246
+ export function resolveJobOwnerSessionId(sessionId) {
247
+ const value = String(sessionId ?? '').trim();
248
+ return value || null;
249
+ }
250
+
239
251
  export function writeShellJobDetail(detail) {
240
252
  // Session scope stamp: every job record is tagged with the CC host pid
241
253
  // that owns it (the claude.exe pid). This is the SAME physical pid the
@@ -29,6 +29,7 @@ import {
29
29
  shellJobEnforcedPath,
30
30
  shellJobGuardianReceiptPath,
31
31
  resolveJobOwnerHostPid,
32
+ resolveJobOwnerSessionId,
32
33
  trimShellJobSpill,
33
34
  writeShellJobDetail,
34
35
  readShellJobDetail,
@@ -82,7 +83,7 @@ import {
82
83
 
83
84
  export async function _startBackgroundShellJobImpl({
84
85
  command, timeoutMs, workDir, mergeStderr, spawnEnv, shell, shellArg, shellArgs,
85
- shellType, clientHostPid, spawnFn = spawn, writeDetailFn = writeShellJobDetail,
86
+ shellType, clientHostPid, ownerSessionId, spawnFn = spawn, writeDetailFn = writeShellJobDetail,
86
87
  rollbackTimeoutMs = 5000,
87
88
  }) {
88
89
  // Route ANY PowerShell shell to the PS wrapper, regardless of platform.
@@ -91,7 +92,7 @@ export async function _startBackgroundShellJobImpl({
91
92
  // bash script. isPowerShellShell already matches pwsh/powershell by stem.
92
93
  if (isPowerShellShell(shell, shellType)) {
93
94
  return startBackgroundPowerShellJob({
94
- command, timeoutMs, workDir, mergeStderr, spawnEnv, shell, clientHostPid,
95
+ command, timeoutMs, workDir, mergeStderr, spawnEnv, shell, clientHostPid, ownerSessionId,
95
96
  spawnFn, writeDetailFn,
96
97
  });
97
98
  }
@@ -201,6 +202,9 @@ export async function _startBackgroundShellJobImpl({
201
202
  donePath,
202
203
  // Per-terminal session stamp (see resolveJobOwnerHostPid).
203
204
  ownerHostPid: resolveJobOwnerHostPid(clientHostPid),
205
+ // Dispatching session (see resolveJobOwnerSessionId): a pane counts
206
+ // only the jobs its own session started.
207
+ ownerSessionId: resolveJobOwnerSessionId(ownerSessionId),
204
208
  startedAt: new Date().toISOString(),
205
209
  };
206
210
  let terminal = false;
@@ -293,7 +297,7 @@ export async function _startBackgroundShellJobImpl({
293
297
  }
294
298
 
295
299
  async function startBackgroundPowerShellJob({
296
- command, timeoutMs, workDir, mergeStderr, spawnEnv, shell, clientHostPid,
300
+ command, timeoutMs, workDir, mergeStderr, spawnEnv, shell, clientHostPid, ownerSessionId,
297
301
  spawnFn = spawn, writeDetailFn = writeShellJobDetail, rollbackTimeoutMs = 5000,
298
302
  }) {
299
303
  const jobId = `job_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
@@ -447,6 +451,9 @@ async function startBackgroundPowerShellJob({
447
451
  timeoutEnforced: Number(timeoutMs) > 0,
448
452
  // Per-terminal session stamp (see resolveJobOwnerHostPid).
449
453
  ownerHostPid: resolveJobOwnerHostPid(clientHostPid),
454
+ // Dispatching session (see resolveJobOwnerSessionId): a pane counts
455
+ // only the jobs its own session started.
456
+ ownerSessionId: resolveJobOwnerSessionId(ownerSessionId),
450
457
  startedAt: new Date().toISOString(),
451
458
  };
452
459
  let terminal = false;