mixdog 0.9.95 → 0.9.97
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/LICENSES/Apache-2.0.txt +201 -0
- package/LICENSES/MIT.txt +56 -0
- package/LICENSES/codex-NOTICE.txt +6 -0
- package/NOTICE.md +46 -9
- package/README.md +0 -17
- package/package.json +16 -12
- package/scripts/.tmp-coverage-report.mjs +96 -0
- package/scripts/build-tui.mjs +17 -1
- package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
- package/scripts/fixtures/patch-replay-corpus.json +98 -0
- package/src/defaults/mixdog-config.template.json +5 -8
- package/src/defaults/skills/setup/SKILL.md +1 -1
- package/src/lib/keychain-cjs.cjs +1 -1
- package/src/lib/mixdog-debug.cjs +2 -3
- package/src/rules/agent/30-explorer.md +7 -4
- package/src/rules/lead/01-general.md +1 -2
- package/src/rules/lead/lead-tool.md +0 -3
- package/src/rules/shared/01-tool.md +21 -29
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +18 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +31 -1
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +6 -17
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -1
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
- package/src/runtime/agent/orchestrator/config.mjs +246 -67
- package/src/runtime/agent/orchestrator/mcp/client.mjs +104 -41
- package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +23 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +113 -14
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +5 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +32 -11
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +89 -2
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +19 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +144 -39
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -8
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -1
- package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +3 -4
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +67 -7
- package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +291 -0
- package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +36 -10
- package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -33
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +0 -24
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +21 -29
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +7 -10
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +45 -25
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +142 -84
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
- package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +31 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +29 -9
- package/src/runtime/agent/orchestrator/session/manager.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +86 -8
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +71 -42
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +4 -33
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +4 -5
- package/src/runtime/agent/orchestrator/stall-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +35 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +116 -26
- package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +8 -9
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +6 -15
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +12 -26
- package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +83 -33
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +42 -45
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +27 -9
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +7 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +17 -39
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +73 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +0 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +3 -8
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +30 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +142 -26
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +47 -19
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +83 -13
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +39 -1
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +94 -20
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +16 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +108 -12
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +12 -11
- package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +63 -9
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
- package/src/runtime/channels/lib/config.mjs +34 -34
- package/src/runtime/channels/lib/inbound-handler.mjs +8 -8
- package/src/runtime/channels/lib/interaction-handlers.mjs +6 -6
- package/src/runtime/channels/lib/memory-client.mjs +31 -157
- package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
- package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
- package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
- package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
- package/src/runtime/channels/lib/runtime-paths.mjs +4 -30
- package/src/runtime/channels/lib/scheduler.mjs +2 -2
- package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
- package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
- package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
- package/src/runtime/channels/lib/worker-ipc.mjs +7 -7
- package/src/runtime/channels/lib/worker-main.mjs +38 -38
- package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
- package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
- package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
- package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/index.mjs +76 -248
- package/src/runtime/memory/lib/agent-ipc.mjs +146 -81
- package/src/runtime/memory/lib/compact-vector-cache.mjs +88 -0
- package/src/runtime/memory/lib/core-memory-store.mjs +18 -1
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +13 -34
- package/src/runtime/memory/lib/embedding-provider.mjs +47 -26
- package/src/runtime/memory/lib/embedding-worker.mjs +30 -34
- package/src/runtime/memory/lib/http-router.mjs +9 -6
- package/src/runtime/memory/lib/ko-morph.mjs +3 -20
- package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -1
- package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -0
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -2
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -0
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
- package/src/runtime/memory/lib/memory-cycle3.mjs +3 -1
- package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
- package/src/runtime/memory/lib/memory-log.mjs +1 -1
- package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
- package/src/runtime/memory/lib/memory.mjs +1 -1
- package/src/runtime/memory/lib/pg/process.mjs +32 -9
- package/src/runtime/memory/lib/session-ingest.mjs +1 -1
- package/src/runtime/memory/tool-defs.mjs +1 -5
- package/src/runtime/search/index.mjs +2 -2
- package/src/runtime/search/tool-defs.mjs +2 -2
- package/src/runtime/shared/abort-race.mjs +70 -0
- package/src/runtime/shared/agent-route-config.mjs +119 -0
- package/src/runtime/shared/atomic-file.mjs +80 -4
- package/src/runtime/shared/channel-notification-routing.mjs +2 -2
- package/src/runtime/shared/child-guardian.mjs +204 -108
- package/src/runtime/shared/child-spawn-gate.mjs +139 -72
- package/src/runtime/shared/config.mjs +120 -47
- package/src/runtime/shared/llm/index.mjs +1 -1
- package/src/runtime/shared/owner-fair-gate.mjs +134 -0
- package/src/runtime/shared/resource-admission.mjs +150 -100
- package/src/runtime/shared/stream-progress.mjs +6 -0
- package/src/runtime/shared/tool-execution-owner.mjs +12 -0
- package/src/runtime/shared/tool-result-summary.mjs +1 -1
- package/src/runtime/shared/tool-surface.mjs +91 -27
- package/src/runtime/shared/tool-workload-gates.mjs +29 -0
- package/src/runtime/shared/turn-snapshot.mjs +47 -6
- package/src/runtime/shared/turn-worktree-snapshot.mjs +14 -2
- package/src/session-runtime/channel-config-api.mjs +5 -5
- package/src/session-runtime/config-helpers.mjs +39 -37
- package/src/session-runtime/config-lifecycle.mjs +27 -27
- package/src/session-runtime/lifecycle-api.mjs +22 -3
- package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
- package/src/session-runtime/model-route-api.mjs +7 -5
- package/src/session-runtime/prewarm.mjs +1 -1
- package/src/session-runtime/provider-auth-api.mjs +0 -7
- package/src/session-runtime/provider-models.mjs +1 -1
- package/src/session-runtime/remote-control.mjs +6 -7
- package/src/session-runtime/runtime-core.mjs +63 -39
- package/src/session-runtime/runtime-tunables.mjs +4 -4
- package/src/session-runtime/self-update.mjs +1 -1
- package/src/session-runtime/session-lifecycle.mjs +45 -17
- package/src/session-runtime/session-title.mjs +1 -1
- package/src/session-runtime/session-turn-api.mjs +96 -24
- package/src/session-runtime/settings-api.mjs +12 -24
- package/src/session-runtime/tool-defs.mjs +3 -3
- package/src/session-runtime/workflow-agents-api.mjs +55 -81
- package/src/session-runtime/workflow.mjs +14 -24
- package/src/standalone/agent-dispatch-broker.mjs +198 -0
- package/src/standalone/agent-tool/helpers.mjs +2 -2
- package/src/standalone/agent-tool/spawn-flow.mjs +2 -0
- package/src/standalone/agent-tool/spawn-preset.mjs +11 -21
- package/src/standalone/agent-tool/tool-def.mjs +0 -14
- package/src/standalone/channel-admin.mjs +53 -64
- package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +33 -19
- package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +127 -46
- package/src/standalone/channel-worker.mjs +124 -647
- package/src/standalone/{backend-daemon.mjs → daemon.mjs} +270 -154
- package/src/standalone/explore-tool.mjs +187 -70
- package/src/standalone/fair-call-scheduler.mjs +264 -0
- package/src/standalone/memory-runtime-proxy.mjs +58 -1
- package/src/standalone/session-client.mjs +1134 -0
- package/src/standalone/session-protocol.mjs +192 -0
- package/src/standalone/session-runtime-pool.mjs +393 -0
- package/src/standalone/session-runtime-record.mjs +20 -0
- package/src/standalone/session-runtime-worker.mjs +203 -0
- package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +481 -224
- package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +390 -121
- package/src/standalone/session-wire.mjs +55 -0
- package/src/tui/App.jsx +34 -21
- package/src/tui/app/app-format.mjs +1 -1
- package/src/tui/app/app-view.jsx +5 -2
- package/src/tui/app/channel-pickers.mjs +41 -41
- package/src/tui/app/core-memory-picker.mjs +19 -19
- package/src/tui/app/doctor.mjs +5 -11
- package/src/tui/app/input-parsers.mjs +2 -2
- package/src/tui/app/message-selector.mjs +103 -0
- package/src/tui/app/onboarding-steps.mjs +3 -4
- package/src/tui/app/project-picker.mjs +80 -56
- package/src/tui/app/prompt-submit.mjs +48 -43
- package/src/tui/app/resume-picker.mjs +2 -2
- package/src/tui/app/route-pickers.mjs +3 -1
- package/src/tui/app/settings-picker.mjs +31 -33
- package/src/tui/app/transcript-window.mjs +38 -0
- package/src/tui/app/use-prompt-draft-flow.mjs +5 -7
- package/src/tui/app/use-prompt-handlers.mjs +86 -21
- package/src/tui/app/use-prompt-hint.mjs +3 -2
- package/src/tui/app/use-prompt-queue-history.mjs +10 -3
- package/src/tui/app/use-transcript-scroll.mjs +10 -3
- package/src/tui/app/use-transcript-window.mjs +50 -17
- package/src/tui/components/PromptInput.jsx +71 -38
- package/src/tui/components/Spinner.jsx +1 -1
- package/src/tui/components/ToolExecution.jsx +1 -1
- package/src/tui/components/prompt-input/edit-helpers.mjs +9 -11
- package/src/tui/components/prompt-input/escape-policy.mjs +42 -0
- package/src/tui/components/prompt-input/immediate-render.mjs +0 -10
- package/src/tui/components/prompt-input/interrupt-policy.mjs +10 -0
- package/src/tui/components/prompt-input/restore-policy.mjs +10 -0
- package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
- package/src/tui/dist/index.mjs +1031 -1676
- package/src/tui/hooks/useSession.mjs +16 -0
- package/src/tui/index.jsx +12 -13
- package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
- package/src/tui/{engine → session}/agent-job-feed.mjs +4 -4
- package/src/tui/{engine → session}/boot-profile.mjs +2 -2
- package/src/tui/{engine → session}/context-state.mjs +3 -3
- package/src/tui/{engine → session}/labels.mjs +2 -2
- package/src/tui/{engine → session}/live-share.mjs +38 -4
- package/src/tui/{engine → session}/notice-text.mjs +2 -2
- package/src/tui/{engine → session}/notification-plan.mjs +3 -3
- package/src/tui/session/oauth-flows.mjs +140 -0
- package/src/tui/{engine → session}/prompt-history.mjs +3 -3
- package/src/tui/{engine → session}/queue-helpers.mjs +2 -2
- package/src/tui/{engine → session}/render-timing.mjs +2 -2
- package/src/tui/{engine → session}/session-api-ext.mjs +30 -20
- package/src/tui/{engine → session}/session-api.mjs +110 -93
- package/src/tui/{engine → session}/session-flow.mjs +8 -2
- package/src/tui/{engine → session}/session-stats.mjs +3 -3
- package/src/tui/{engine → session}/tool-approval.mjs +3 -3
- package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
- package/src/tui/{engine → session}/tool-card-results.mjs +8 -6
- package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
- package/src/tui/{engine → session}/tool-result-text.mjs +13 -3
- package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
- package/src/tui/{engine → session}/turn.mjs +38 -176
- package/src/tui/{engine-local-session.mjs → session-local.mjs} +103 -103
- package/src/tui/session.mjs +22 -0
- package/src/tui/spinner-meta.mjs +4 -4
- package/src/tui/spinner-verbs.mjs +51 -3
- package/src/ui/statusline-segments.mjs +1 -1
- package/src/ui/statusline.mjs +1 -1
- package/vendor/ink/build/components/App.js +10 -1
- package/src/session-runtime/memory-daemon-probe.mjs +0 -61
- package/src/standalone/engine-daemon-client.mjs +0 -894
- package/src/standalone/engine-daemon-local-bridge.mjs +0 -20
- package/src/standalone/engine-daemon-protocol.mjs +0 -33
- package/src/tui/engine/turn-watchdog.mjs +0 -92
- package/src/tui/engine.mjs +0 -22
- package/src/tui/hooks/useEngine.mjs +0 -16
- /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
- /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
- /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
- /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
- /package/src/tui/{engine → session}/tui-steering-persist.mjs +0 -0
|
@@ -345,6 +345,8 @@ function normalizedRow(row, heartbeatAt = 0) {
|
|
|
345
345
|
preview: cleanText(row.preview),
|
|
346
346
|
generation: typeof row.generation === 'number' ? row.generation : 0,
|
|
347
347
|
implicitBashSessionId: row.implicitBashSessionId || null,
|
|
348
|
+
storageMtimeMs: positiveNumber(row.storageMtimeMs, 0),
|
|
349
|
+
storageSize: positiveNumber(row.storageSize, 0),
|
|
348
350
|
detachedReason: row.detachedReason || null,
|
|
349
351
|
};
|
|
350
352
|
}
|
|
@@ -412,10 +414,15 @@ function sessionHeartbeatMtimes() {
|
|
|
412
414
|
* Returns `null` when the directory itself is NOT enumerable (unreadable
|
|
413
415
|
* stat / readdir): that is not "no sessions", and the caller must keep the
|
|
414
416
|
* authority it already has (the index rows) instead of publishing an empty
|
|
415
|
-
* catalog. `indexRowsById` carries those rows so
|
|
416
|
-
* file
|
|
417
|
+
* catalog. `indexRowsById` carries those rows so unchanged files can reuse the
|
|
418
|
+
* durable summary and a single UNREADABLE file retains its last known row
|
|
419
|
+
* instead of vanishing.
|
|
417
420
|
*/
|
|
418
|
-
function scanSessionFiles(
|
|
421
|
+
function scanSessionFiles(
|
|
422
|
+
heartbeatMtimes = sessionHeartbeatMtimes(),
|
|
423
|
+
indexRowsById = new Map(),
|
|
424
|
+
{ indexMtimeMs = 0, forceRead = false } = {},
|
|
425
|
+
) {
|
|
419
426
|
const directory = join(dataDir(), 'sessions');
|
|
420
427
|
const dirProbe = probePath(directory);
|
|
421
428
|
if (dirProbe.state === PROBE_ABSENT) return [];
|
|
@@ -426,13 +433,37 @@ function scanSessionFiles(heartbeatMtimes = sessionHeartbeatMtimes(), indexRowsB
|
|
|
426
433
|
for (const filename of entries) {
|
|
427
434
|
if (!filename.endsWith('.json')) continue;
|
|
428
435
|
const storageId = filename.slice(0, -5);
|
|
429
|
-
const
|
|
436
|
+
const path = join(directory, filename);
|
|
437
|
+
const retained = indexRowsById.get(storageId);
|
|
438
|
+
const probe = probePath(path);
|
|
439
|
+
// Provably gone: the row legitimately disappears with the file.
|
|
440
|
+
if (probe.state === PROBE_ABSENT) continue;
|
|
441
|
+
if (probe.state !== PROBE_PRESENT) {
|
|
442
|
+
if (retained) rows.push(retained);
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (!forceRead && retained) {
|
|
446
|
+
const exactFingerprint = retained.storageMtimeMs > 0
|
|
447
|
+
&& retained.storageSize > 0
|
|
448
|
+
&& retained.storageMtimeMs === probe.mtimeMs
|
|
449
|
+
&& retained.storageSize === probe.size;
|
|
450
|
+
// Older v2 rows predate per-file fingerprints. Their global index
|
|
451
|
+
// timestamp remains a one-release compatibility fallback; every
|
|
452
|
+
// later save/rebuild stamps the exact (mtimeMs,size) pair.
|
|
453
|
+
const legacyUnchanged = retained.storageMtimeMs === 0
|
|
454
|
+
&& indexMtimeMs > 0
|
|
455
|
+
&& (probe.mtimeMs || 0) <= indexMtimeMs;
|
|
456
|
+
if (exactFingerprint || legacyUnchanged) {
|
|
457
|
+
rows.push(retained);
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
const read = readTextFile(path);
|
|
430
462
|
// Provably gone: the row legitimately disappears with the file.
|
|
431
463
|
if (read.state === PROBE_ABSENT) continue;
|
|
432
464
|
if (read.state !== PROBE_PRESENT) {
|
|
433
465
|
// Exists and could not be read (EACCES/EIO): retain the last known
|
|
434
466
|
// row rather than let a transient IO error delete it from view.
|
|
435
|
-
const retained = indexRowsById.get(storageId);
|
|
436
467
|
if (retained) rows.push(retained);
|
|
437
468
|
continue;
|
|
438
469
|
}
|
|
@@ -440,36 +471,16 @@ function scanSessionFiles(heartbeatMtimes = sessionHeartbeatMtimes(), indexRowsB
|
|
|
440
471
|
// record or a foreign identity is not this file's session.
|
|
441
472
|
const record = readTopLevelLifecycleRecord(read.text);
|
|
442
473
|
if (isLifecycleUnreadable(record) || record.id !== storageId) continue;
|
|
443
|
-
const
|
|
474
|
+
const summary = rowFromSession(record.doc, heartbeatMtimes.get(record.id) || 0);
|
|
475
|
+
const row = summary
|
|
476
|
+
? { ...summary, storageMtimeMs: probe.mtimeMs, storageSize: probe.size }
|
|
477
|
+
: null;
|
|
444
478
|
if (row) rows.push(row);
|
|
445
479
|
}
|
|
446
480
|
return leadRowsWithAgentHeartbeat(rows).sort((left, right) =>
|
|
447
481
|
(right.lastUsedAt || right.updatedAt || 0) - (left.lastUsedAt || left.updatedAt || 0));
|
|
448
482
|
}
|
|
449
483
|
|
|
450
|
-
function sessionFilesAreNewerThan(indexPath) {
|
|
451
|
-
const directory = join(dataDir(), 'sessions');
|
|
452
|
-
if (probePath(directory).state !== PROBE_PRESENT) return false;
|
|
453
|
-
const indexProbe = probePath(indexPath);
|
|
454
|
-
// Only a PROVABLY absent index makes the files newer by definition. An
|
|
455
|
-
// index that exists but cannot be stat'ed (EACCES/EIO) is still the
|
|
456
|
-
// authority we failed to consult — claiming "files are newer" there would
|
|
457
|
-
// replace the whole catalog on the strength of an IO error.
|
|
458
|
-
if (indexProbe.state === PROBE_ABSENT) return true;
|
|
459
|
-
if (indexProbe.state !== PROBE_PRESENT) return false;
|
|
460
|
-
const indexMtime = indexProbe.mtimeMs || 0;
|
|
461
|
-
let entries;
|
|
462
|
-
try { entries = readdirSync(directory); } catch { return false; }
|
|
463
|
-
for (const filename of entries) {
|
|
464
|
-
if (!filename.endsWith('.json')) continue;
|
|
465
|
-
// Absent (raced unlink) and unreadable both prove nothing about
|
|
466
|
-
// freshness: only an observed newer mtime does.
|
|
467
|
-
const probe = probePath(join(directory, filename));
|
|
468
|
-
if (probe.state === PROBE_PRESENT && (probe.mtimeMs || 0) > indexMtime) return true;
|
|
469
|
-
}
|
|
470
|
-
return false;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
484
|
export function listStoredSessionSummaries(options = {}) {
|
|
474
485
|
const heartbeatMtimes = sessionHeartbeatMtimes();
|
|
475
486
|
const indexPath = join(dataDir(), 'session-summaries.json');
|
|
@@ -478,6 +489,7 @@ export function listStoredSessionSummaries(options = {}) {
|
|
|
478
489
|
// an individual session file cannot be read.
|
|
479
490
|
const indexRowsById = new Map();
|
|
480
491
|
let indexRows = null;
|
|
492
|
+
let indexMtimeMs = 0;
|
|
481
493
|
const indexRead = readTextFile(indexPath);
|
|
482
494
|
if (indexRead.state === PROBE_PRESENT) {
|
|
483
495
|
try {
|
|
@@ -490,25 +502,42 @@ export function listStoredSessionSummaries(options = {}) {
|
|
|
490
502
|
indexRows = leadRowsWithAgentHeartbeat(normalizedRows)
|
|
491
503
|
.sort((left, right) =>
|
|
492
504
|
(right.lastUsedAt || right.updatedAt || 0) - (left.lastUsedAt || left.updatedAt || 0));
|
|
505
|
+
const indexProbe = probePath(indexPath);
|
|
506
|
+
if (indexProbe.state === PROBE_PRESENT) indexMtimeMs = indexProbe.mtimeMs || 0;
|
|
493
507
|
}
|
|
494
508
|
} catch { /* malformed sidecar: the files below are the authority */ }
|
|
495
509
|
}
|
|
496
510
|
// An index that EXISTS but is unreadable (EACCES/EIO) is neither missing
|
|
497
511
|
// nor empty: the scan below still runs, and when IT cannot enumerate
|
|
498
512
|
// either, the catalog reports nothing rather than inventing an empty truth.
|
|
499
|
-
const scan = () => scanSessionFiles(
|
|
500
|
-
|
|
513
|
+
const scan = (forceRead = false) => scanSessionFiles(
|
|
514
|
+
heartbeatMtimes,
|
|
515
|
+
indexRowsById,
|
|
516
|
+
{ indexMtimeMs, forceRead },
|
|
517
|
+
);
|
|
518
|
+
if (options.refreshFromStorage === true) return scan(true) ?? indexRows ?? [];
|
|
501
519
|
if (indexRows) {
|
|
502
|
-
|
|
503
|
-
//
|
|
504
|
-
//
|
|
505
|
-
//
|
|
506
|
-
|
|
507
|
-
return scan() ?? indexRows;
|
|
508
|
-
}
|
|
509
|
-
if (indexRows.length > 0 || options.rebuildIfMissing === false) return indexRows;
|
|
520
|
+
if (options.rebuildIfMissing === false) return indexRows;
|
|
521
|
+
// Enumerate identities to detect deletions, stat known files, and parse
|
|
522
|
+
// only rows newer than the index. Cold-start cost therefore scales with
|
|
523
|
+
// changed transcripts rather than total transcript bytes.
|
|
524
|
+
return scan(false) ?? indexRows;
|
|
510
525
|
}
|
|
511
|
-
|
|
526
|
+
// Missing/malformed index is the sole cold path that must rebuild every
|
|
527
|
+
// summary from canonical session bytes.
|
|
528
|
+
return options.rebuildIfMissing === false ? [] : (scan(true) ?? indexRows ?? []);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** Exact, fail-closed existence check for a durable session address.
|
|
532
|
+
* Summary indexes and desktop metadata are presentation caches; neither may
|
|
533
|
+
* make a missing sessions/<id>.json record addressable again. */
|
|
534
|
+
export function storedSessionExists(id) {
|
|
535
|
+
const sessionId = String(id || '').trim();
|
|
536
|
+
if (!/^[A-Za-z0-9_-]+$/.test(sessionId)) return false;
|
|
537
|
+
const read = readTextFile(join(dataDir(), 'sessions', `${sessionId}.json`));
|
|
538
|
+
if (read.state !== PROBE_PRESENT) return false;
|
|
539
|
+
const record = readTopLevelLifecycleRecord(read.text);
|
|
540
|
+
return !isLifecycleUnreadable(record) && record.id === sessionId;
|
|
512
541
|
}
|
|
513
542
|
|
|
514
543
|
/** Read exactly one persisted session for a visible desktop pane. This never
|
|
@@ -555,7 +584,7 @@ export async function readStoredSessionTranscript(id, options = {}) {
|
|
|
555
584
|
restoreTranscriptItems,
|
|
556
585
|
sessionContextSnapshotProjection,
|
|
557
586
|
} = await import(
|
|
558
|
-
'../../../../tui/
|
|
587
|
+
'../../../../tui/session/session-api-ext.mjs'
|
|
559
588
|
);
|
|
560
589
|
let preparedContextProjection = null;
|
|
561
590
|
try {
|
|
@@ -712,7 +741,7 @@ export async function createStoredSessionLiveViewer(id, options = {}) {
|
|
|
712
741
|
clearStreamingTail: () => commit({ streamingTail: null }),
|
|
713
742
|
};
|
|
714
743
|
const { createLiveShare, liveSharePipePath } = await import(
|
|
715
|
-
'../../../../tui/
|
|
744
|
+
'../../../../tui/session/live-share.mjs'
|
|
716
745
|
);
|
|
717
746
|
const share = createLiveShare({
|
|
718
747
|
ownerSessionId: () => '',
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
import { isInvalidToolArgsMarker, formatInvalidToolArgsResult } from '../providers/openai-compat-stream.mjs';
|
|
25
25
|
import {
|
|
26
26
|
_stripMcpPrefix, _isReadTool, _isMutationTool, _isScopedCacheableTool,
|
|
27
|
-
_isShellTool,
|
|
27
|
+
_isShellTool, _intraTurnSig,
|
|
28
28
|
} from './loop/tool-classify.mjs';
|
|
29
29
|
import { preDispatchDenyForSession } from './loop/pre-dispatch-deny.mjs';
|
|
30
30
|
import { executeTool } from './loop/tool-exec.mjs';
|
|
@@ -103,12 +103,6 @@ export async function processToolBatch(ctx) {
|
|
|
103
103
|
if (message?.toolCallId) _batchToolResultByCallId.set(message.toolCallId, message);
|
|
104
104
|
else _batchToolResultsWithoutId.push(message);
|
|
105
105
|
};
|
|
106
|
-
// Ordered-mutation batch gate. Each apply_patch is an eager-dispatch
|
|
107
|
-
// barrier: side effects overlap within the current segment, but no
|
|
108
|
-
// later segment starts before the loop crosses the patch in model
|
|
109
|
-
// order. A failed patch prevents every unstarted later side effect;
|
|
110
|
-
// known read-only calls may still complete for diagnostics.
|
|
111
|
-
let _orderedMutationFailed = null;
|
|
112
106
|
for (let callIndex = 0; callIndex < calls.length; callIndex += 1) {
|
|
113
107
|
const call = calls[callIndex];
|
|
114
108
|
if (isBuiltinTool(call.name)) {
|
|
@@ -202,22 +196,6 @@ export async function processToolBatch(ctx) {
|
|
|
202
196
|
continue;
|
|
203
197
|
}
|
|
204
198
|
}
|
|
205
|
-
// Ordered-mutation skip: an earlier apply_patch in THIS batch failed,
|
|
206
|
-
// so this later side-effect call is skipped rather than executed.
|
|
207
|
-
// Restore a later patch's full body first so it can be re-issued.
|
|
208
|
-
// `skipped` is deliberate: the call never started, so it is neither
|
|
209
|
-
// a tool failure nor a cancellation. The earlier patch remains the
|
|
210
|
-
// structural failure that drives retry/stop-hook behavior.
|
|
211
|
-
if (_orderedMutationFailed && _isOrderedGateSkippable(call.name) && !pending.has(call.id)) {
|
|
212
|
-
if (call?.id) restoreToolCallBodyForId(assistantTurnMsg, calls, call.id);
|
|
213
|
-
_stageToolResultMessage({
|
|
214
|
-
role: 'tool',
|
|
215
|
-
content: `[prerequisite-failed] an earlier apply_patch in this same tool batch (call index ${_orderedMutationFailed.index + 1}) failed, so this later \`${call.name}\` was skipped without starting. Re-issue it after resolving the patch failure.`,
|
|
216
|
-
toolCallId: call.id,
|
|
217
|
-
toolKind: 'skipped',
|
|
218
|
-
});
|
|
219
|
-
continue;
|
|
220
|
-
}
|
|
221
199
|
if (sessionId) markSessionToolCall(sessionId, call.name, resolveToolSelfDeadlineMs(call.name, call.arguments));
|
|
222
200
|
let result;
|
|
223
201
|
let toolStartedAt;
|
|
@@ -279,8 +257,9 @@ export async function processToolBatch(ctx) {
|
|
|
279
257
|
_executeOk = true;
|
|
280
258
|
} else {
|
|
281
259
|
// Fallback for providers that don't stream tool calls early:
|
|
282
|
-
//
|
|
283
|
-
//
|
|
260
|
+
// dispatch the whole remaining batch. The eager dispatcher
|
|
261
|
+
// keeps independent calls parallel while making shell wait
|
|
262
|
+
// for earlier apply_patch calls.
|
|
284
263
|
if (isParallelDispatchable(call.name)) {
|
|
285
264
|
startEagerRun(calls, callIndex, _duplicateCallIds);
|
|
286
265
|
}
|
|
@@ -512,14 +491,6 @@ export async function processToolBatch(ctx) {
|
|
|
512
491
|
}
|
|
513
492
|
if (_isMutationTool(call.name)) {
|
|
514
493
|
epoch.mutation += 1;
|
|
515
|
-
// Record the first failed ordered mutation in this batch so any
|
|
516
|
-
// later side effect is skipped by the gate at the top of the
|
|
517
|
-
// loop. Keyed on exec outcome (not post-processing): a mutation
|
|
518
|
-
// whose write succeeded but post-processing threw still landed
|
|
519
|
-
// on disk, so it must NOT block subsequent ordered patches.
|
|
520
|
-
if ((!_executeOk || _resultKind === 'error') && !_orderedMutationFailed) {
|
|
521
|
-
_orderedMutationFailed = { index: callIndex, callId: call.id, name: call.name };
|
|
522
|
-
}
|
|
523
494
|
}
|
|
524
495
|
// Bash always clears scoped cache UNCONDITIONALLY — a mutating bash
|
|
525
496
|
// that throws or fails partway can still leave stale find_symbol / grep entries.
|
|
@@ -52,11 +52,10 @@ function maybeSweepStaleOffloadDirs() {
|
|
|
52
52
|
void sweepStaleOffloadDirs();
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
// Per-tool persistence limits
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
// (CC BASH_MAX_OUTPUT_LENGTH default). Read/head/tail/diff stay inline
|
|
55
|
+
// Per-tool persistence limits are per-tool maxResultSizeChars values rather
|
|
56
|
+
// than a single global value: grep persists at 20k, glob and list/find_* at
|
|
57
|
+
// the 50k system default (deliberately tighter than the common 100k), and
|
|
58
|
+
// shell/bash_session/task at 30k. Read/head/tail/diff stay inline
|
|
60
59
|
// (Infinity) — they are self-bound by FileRead semantics and the upstream
|
|
61
60
|
// READ_MAX_SIZE_BYTES cap, so persisting to a sidecar to be re-read would be
|
|
62
61
|
// circular. These values keep context-rich IO tools from turning into "read
|
|
@@ -3,7 +3,7 @@ import { getHiddenAgent } from './internal-agents.mjs';
|
|
|
3
3
|
const SECOND_MS = 1000;
|
|
4
4
|
const MIN_PROVIDER_TIMEOUT_MS = 30_000;
|
|
5
5
|
|
|
6
|
-
const STALL_TICK_MS = 15_000;
|
|
6
|
+
export const STALL_TICK_MS = 15_000;
|
|
7
7
|
const DEFAULT_STALL_WARN_S = 300;
|
|
8
8
|
const DEFAULT_STALL_ABORT_S = 600;
|
|
9
9
|
// First-byte (no-stream-delta) abort for the agent stall watchdog. A wedged
|
|
@@ -65,7 +65,7 @@ const MAX_TIMEOUT_MS = Math.max(_envMaxTimeout > 0 ? _envMaxTimeout : 600_000, D
|
|
|
65
65
|
// JS setTimeout / PS WaitForExit(ms) are 32-bit: a delay above 2^31-1 wraps and
|
|
66
66
|
// fires immediately. Hard ceiling (~24.8 days) for an uncapped explicit timeout.
|
|
67
67
|
const TIMER_MAX_MS = 2_147_483_647;
|
|
68
|
-
const IDLE_TIMEOUT_MS =
|
|
68
|
+
const IDLE_TIMEOUT_MS = 2 * 60_000;
|
|
69
69
|
const MAX_SESSIONS = 10;
|
|
70
70
|
const STDERR_DRAIN_MS = 25;
|
|
71
71
|
const STDERR_DRAIN_MAX_MS = 250;
|
|
@@ -364,7 +364,6 @@ function _spawnSession(id, initialCwd = process.cwd(), resourceLease = null) {
|
|
|
364
364
|
childPid: proc.pid,
|
|
365
365
|
childGroupPid: proc.pid,
|
|
366
366
|
label: 'bash-session',
|
|
367
|
-
protectHostMemory: true,
|
|
368
367
|
});
|
|
369
368
|
proc.stdout.setEncoding('utf-8');
|
|
370
369
|
proc.stderr.setEncoding('utf-8');
|
|
@@ -446,6 +445,7 @@ async function _getOrCreate(sessionId, initialCwd = process.cwd(), opts = {}) {
|
|
|
446
445
|
signal: opts.signal || null,
|
|
447
446
|
label: `persistent:${id}`,
|
|
448
447
|
dependency: 'detached',
|
|
448
|
+
ownerKey: opts.sessionId || id,
|
|
449
449
|
});
|
|
450
450
|
try {
|
|
451
451
|
entry = _sessions.get(id);
|
|
@@ -849,7 +849,40 @@ const GUARDS = {
|
|
|
849
849
|
// key instead of failing the call on a field the caller never set.
|
|
850
850
|
function dropUndefinedArgs(args) {
|
|
851
851
|
for (const key of Object.keys(args)) {
|
|
852
|
-
|
|
852
|
+
// `null` gets the same treatment: it is the only way JSON can express
|
|
853
|
+
// "this optional field was not set", and failing the whole call on it
|
|
854
|
+
// was measured as a recurring, fully recoverable tool error.
|
|
855
|
+
if (args[key] === undefined || args[key] === null) delete args[key];
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// string | string[] arguments arrive nested ([["a","b"]] from a batching
|
|
860
|
+
// wrapper), padded with null/empty entries, or empty. The intent is
|
|
861
|
+
// unambiguous, so flatten one level, drop the blanks, and remove a key that
|
|
862
|
+
// ends up empty (the tool then uses its default scope) instead of rejecting
|
|
863
|
+
// the call.
|
|
864
|
+
const STRING_LIST_ARG_KEYS = [
|
|
865
|
+
'path', 'paths', 'root', 'file', 'files',
|
|
866
|
+
'pattern', 'patterns', 'query', 'regex', 'needle',
|
|
867
|
+
'glob', 'file_pattern', 'include', 'type', 'symbols',
|
|
868
|
+
];
|
|
869
|
+
function normalizeStringListArgs(args) {
|
|
870
|
+
for (const key of STRING_LIST_ARG_KEYS) {
|
|
871
|
+
if (!hasOwn(args, key) || !Array.isArray(args[key])) continue;
|
|
872
|
+
const source = args[key];
|
|
873
|
+
// Flatten ONE nesting level and drop blank entries; every other entry
|
|
874
|
+
// type is left untouched so the per-tool guards still see (and coerce
|
|
875
|
+
// or reject) it — `read`'s {path,offset,limit}[] regions must survive
|
|
876
|
+
// this pass verbatim.
|
|
877
|
+
const flat = [];
|
|
878
|
+
for (const entry of source.flat(1)) {
|
|
879
|
+
if (entry === null || entry === undefined) continue;
|
|
880
|
+
if (typeof entry === 'string' && entry.trim().length === 0) continue;
|
|
881
|
+
flat.push(entry);
|
|
882
|
+
}
|
|
883
|
+
if (flat.length === source.length && flat.every((entry, idx) => entry === source[idx])) continue;
|
|
884
|
+
if (flat.length === 0) delete args[key];
|
|
885
|
+
else args[key] = flat;
|
|
853
886
|
}
|
|
854
887
|
}
|
|
855
888
|
|
|
@@ -863,6 +896,7 @@ export function validateBuiltinArgs(toolName, args) {
|
|
|
863
896
|
return `Error: ${toolName} arguments must be an object (got ${describeType(args)})`;
|
|
864
897
|
}
|
|
865
898
|
dropUndefinedArgs(args);
|
|
899
|
+
normalizeStringListArgs(args);
|
|
866
900
|
if (toolName === 'grep') applyGrepContextLeadPolicy(args);
|
|
867
901
|
try {
|
|
868
902
|
return guard(args) || null;
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { getAbortSignalForSession } from '../../session/abort-lookup.mjs';
|
|
2
|
+
import { unlinkSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join as pathJoin } from 'node:path';
|
|
5
|
+
import { isLegitimateShellExit } from '../../session/result-classification.mjs';
|
|
6
|
+
import { makeToolEnvelope } from '../../session/tool-envelope.mjs';
|
|
2
7
|
import { acquireShellLeaseBounded, execShellCommand, stripAnsi } from '../shell-command.mjs';
|
|
3
8
|
import { wrapCommandWithSnapshot } from '../shell-snapshot.mjs';
|
|
4
9
|
import { getDestructiveCommandWarning } from '../destructive-warning.mjs';
|
|
@@ -27,7 +32,9 @@ import {
|
|
|
27
32
|
detectLongForegroundReason,
|
|
28
33
|
extractShellApplyPatchInvocation,
|
|
29
34
|
foregroundLongCommandHint,
|
|
35
|
+
hasPowerShellOnlySyntax,
|
|
30
36
|
isAutobackgroundingAllowed,
|
|
37
|
+
planInlineScriptHoist,
|
|
31
38
|
preflightPowerShellHygiene,
|
|
32
39
|
shellSplitSegments,
|
|
33
40
|
shellSplitPipelineSegments,
|
|
@@ -89,10 +96,12 @@ export function _trackedDriftNoteAfter(_scope, _pre) {
|
|
|
89
96
|
|
|
90
97
|
// Search-style commands and `git diff --exit-code` use exit 1 as a SIGNAL
|
|
91
98
|
// (no match / has diff), not a failure. Benign ONLY when exitCode===1, no
|
|
92
|
-
// signal, stderr blank, AND the
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
// can
|
|
99
|
+
// signal, stderr blank, AND the exit status provably comes from a search-style
|
|
100
|
+
// stage: the LAST segment of a `;`/`&&` chain (its status IS the chain's) whose
|
|
101
|
+
// last pipeline stage is a search head. `||` chains stay ambiguous (either
|
|
102
|
+
// branch can supply the status) and stay Error. Quote/comment aware via the
|
|
103
|
+
// shared shell tokenizers, so quoted/commented `;` `|` `grep` can never
|
|
104
|
+
// masquerade as a connector/command and hide a real failure.
|
|
96
105
|
const _SEARCH_HEADS = new Set(['select-string', 'sls', 'grep', 'egrep', 'fgrep', 'findstr']);
|
|
97
106
|
const _GIT_GLOBAL_VALUE_OPTS = new Set(['-c', '-C', '--git-dir', '--work-tree', '--namespace', '--exec-path', '--config-env']);
|
|
98
107
|
// Command/process/subshell substitution or a backslash/backtick-escaped pipe
|
|
@@ -120,9 +129,11 @@ export function _isBenignSearchExitOne(command, exitCode, signal, stderr) {
|
|
|
120
129
|
const text = _stripShellComment(String(command || ''));
|
|
121
130
|
if (_AMBIGUOUS_SYNTAX.test(text)) return false; // subshell/subst/escaped pipe → ambiguous
|
|
122
131
|
const segments = shellSplitSegments(text);
|
|
123
|
-
if (segments.length
|
|
124
|
-
|
|
125
|
-
const
|
|
132
|
+
if (segments.length === 0) return false;
|
|
133
|
+
if (segments.length > 1 && /\|\|/.test(text)) return false; // || → which branch exited 1?
|
|
134
|
+
const lastSegment = segments[segments.length - 1];
|
|
135
|
+
const stages = shellSplitPipelineSegments(lastSegment);
|
|
136
|
+
const last = stages[stages.length - 1] || lastSegment;
|
|
126
137
|
const raw = shellTokenize(last);
|
|
127
138
|
if (!raw) return false; // unbalanced quotes
|
|
128
139
|
const tokens = stripShellProbeWrappers(raw);
|
|
@@ -208,6 +219,14 @@ export function formatShellToolFailure(message) {
|
|
|
208
219
|
return `Error: [shell-tool-failed] ${text}`;
|
|
209
220
|
}
|
|
210
221
|
|
|
222
|
+
// A legitimate non-zero exit keeps its `[exit code: N]` marker but must not be
|
|
223
|
+
// CLASSIFIED as a failure downstream — the marker alone would read as an error
|
|
224
|
+
// to classifyResultKind. The explicit-success envelope is the documented
|
|
225
|
+
// channel for output that looks like an error and is not one.
|
|
226
|
+
function _finalizeShellResult(legitExit, text) {
|
|
227
|
+
return legitExit ? makeToolEnvelope(text, [], { explicitSuccess: true }) : text;
|
|
228
|
+
}
|
|
229
|
+
|
|
211
230
|
export function _shellFailureStatus(result, timeout) {
|
|
212
231
|
// Prefer the signal reported by the process. `killed` is only a fallback
|
|
213
232
|
// for platforms (notably taskkill on Windows) that close without one.
|
|
@@ -324,7 +343,7 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
324
343
|
// null spec means it is genuinely not installed — surface a clear error with
|
|
325
344
|
// NO silent fallback to the other shell.
|
|
326
345
|
const shellKind = args.shell === 'bash' || args.shell === 'powershell' ? args.shell : 'default';
|
|
327
|
-
|
|
346
|
+
let resolvedSpec = resolveShellFor(shellKind);
|
|
328
347
|
if (!resolvedSpec) {
|
|
329
348
|
if (shellKind === 'bash') {
|
|
330
349
|
return formatShellToolFailure("Git Bash not found — install Git for Windows or omit shell:'bash'.");
|
|
@@ -351,13 +370,52 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
351
370
|
shellType: resolvedSpec.shellType,
|
|
352
371
|
shellName: resolvedSpec.shell,
|
|
353
372
|
});
|
|
354
|
-
|
|
355
|
-
|
|
373
|
+
let shellRescueNote = '';
|
|
374
|
+
if (psHygiene.block) {
|
|
375
|
+
// Auto-rescue: the block fired ONLY because the command is written in
|
|
376
|
+
// bash (unix filter heads, `&&`) and it carries no PowerShell-only
|
|
377
|
+
// construct — running it in Git Bash is exactly what the caller meant,
|
|
378
|
+
// byte-for-byte. A mixed command ($env:, cmdlets, `2>$null`) or a
|
|
379
|
+
// PowerShell-specific violation stays a hard block, and an explicit
|
|
380
|
+
// shell:'powershell' is never overridden.
|
|
381
|
+
const bashSpec = psHygiene.bashOnly
|
|
382
|
+
&& shellKind === 'default'
|
|
383
|
+
&& !hasPowerShellOnlySyntax(psHygiene.original ?? command)
|
|
384
|
+
? resolveShellFor('bash')
|
|
385
|
+
: null;
|
|
386
|
+
if (!bashSpec) return formatShellToolFailure(psHygiene.block);
|
|
387
|
+
// The MSYS rewrite targets PowerShell; bash gets the original text.
|
|
388
|
+
command = psHygiene.original ?? command;
|
|
389
|
+
resolvedSpec = bashSpec;
|
|
390
|
+
shellRescueNote = "note: bash-only syntax on a PowerShell host — ran it in Git Bash (pass shell:'bash' to make that explicit).";
|
|
391
|
+
} else {
|
|
392
|
+
command = psHygiene.command;
|
|
393
|
+
}
|
|
356
394
|
|
|
357
395
|
const _execPolicyBlock = checkExecPolicyMessage(command);
|
|
358
396
|
if (_execPolicyBlock) {
|
|
359
397
|
return formatShellToolFailure(_execPolicyBlock);
|
|
360
398
|
}
|
|
399
|
+
// Inline-script hoisting (sync runs only — a background job would outlive
|
|
400
|
+
// the temp file). The body is written verbatim and the invocation becomes a
|
|
401
|
+
// file run, so the host shell never has to carry the script through its
|
|
402
|
+
// quoting layer. planInlineScriptHoist refuses every case where file
|
|
403
|
+
// semantics would differ, so this is a transport change only.
|
|
404
|
+
let _inlineHoistPath = null;
|
|
405
|
+
if (!runInBackground && args.persistent !== true) {
|
|
406
|
+
const hoist = planInlineScriptHoist(command);
|
|
407
|
+
if (hoist) {
|
|
408
|
+
try {
|
|
409
|
+
const file = pathJoin(
|
|
410
|
+
tmpdir(),
|
|
411
|
+
`mixdog-inline-${process.pid}-${Date.now().toString(36)}${hoist.extension}`,
|
|
412
|
+
);
|
|
413
|
+
writeFileSync(file, hoist.body, 'utf8');
|
|
414
|
+
_inlineHoistPath = file;
|
|
415
|
+
command = hoist.replace(file.replace(/\\/g, '/'));
|
|
416
|
+
} catch { _inlineHoistPath = null; }
|
|
417
|
+
}
|
|
418
|
+
}
|
|
361
419
|
|
|
362
420
|
// Sleep-chain auto-promotion: a leading `sleep N && …` / `Start-Sleep N; …`
|
|
363
421
|
// used to be DENIED preflight (CC detectBlockedSleepPattern parity).
|
|
@@ -448,19 +506,24 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
448
506
|
{ backgroundTasksDisabled: _bgTasksDisabled },
|
|
449
507
|
);
|
|
450
508
|
if (longForegroundHint) return formatShellToolFailure(longForegroundHint);
|
|
451
|
-
//
|
|
452
|
-
//
|
|
453
|
-
//
|
|
454
|
-
//
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
//
|
|
458
|
-
|
|
459
|
-
const _autoBgEnvMs = Number(
|
|
460
|
-
const DEFAULT_AUTO_BACKGROUND_MS =
|
|
509
|
+
// Main-agent blocking budget. A timeout is the command's total deadline,
|
|
510
|
+
// not permission to hold the conversation open for that whole duration:
|
|
511
|
+
// after 15 s a still-running command becomes a tracked background task and
|
|
512
|
+
// completion is pushed to the owner. Explicit timeouts keep their remaining
|
|
513
|
+
// deadline after promotion. Never applies to run_in_background (already
|
|
514
|
+
// detached), persistent sessions, or commands barred from backgrounding.
|
|
515
|
+
// MIXDOG_SHELL_AUTO_BACKGROUND_MS overrides; an explicit 0 disables.
|
|
516
|
+
const _autoBgEnvRaw = process.env.MIXDOG_SHELL_AUTO_BACKGROUND_MS;
|
|
517
|
+
const _autoBgEnvMs = Number(_autoBgEnvRaw);
|
|
518
|
+
const DEFAULT_AUTO_BACKGROUND_MS = (_autoBgEnvRaw != null && String(_autoBgEnvRaw).trim() !== ''
|
|
519
|
+
&& Number.isFinite(_autoBgEnvMs) && _autoBgEnvMs >= 0)
|
|
461
520
|
? Math.floor(_autoBgEnvMs)
|
|
462
|
-
:
|
|
463
|
-
|
|
521
|
+
: 15_000;
|
|
522
|
+
// Gate on backgroundOnTimeout rather than just `runInBackground`: it already
|
|
523
|
+
// encodes "promotion is available for this command" (not detached, background
|
|
524
|
+
// tasks enabled, command shape allows it), so the soft threshold can never
|
|
525
|
+
// promote something the hard timeout would refuse to.
|
|
526
|
+
const autoBackgroundMs = (!backgroundOnTimeout || DEFAULT_AUTO_BACKGROUND_MS <= 0)
|
|
464
527
|
? 0
|
|
465
528
|
: Math.min(DEFAULT_AUTO_BACKGROUND_MS, timeout);
|
|
466
529
|
|
|
@@ -507,6 +570,7 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
507
570
|
abortSignal: combinedAsyncAbort.signal,
|
|
508
571
|
label: String(command).replace(/\s+/g, ' ').slice(0, 120),
|
|
509
572
|
dependency: 'detached',
|
|
573
|
+
ownerKey: options?.callerSessionId || options?.sessionId || null,
|
|
510
574
|
});
|
|
511
575
|
if (combinedAsyncAbort.signal?.aborted) {
|
|
512
576
|
throw combinedAsyncAbort.signal.reason || new Error('shell background task cancelled before spawn');
|
|
@@ -634,6 +698,10 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
634
698
|
// tracked background job (unlimited) instead of killing it.
|
|
635
699
|
backgroundOnTimeout,
|
|
636
700
|
promotedTimeoutMs,
|
|
701
|
+
// Soft/interrupt promotion happens before the foreground cap.
|
|
702
|
+
// Preserve an explicit total deadline by passing it separately;
|
|
703
|
+
// omitted-timeout promotions retain the existing unlimited job.
|
|
704
|
+
backgroundDeadlineMs: hasExplicitTimeout ? totalTimeout : 0,
|
|
637
705
|
// Threaded so an auto-backgrounded foreground job is stamped with
|
|
638
706
|
// the dispatching terminal's claude.exe pid (per-terminal scope).
|
|
639
707
|
clientHostPid: options?.clientHostPid,
|
|
@@ -704,7 +772,17 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
704
772
|
const failureStatus = _shellFailureStatus(result, timeout);
|
|
705
773
|
const { signal, exitCode, shellToolFailed } = failureStatus;
|
|
706
774
|
const benignExitOne = _isBenignSearchExitOne(command, exitCode, signal, stderr);
|
|
707
|
-
|
|
775
|
+
// Legitimate non-zero exit: the command printed its result and ended
|
|
776
|
+
// with 1 without writing a single byte to stderr (probes, reports,
|
|
777
|
+
// `git diff --exit-code`-shaped checks). It keeps its `[exit code: 1]`
|
|
778
|
+
// marker so the caller sees the code, but it is NOT framed as a tool
|
|
779
|
+
// error and NOT classified as a failure — measured as the dominant
|
|
780
|
+
// shape among non-zero exits.
|
|
781
|
+
const legitExit = !shellToolFailed
|
|
782
|
+
&& !benignExitOne
|
|
783
|
+
&& isLegitimateShellExit({ exitCode, signal, stdout, stderr });
|
|
784
|
+
const shellRunFailed = !shellToolFailed
|
|
785
|
+
&& (!!signal || (exitCode !== 0 && exitCode !== null && !benignExitOne && !legitExit));
|
|
708
786
|
const isReallyErrored = shellToolFailed || shellRunFailed;
|
|
709
787
|
// Filter-swallow rescue: the tee file is ALWAYS consumed (deleted)
|
|
710
788
|
// here; its tail is attached only when the run failed with an empty
|
|
@@ -729,8 +807,16 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
729
807
|
const statusDetail = failureStatus.statusDetail;
|
|
730
808
|
const statusMarker = shellToolFailed
|
|
731
809
|
? `[shell-tool-failed] ${statusDetail}`
|
|
732
|
-
: (shellRunFailed ? `[shell-run-failed] ${statusDetail}` : '');
|
|
810
|
+
: (shellRunFailed ? `[shell-run-failed] ${statusDetail}` : (legitExit ? statusDetail : ''));
|
|
733
811
|
const errorPrefix = isReallyErrored ? 'Error: ' : '';
|
|
812
|
+
// Three outcomes, three labels: a TOOL error (`[shell-tool-failed]`),
|
|
813
|
+
// a command that RAN AND FAILED (`Error: [shell-run-failed]`), and a
|
|
814
|
+
// command that RAN TO COMPLETION and merely ended non-zero. The last
|
|
815
|
+
// one keeps its exit code but says so in words, so neither the model
|
|
816
|
+
// nor the failure log treats a finished report as a failure.
|
|
817
|
+
const completionNote = legitExit
|
|
818
|
+
? '\n[completed: the command finished and wrote nothing to stderr; the exit code is its own report, not a tool failure]'
|
|
819
|
+
: '';
|
|
734
820
|
if (mergeStderr) {
|
|
735
821
|
// Post-exit concatenation. True chunk-level interleaving would
|
|
736
822
|
// require shell-level `2>&1` redirection (bash) or `*>&1`
|
|
@@ -740,7 +826,7 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
740
826
|
// cross-stream interleaving. Acceptable for most diagnostic
|
|
741
827
|
// outputs; flag in shell-command if exact interleaving is required.
|
|
742
828
|
const merged = stdout + stderr;
|
|
743
|
-
if (statusMarker) return _prependDestructiveWarning(command, errorPrefix + smartMiddleTruncate(`${statusMarker}\n\n${merged || '(no output)'}`) + _rescueNote + _driftNote);
|
|
829
|
+
if (statusMarker) return _finalizeShellResult(legitExit, _prependDestructiveWarning(command, errorPrefix + smartMiddleTruncate(`${statusMarker}${completionNote}\n\n${merged || '(no output)'}`) + _rescueNote + _driftNote));
|
|
744
830
|
return _prependDestructiveWarning(command, smartMiddleTruncate(merged || '(no output)') + _driftNote);
|
|
745
831
|
}
|
|
746
832
|
const truncatedStdout = smartMiddleTruncate(stdout);
|
|
@@ -761,13 +847,17 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
761
847
|
}
|
|
762
848
|
const warningBlock = [
|
|
763
849
|
wmicRewrite?.note || '',
|
|
850
|
+
shellRescueNote,
|
|
764
851
|
].filter(Boolean).join('\n');
|
|
765
852
|
const payload = `${body}${stderrBlock}${spillBlock}${_rescueNote}${_driftNote}`;
|
|
766
|
-
if (statusMarker) return _prependDestructiveWarning(command, _composeShellFailure(statusMarker
|
|
853
|
+
if (statusMarker) return _finalizeShellResult(legitExit, _prependDestructiveWarning(command, _composeShellFailure(`${statusMarker}${completionNote}`, errorPrefix, warningBlock, payload)));
|
|
767
854
|
return _prependDestructiveWarning(command, warningBlock ? `${warningBlock}\n${payload}` : payload);
|
|
768
855
|
}
|
|
769
856
|
finally {
|
|
770
857
|
combinedBashAbort?.cleanup?.();
|
|
858
|
+
if (_inlineHoistPath) {
|
|
859
|
+
try { unlinkSync(_inlineHoistPath); } catch { /* best-effort cleanup */ }
|
|
860
|
+
}
|
|
771
861
|
if (shellEffects.mutationMode === 'paths') {
|
|
772
862
|
invalidateBuiltinResultCache(shellEffects.paths);
|
|
773
863
|
markCodeGraphDirtyPaths(shellEffects.paths);
|