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.
- package/NOTICE.md +72 -0
- package/package.json +16 -6
- package/scripts/lib/isolated-root-cleanup.mjs +19 -0
- package/scripts/run-suite.mjs +100 -0
- package/src/lib/rules-builder.cjs +4 -3
- package/src/output-styles/detailed.md +13 -15
- package/src/output-styles/extreme-minimal.md +7 -11
- package/src/output-styles/minimal.md +4 -7
- package/src/output-styles/simple.md +11 -13
- package/src/rules/lead/01-general.md +1 -2
- package/src/rules/lead/lead-brief.md +4 -5
- package/src/rules/lead/lead-tool.md +3 -2
- package/src/rules/shared/01-tool.md +28 -34
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -2
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +1 -1
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -0
- package/src/runtime/agent/orchestrator/agent-trace.mjs +29 -0
- package/src/runtime/agent/orchestrator/mcp/client.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +97 -7
- package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +11 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +2 -3
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +4 -6
- package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +8 -8
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +7 -8
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +14 -16
- package/src/runtime/agent/orchestrator/providers/openai-ws-headers.mjs +5 -4
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +10 -10
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -3
- package/src/runtime/agent/orchestrator/providers/registry.mjs +51 -4
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +15 -15
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +12 -32
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +10 -11
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +0 -14
- package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +9 -8
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +7 -4
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +40 -13
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +8 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +5 -9
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +3 -6
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +1 -1
- package/src/runtime/agent/orchestrator/stall-policy.mjs +2 -3
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +22 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +3 -4
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +10 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +35 -5
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +98 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +53 -12
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +47 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +9 -2
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +57 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +148 -15
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +5 -1
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +109 -13
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +10 -10
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +4 -5
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +4 -0
- package/src/runtime/channels/backends/discord.mjs +5 -14
- package/src/runtime/channels/backends/telegram.mjs +0 -5
- package/src/runtime/channels/lib/inbound-handler.mjs +0 -1
- package/src/runtime/channels/lib/output-forwarder.mjs +24 -5
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/worker-main.mjs +1 -1
- package/src/runtime/media/renditions.mjs +21 -2
- package/src/runtime/memory/tool-defs.mjs +6 -6
- package/src/runtime/shared/atomic-file.mjs +53 -0
- package/src/runtime/shared/background-tasks.mjs +6 -3
- package/src/runtime/shared/resource-admission.mjs +40 -1
- package/src/runtime/shared/tool-status.mjs +10 -1
- package/src/runtime/shared/tool-surface.mjs +7 -7
- package/src/runtime/shared/turn-snapshot.mjs +385 -21
- package/src/runtime/shared/turn-worktree-snapshot.mjs +540 -0
- package/src/session-runtime/context-status.mjs +9 -3
- package/src/session-runtime/lifecycle-api.mjs +35 -5
- package/src/session-runtime/mcp-glue.mjs +11 -6
- package/src/session-runtime/provider-models.mjs +94 -43
- package/src/session-runtime/runtime-core.mjs +46 -8
- package/src/session-runtime/runtime-tunables.mjs +4 -0
- package/src/session-runtime/self-update.mjs +33 -4
- package/src/session-runtime/session-lifecycle.mjs +2 -0
- package/src/session-runtime/session-text.mjs +2 -1
- package/src/session-runtime/session-turn-api.mjs +106 -22
- package/src/session-runtime/workflow.mjs +11 -4
- package/src/standalone/agent-tool.mjs +4 -4
- package/src/standalone/backend-daemon.mjs +570 -0
- package/src/standalone/channel-daemon-transport.mjs +141 -1
- package/src/standalone/channel-worker.mjs +3 -2
- package/src/standalone/engine-daemon-client.mjs +894 -0
- package/src/standalone/engine-daemon-local-bridge.mjs +20 -0
- package/src/standalone/engine-daemon-protocol.mjs +33 -0
- package/src/standalone/engine-daemon-service.mjs +864 -0
- package/src/standalone/engine-daemon-transport.mjs +603 -0
- package/src/tui/App.jsx +62 -47
- package/src/tui/app/app-format.mjs +4 -2
- package/src/tui/app/app-view.jsx +5 -0
- package/src/tui/app/channel-pickers.mjs +7 -6
- package/src/tui/app/core-memory-picker.mjs +4 -4
- package/src/tui/app/extension-pickers.mjs +20 -18
- package/src/tui/app/maintenance-pickers.mjs +27 -27
- package/src/tui/app/onboarding-steps.mjs +23 -18
- package/src/tui/app/prompt-submit.mjs +13 -2
- package/src/tui/app/route-pickers.mjs +13 -8
- package/src/tui/app/settings-picker.mjs +55 -58
- package/src/tui/app/slash-dispatch.mjs +32 -28
- package/src/tui/app/transcript-window.mjs +19 -0
- package/src/tui/app/usage-context-panels.mjs +22 -7
- package/src/tui/app/use-mouse-input.mjs +25 -3
- package/src/tui/app/use-prompt-queue-history.mjs +31 -16
- package/src/tui/app/use-transcript-scroll.mjs +30 -8
- package/src/tui/app/use-transcript-window.mjs +15 -0
- package/src/tui/app/use-welcome-prompt-hint.mjs +2 -2
- package/src/tui/components/PromptInput.jsx +10 -0
- package/src/tui/components/Spinner.jsx +89 -95
- package/src/tui/components/TextEntryPanel.jsx +14 -0
- package/src/tui/components/ToolExecution.jsx +2 -2
- package/src/tui/dist/index.mjs +1534 -9801
- package/src/tui/engine/agent-job-feed.mjs +2 -2
- package/src/tui/engine/live-share.mjs +76 -3
- package/src/tui/engine/session-api-ext.mjs +23 -1
- package/src/tui/engine/session-api.mjs +81 -41
- package/src/tui/engine/session-flow.mjs +43 -4
- package/src/tui/engine/tool-card-results.mjs +6 -0
- package/src/tui/engine/turn.mjs +8 -1
- package/src/tui/engine-local-session.mjs +1108 -0
- package/src/tui/engine.mjs +16 -1065
- package/src/tui/index.jsx +40 -2
- package/src/tui/markdown/stream-fence.mjs +1 -1
- package/src/tui/spinner-meta.mjs +80 -0
- package/src/tui/spinner-verbs.mjs +35 -0
- package/src/ui/statusline-segments.mjs +43 -10
- package/src/ui/statusline.mjs +10 -1
- package/scripts/tmp-cdp-errors.mjs +0 -41
- package/scripts/tmp-cdp-inspect.mjs +0 -41
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +0 -176
- 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 {
|
|
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
|
-
|
|
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
|
-
//
|
|
992
|
-
//
|
|
993
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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))
|
|
1176
|
-
|
|
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
|
-
}, {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
13
|
-
|
|
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:
|
|
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
|
-
//
|
|
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
|
|
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 (
|
|
196
|
-
//
|
|
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,
|
|
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
|
|
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
|
|
148
|
-
//
|
|
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
|
-
//
|
|
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: '
|
|
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
|
|
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.
|
|
22
|
-
//
|
|
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:
|
|
25
|
-
//
|
|
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 (
|
|
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
|
-
|
|
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)
|
|
12
|
-
//
|
|
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 (
|
|
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.
|
|
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
|
|
105
|
-
//
|
|
106
|
-
//
|
|
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
|
-
//
|
|
794
|
-
//
|
|
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 (
|
|
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;
|