mixdog 0.9.147 → 0.9.148
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/package.json +23 -3
- package/scripts/build-voice-runtime-unix.sh +101 -0
- package/scripts/embedding-quality-probe.mjs +96 -0
- package/scripts/generate-voice-runtime-manifest.mjs +300 -0
- package/scripts/generate-voice-runtime-manifest.test.mjs +135 -0
- package/scripts/native-binary-arch.mjs +30 -13
- package/scripts/native-binary-arch.test.mjs +40 -4
- package/scripts/plan-voice-runtime-release.mjs +73 -0
- package/scripts/prepare-native-assets.mjs +50 -9
- package/scripts/prune-embedding-runtime.mjs +3 -3
- package/scripts/sync-voice-runtime-manifest.mjs +46 -0
- package/scripts/verify-release-assets.mjs +106 -6
- package/scripts/verify-voice-runtime-release.mjs +89 -0
- package/scripts/voice-runtime-config.json +52 -0
- package/src/app.mjs +42 -5
- package/src/headless-exec.mjs +58 -32
- package/src/headless-exec.test.mjs +74 -13
- package/src/rules/lead/01-general.md +2 -0
- package/src/rules/shared/10-tool-workflow.md +7 -4
- package/src/rules/shared/30-exploration.md +2 -1
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +28 -9
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +6 -1
- package/src/runtime/agent/orchestrator/config.mjs +53 -18
- package/src/runtime/agent/orchestrator/mcp/cancellation.test.mjs +139 -0
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +34 -5
- package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -10
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +5 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +98 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.test.mjs +90 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +67 -16
- package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +4 -82
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +0 -5
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +62 -121
- package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +48 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.test.mjs +215 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +44 -48
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +54 -31
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +56 -2
- package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +34 -2
- package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +61 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +61 -15
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +120 -1
- package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +0 -3
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +187 -84
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +36 -36
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +16 -6
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +50 -213
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +135 -126
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +17 -11
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +20 -7
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +132 -42
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +28 -10
- package/src/runtime/agent/orchestrator/providers/registry.mjs +122 -95
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +58 -3
- package/src/runtime/agent/orchestrator/providers/tool-stream-state.mjs +4 -22
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +7 -14
- package/src/runtime/agent/orchestrator/session/compact-policy.test.mjs +128 -0
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +27 -48
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +19 -14
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -43
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -2
- package/src/runtime/agent/orchestrator/session/manager/pending-messages-persistence.test.mjs +325 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +599 -195
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +22 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +9 -2
- package/src/runtime/agent/orchestrator/session/manager.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +26 -16
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +23 -11
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +118 -3
- package/src/runtime/agent/orchestrator/session/store-summary-reader.test.mjs +332 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +38 -1
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +27 -6
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -20
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +291 -128
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +45 -8
- package/src/runtime/agent/orchestrator/tools/builtin/edit-encoding-eol.test.mjs +224 -0
- package/src/runtime/agent/orchestrator/tools/builtin/edit-sequential-occupation.test.mjs +53 -3
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +255 -25
- package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +87 -0
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +132 -9
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +29 -3
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +178 -29
- package/src/runtime/agent/orchestrator/tools/builtin/local-search-telemetry.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +32 -5
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-health.test.mjs +5 -2
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +10 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +38 -10
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +31 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +11 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-windows.mjs +22 -4
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +6 -8
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +25 -24
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +152 -133
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +114 -42
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +129 -15
- package/src/runtime/agent/orchestrator/tools/code-graph/loose-file-target.test.mjs +72 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +7 -8
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +201 -19
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +26 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +10 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-search-scope.test.mjs +55 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +115 -25
- package/src/runtime/agent/orchestrator/tools/code-graph/text-span-mask.test.mjs +268 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +296 -46
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.test.mjs +104 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +136 -0
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +127 -38
- package/src/runtime/agent/orchestrator/tools/patch/engine-contract-gate.test.mjs +661 -0
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +350 -9
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +418 -44
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +27 -3
- package/src/runtime/agent/orchestrator/tools/patch/patch-codec-eol.test.mjs +552 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +65 -15
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +284 -109
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +51 -21
- package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +44 -27
- package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
- package/src/runtime/attachments/store.mjs +11 -3
- package/src/runtime/channels/data/voice-runtime-manifest.json +46 -70
- package/src/runtime/channels/lib/event-queue.mjs +51 -41
- package/src/runtime/channels/lib/scheduler.mjs +23 -28
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -17
- package/src/runtime/channels/lib/voice-runtime-fetcher.test.mjs +17 -0
- package/src/runtime/channels/lib/webhook.mjs +39 -20
- package/src/runtime/channels/lib/whisper-server.mjs +46 -1
- package/src/runtime/channels/lib/worker-main.mjs +3 -1
- package/src/runtime/media/adapters/gemini-video.mjs +20 -4
- package/src/runtime/media/jobs.mjs +13 -0
- package/src/runtime/media/renditions.mjs +7 -1
- package/src/runtime/media/store.mjs +42 -22
- package/src/runtime/memory/index.mjs +4 -9
- package/src/runtime/memory/lib/agent-ipc.mjs +1 -6
- package/src/runtime/memory/lib/archive-security.test.mjs +21 -1
- package/src/runtime/memory/lib/core-memory-store.mjs +28 -50
- package/src/runtime/memory/lib/embedding-model-config.mjs +37 -1
- package/src/runtime/memory/lib/embedding-model-config.test.mjs +33 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +34 -12
- package/src/runtime/memory/lib/embedding-worker.mjs +37 -5
- package/src/runtime/memory/lib/ko-morph.mjs +46 -15
- package/src/runtime/memory/lib/memory-action-handlers.mjs +18 -1
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -18
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +1 -18
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +98 -45
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +71 -0
- package/src/runtime/memory/lib/memory-cycle2-shared.test.mjs +78 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +37 -3
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory-embedding-migration.test.mjs +50 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +9 -24
- package/src/runtime/memory/lib/memory.mjs +50 -12
- package/src/runtime/memory/lib/pg/process.mjs +1 -10
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -9
- package/src/runtime/memory/lib/query-handlers.mjs +2 -4
- package/src/runtime/memory/lib/session-ingest-runtime.mjs +2 -2
- package/src/runtime/shared/background-tasks.mjs +15 -0
- package/src/runtime/shared/background-tasks.test.mjs +78 -0
- package/src/runtime/shared/child-guardian.mjs +2 -10
- package/src/runtime/shared/pid-liveness.mjs +27 -0
- package/src/runtime/shared/runtime-root.mjs +1 -11
- package/src/runtime/shared/service-discovery.mjs +1 -17
- package/src/runtime/shared/singleton-owner.mjs +1 -11
- package/src/runtime/shared/staged-update.mjs +2 -12
- package/src/runtime/shared/tool-status.mjs +5 -1
- package/src/runtime/shared/tool-surface.mjs +15 -7
- package/src/runtime/shared/turn-snapshot-store.mjs +168 -0
- package/src/runtime/shared/turn-snapshot.mjs +137 -13
- package/src/runtime/shared/turn-worktree-snapshot.mjs +110 -18
- package/src/runtime/shared/webhook-session-run.mjs +16 -2
- package/src/runtime/web-search/lib/http-fetch.mjs +34 -4
- package/src/runtime/web-search/lib/state.mjs +15 -15
- package/src/runtime/web-search/lib/web-tools.mjs +46 -5
- package/src/session-runtime/context-status.mjs +4 -12
- package/src/session-runtime/runtime-core.mjs +20 -1
- package/src/standalone/agent-dispatch-broker.mjs +33 -2
- package/src/standalone/agent-tool/job-task-reconcile.mjs +78 -0
- package/src/standalone/agent-tool/notify.mjs +3 -1
- package/src/standalone/agent-tool/spawn-flow.mjs +48 -66
- package/src/standalone/agent-tool/tag-registry.mjs +116 -33
- package/src/standalone/agent-tool/tool-def.mjs +2 -2
- package/src/standalone/agent-tool/worker-index.mjs +8 -11
- package/src/standalone/agent-tool/worker-rows.mjs +4 -1
- package/src/standalone/agent-tool.mjs +20 -55
- package/src/standalone/agent-watchdog-registry.mjs +28 -4
- package/src/standalone/channel-client.mjs +47 -15
- package/src/standalone/channel-transport.mjs +44 -25
- package/src/standalone/daemon.mjs +31 -2
- package/src/standalone/hook-bus/config.mjs +25 -9
- package/src/standalone/hook-bus/handlers.mjs +16 -52
- package/src/standalone/hook-bus.mjs +29 -23
- package/src/standalone/memory-runtime-proxy.mjs +1 -16
- package/src/standalone/session-client.mjs +86 -23
- package/src/standalone/session-protocol.mjs +0 -2
- package/src/standalone/session-runtime-host-health.test.mjs +5 -4
- package/src/standalone/session-runtime-host.mjs +101 -6
- package/src/standalone/session-runtime-record.mjs +1 -5
- package/src/standalone/session-runtime-worker.mjs +52 -57
- package/src/standalone/session-service.mjs +4 -1
- package/src/standalone/session-transport.mjs +56 -25
- package/src/tui/App.jsx +40 -49
- package/src/tui/app/app-view.jsx +14 -21
- package/src/tui/app/core-memory-picker.mjs +29 -16
- package/src/tui/app/create-app-pickers.mjs +15 -36
- package/src/tui/app/extension-pickers.mjs +60 -49
- package/src/tui/app/maintenance-pickers.mjs +100 -57
- package/src/tui/app/message-selector.mjs +6 -5
- package/src/tui/app/model-picker.mjs +30 -13
- package/src/tui/app/onboarding-steps.mjs +21 -12
- package/src/tui/app/panel-epoch.mjs +56 -0
- package/src/tui/app/panel-epoch.test.mjs +1009 -0
- package/src/tui/app/panel-surface.mjs +98 -0
- package/src/tui/app/project-picker.mjs +30 -29
- package/src/tui/app/prompt-submit.mjs +192 -97
- package/src/tui/app/prompt-submit.test.mjs +206 -0
- package/src/tui/app/provider-setup-picker.mjs +74 -43
- package/src/tui/app/resume-picker.mjs +11 -8
- package/src/tui/app/route-pickers.mjs +45 -28
- package/src/tui/app/settings-picker.mjs +69 -18
- package/src/tui/app/shell-layout.mjs +1 -3
- package/src/tui/app/slash-commands.mjs +0 -2
- package/src/tui/app/slash-dispatch.mjs +4 -2
- package/src/tui/app/text-entry-policy.mjs +25 -0
- package/src/tui/app/theme-effort-pickers.mjs +11 -16
- package/src/tui/app/usage-context-panels.mjs +21 -25
- package/src/tui/app/use-global-key-input.mjs +3 -2
- package/src/tui/app/use-prompt-draft-flow.mjs +6 -18
- package/src/tui/app/use-prompt-handlers.mjs +3 -2
- package/src/tui/app/use-welcome-prompt-hint.mjs +17 -3
- package/src/tui/components/Picker.jsx +1 -1
- package/src/tui/components/TextEntryPanel.jsx +7 -2
- package/src/tui/dist/index.mjs +652 -561
- package/src/tui/lib/voice-setup.mjs +1 -2
- package/src/tui/session/agent-job-feed.mjs +73 -11
- package/src/tui/session/session-api-ext.mjs +5 -6
- package/src/tui/session/session-api.mjs +1 -0
- package/src/tui/session/tool-approval.mjs +15 -5
- package/src/tui/session/tui-steering-persist.mjs +33 -19
- package/src/tui/session/tui-steering-persist.test.mjs +104 -0
- package/src/tui/session-local.mjs +0 -22
- package/scripts/.tmp-coverage-report.mjs +0 -96
- package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.mjs +0 -57
- package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.test.mjs +0 -57
|
@@ -14,22 +14,21 @@ import {
|
|
|
14
14
|
import {
|
|
15
15
|
completeBackgroundTask,
|
|
16
16
|
getBackgroundTask,
|
|
17
|
-
notifyBackgroundTaskProgress,
|
|
18
17
|
} from '../../../../shared/background-tasks.mjs';
|
|
19
18
|
import {
|
|
20
|
-
adoptNativeTaskByPid,
|
|
21
19
|
cancelNativeTask,
|
|
22
20
|
getNativeTask,
|
|
23
21
|
listNativeTasks,
|
|
22
|
+
promoteNativeTask,
|
|
23
|
+
releaseNativeTask,
|
|
24
24
|
setNativeTaskStartedAt,
|
|
25
|
-
startNativeTask,
|
|
26
25
|
subscribeNativeTask,
|
|
26
|
+
trackNativeForegroundTask,
|
|
27
27
|
waitNativeTask,
|
|
28
28
|
} from '../lib/native-spawn-client.mjs';
|
|
29
29
|
import {
|
|
30
30
|
attachJobInsights,
|
|
31
31
|
shellJobPublicTaskResult,
|
|
32
|
-
SHELL_JOB_OUTPUT_DISK_CAP,
|
|
33
32
|
} from './lib/shell-job-insights.mjs';
|
|
34
33
|
import {
|
|
35
34
|
completeShellJobRecord,
|
|
@@ -42,10 +41,6 @@ import {
|
|
|
42
41
|
renderShellCompletionEnvelope,
|
|
43
42
|
shellCompletionInstruction,
|
|
44
43
|
} from '../../../../shared/task-notification-envelope.mjs';
|
|
45
|
-
import {
|
|
46
|
-
resolveShellMonitorIntervalMs,
|
|
47
|
-
startShellMonitor,
|
|
48
|
-
} from './shell-monitor.mjs';
|
|
49
44
|
|
|
50
45
|
export { shellJobPublicTaskResult } from './lib/shell-job-insights.mjs';
|
|
51
46
|
|
|
@@ -81,7 +76,7 @@ function trackShellJobRecord(task, options) {
|
|
|
81
76
|
const settle = (detail) => {
|
|
82
77
|
if (!detail || detail.status === 'running') return;
|
|
83
78
|
try { unsubscribe?.(); } catch {}
|
|
84
|
-
void completeShellJobRecord(jobId, detail);
|
|
79
|
+
void completeShellJobRecord(jobId, normalizeShellJobDetail(detail));
|
|
85
80
|
};
|
|
86
81
|
unsubscribe = subscribeNativeTask(jobId, settle);
|
|
87
82
|
settle(getNativeTask(jobId) || task);
|
|
@@ -98,12 +93,66 @@ function releaseShellJobResourceLease(jobId) {
|
|
|
98
93
|
return true;
|
|
99
94
|
}
|
|
100
95
|
|
|
101
|
-
|
|
96
|
+
/** Native task status → background-task status. A non-zero exit is a COMMAND
|
|
97
|
+
* result, not a task failure: the identical run in the foreground reports
|
|
98
|
+
* `[exit code: N]` and stays a success, so crossing the promotion threshold
|
|
99
|
+
* must not flip it to failed. Only control-plane outcomes (spawn/server
|
|
100
|
+
* error, timeout kill, cancellation) are terminal failures. */
|
|
101
|
+
export function shellJobTaskStatus(detail) {
|
|
102
|
+
const status = typeof detail === 'string' ? detail : String(detail?.status || '');
|
|
103
|
+
if (status === 'running') return 'running';
|
|
102
104
|
if (status === 'completed') return 'completed';
|
|
103
105
|
if (status === 'cancelled') return 'cancelled';
|
|
106
|
+
// ONLY a real numeric non-zero exit normalizes. A signal death / null exit
|
|
107
|
+
// (SIGSEGV, SIGKILL, a lost process) is a genuine failure and must never
|
|
108
|
+
// be reported as success — `Number(null)` is 0, so an integer test on the
|
|
109
|
+
// coerced value silently turned every crash into a completed task.
|
|
110
|
+
const exitCode = (detail && typeof detail === 'object') ? detail.exitCode : null;
|
|
111
|
+
if (
|
|
112
|
+
status === 'failed'
|
|
113
|
+
&& detail && typeof detail === 'object'
|
|
114
|
+
&& !detail.timedOut
|
|
115
|
+
&& !detail.killed
|
|
116
|
+
&& !detail.error
|
|
117
|
+
&& !detail.signal
|
|
118
|
+
&& typeof exitCode === 'number'
|
|
119
|
+
&& Number.isInteger(exitCode)
|
|
120
|
+
&& exitCode !== 0
|
|
121
|
+
) return 'completed';
|
|
104
122
|
return 'failed';
|
|
105
123
|
}
|
|
106
124
|
|
|
125
|
+
/** Canonical shell-job detail. Every surface — wrapper task status, completion
|
|
126
|
+
* notification, public task result, disk record, insights summary — reads
|
|
127
|
+
* `status` from this one mapping, so a promoted non-zero command can never be
|
|
128
|
+
* reported completed by one surface and failed by another. The raw native
|
|
129
|
+
* value is kept as `nativeStatus` for diagnostics. */
|
|
130
|
+
export function normalizeShellJobDetail(detail) {
|
|
131
|
+
if (!detail || typeof detail !== 'object') return detail;
|
|
132
|
+
const nativeStatus = String(detail.status || '');
|
|
133
|
+
const status = shellJobTaskStatus(detail);
|
|
134
|
+
// `nativeStatus` belongs to the contract even when the mapping is identity
|
|
135
|
+
// (a signal death stays `failed`): every surface must be able to read the
|
|
136
|
+
// raw value the canonical status came from.
|
|
137
|
+
if (detail.status === status && detail.nativeStatus === nativeStatus) return detail;
|
|
138
|
+
return { ...detail, status, nativeStatus: detail.nativeStatus || nativeStatus };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Cause line for a failed job. The native side fills `error` only for its own
|
|
142
|
+
* control-plane failures, so a signal death or a status-less exit arrived with
|
|
143
|
+
* a null cause and the completion said "failed" while explaining nothing. */
|
|
144
|
+
export function shellJobFailureCause(detail) {
|
|
145
|
+
const explicit = String(detail?.error || '').trim();
|
|
146
|
+
if (explicit) return explicit;
|
|
147
|
+
const exitCode = typeof detail?.exitCode === 'number' ? detail.exitCode : null;
|
|
148
|
+
const exitDetail = exitCode == null ? '' : ` (exit ${exitCode})`;
|
|
149
|
+
if (detail?.timedOut) return `timed out before completing${exitDetail}`;
|
|
150
|
+
if (detail?.signal) return `terminated by signal ${detail.signal}${exitDetail}`;
|
|
151
|
+
if (detail?.killed) return `terminated before completing${exitDetail}`;
|
|
152
|
+
if (exitCode == null) return 'ended without an exit status';
|
|
153
|
+
return `exited with code ${exitCode}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
107
156
|
export function buildShellCompletion(jobId, detail) {
|
|
108
157
|
const startedAtMs = Date.parse(detail?.startedAt || '');
|
|
109
158
|
const finishedAtMs = Date.parse(detail?.finishedAt || '') || Date.now();
|
|
@@ -112,59 +161,34 @@ export function buildShellCompletion(jobId, detail) {
|
|
|
112
161
|
: null;
|
|
113
162
|
const exitCode = typeof detail?.exitCode === 'number' ? detail.exitCode : null;
|
|
114
163
|
const status = detail?.status || 'unknown';
|
|
164
|
+
const taskStatus = shellJobTaskStatus(detail);
|
|
165
|
+
// A failure always carries its cause — including a signal death, where the
|
|
166
|
+
// native task has no `error` of its own.
|
|
167
|
+
const failureCause = taskStatus === 'failed' ? shellJobFailureCause(detail) : null;
|
|
168
|
+
const reported = (failureCause && !detail?.error) ? { ...detail, error: failureCause } : detail;
|
|
115
169
|
const body = renderShellCompletionEnvelope({
|
|
116
170
|
jobId,
|
|
117
171
|
status,
|
|
118
172
|
exitCode,
|
|
119
173
|
elapsedMs,
|
|
120
174
|
command: detail?.command,
|
|
121
|
-
summary: detail?.summary,
|
|
175
|
+
summary: detail?.summary || failureCause || null,
|
|
122
176
|
stdoutPreview: detail?.stdoutPreview,
|
|
123
177
|
stderrPreview: detail?.stderrPreview,
|
|
124
178
|
mergeStderr: detail?.mergeStderr,
|
|
125
179
|
});
|
|
126
|
-
const taskStatus = shellJobTaskStatus(status);
|
|
127
180
|
return {
|
|
128
181
|
taskStatus,
|
|
129
182
|
body,
|
|
130
183
|
instruction: shellCompletionInstruction({ jobId, status, exitCode }),
|
|
131
|
-
result: shellJobPublicTaskResult(
|
|
132
|
-
error:
|
|
184
|
+
result: shellJobPublicTaskResult(reported),
|
|
185
|
+
error: failureCause,
|
|
133
186
|
};
|
|
134
187
|
}
|
|
135
188
|
|
|
136
|
-
function
|
|
137
|
-
return {
|
|
138
|
-
stdoutBytes: Math.max(0, Number(detail?.stdoutBytes) || 0),
|
|
139
|
-
stderrBytes: Math.max(0, Number(detail?.stderrBytes) || 0),
|
|
140
|
-
summary: String(detail?.summary || '').trim(),
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function renderShellMonitorProgress(jobId, detail, previous) {
|
|
145
|
-
const current = shellMonitorState(detail);
|
|
146
|
-
const startedAtMs = Date.parse(detail?.startedAt || '');
|
|
147
|
-
const elapsedMs = Number.isFinite(startedAtMs) ? Math.max(0, Date.now() - startedAtMs) : null;
|
|
148
|
-
const stdoutDelta = Math.max(0, current.stdoutBytes - previous.stdoutBytes);
|
|
149
|
-
const stderrDelta = Math.max(0, current.stderrBytes - previous.stderrBytes);
|
|
150
|
-
const latestChanged = current.summary && current.summary !== previous.summary;
|
|
151
|
-
const lines = [
|
|
152
|
-
'shell task progress',
|
|
153
|
-
`task_id: ${jobId}`,
|
|
154
|
-
'status: running',
|
|
155
|
-
elapsedMs == null ? null : `elapsed_ms: ${elapsedMs}`,
|
|
156
|
-
`stdout_bytes: ${current.stdoutBytes}`,
|
|
157
|
-
`stderr_bytes: ${current.stderrBytes}`,
|
|
158
|
-
stdoutDelta || stderrDelta ? `new_output_bytes: ${stdoutDelta + stderrDelta}` : null,
|
|
159
|
-
latestChanged ? '' : null,
|
|
160
|
-
latestChanged ? `latest: ${current.summary}` : null,
|
|
161
|
-
];
|
|
162
|
-
return { text: lines.filter((line) => line !== null).join('\n'), state: current };
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export async function waitForShellJob(jobId, { timeoutMs = 30_000 } = {}) {
|
|
189
|
+
export async function waitForShellJob(jobId, { timeoutMs = 30_000, signal = null } = {}) {
|
|
166
190
|
const started = Date.now();
|
|
167
|
-
const detail = await waitNativeTask(jobId, timeoutMs);
|
|
191
|
+
const detail = normalizeShellJobDetail(await waitNativeTask(jobId, timeoutMs, signal));
|
|
168
192
|
const withInsights = attachJobInsights(detail);
|
|
169
193
|
if (!withInsights) return null;
|
|
170
194
|
withInsights.waitedMs = Date.now() - started;
|
|
@@ -173,52 +197,53 @@ export async function waitForShellJob(jobId, { timeoutMs = 30_000 } = {}) {
|
|
|
173
197
|
}
|
|
174
198
|
|
|
175
199
|
export function peekShellJob(jobId) {
|
|
176
|
-
return attachJobInsights(getNativeTask(jobId));
|
|
200
|
+
return attachJobInsights(normalizeShellJobDetail(getNativeTask(jobId)));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** True while the job's process is still alive in this daemon. Owners of
|
|
204
|
+
* in-use artifacts consult it before a fallback cleanup fires. */
|
|
205
|
+
export function isShellJobRunning(jobId) {
|
|
206
|
+
return getNativeTask(String(jobId || ''))?.status === 'running';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Run `onSettled` once the job leaves 'running' (immediately when it already
|
|
210
|
+
* has). Owners of per-command artifacts (hoisted script files, tee captures,
|
|
211
|
+
* cache invalidation) use this instead of a fixed timer, which either fired
|
|
212
|
+
* under a still-running command or never at all. Returns an unsubscribe, or
|
|
213
|
+
* null when the job is unknown here — the caller then keeps its own fallback. */
|
|
214
|
+
export function subscribeShellJobSettled(jobId, onSettled) {
|
|
215
|
+
const key = String(jobId || '').trim();
|
|
216
|
+
if (!key || typeof onSettled !== 'function') return null;
|
|
217
|
+
const current = getNativeTask(key);
|
|
218
|
+
if (!current) return null;
|
|
219
|
+
if (current.status !== 'running') {
|
|
220
|
+
queueMicrotask(() => { try { onSettled(current); } catch {} });
|
|
221
|
+
return () => {};
|
|
222
|
+
}
|
|
223
|
+
let done = false;
|
|
224
|
+
let unsubscribe = () => {};
|
|
225
|
+
const fire = (task) => {
|
|
226
|
+
if (done || !task || task.status === 'running') return;
|
|
227
|
+
done = true;
|
|
228
|
+
try { unsubscribe(); } catch {}
|
|
229
|
+
try { onSettled(task); } catch {}
|
|
230
|
+
};
|
|
231
|
+
unsubscribe = subscribeNativeTask(key, fire);
|
|
232
|
+
return () => {
|
|
233
|
+
done = true;
|
|
234
|
+
try { unsubscribe(); } catch {}
|
|
235
|
+
};
|
|
177
236
|
}
|
|
178
237
|
|
|
179
238
|
export function refreshShellJob(jobId) {
|
|
180
|
-
return getNativeTask(jobId);
|
|
239
|
+
return normalizeShellJobDetail(getNativeTask(jobId));
|
|
181
240
|
}
|
|
182
241
|
|
|
183
242
|
export function killShellJob(jobId) {
|
|
184
243
|
const detail = cancelNativeTask(jobId);
|
|
185
244
|
if (!detail) return null;
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export async function startBackgroundShellJob(options = {}) {
|
|
190
|
-
const {
|
|
191
|
-
command,
|
|
192
|
-
timeoutMs,
|
|
193
|
-
workDir,
|
|
194
|
-
mergeStderr,
|
|
195
|
-
spawnEnv,
|
|
196
|
-
shell,
|
|
197
|
-
shellArg,
|
|
198
|
-
shellArgs,
|
|
199
|
-
shellType,
|
|
200
|
-
clientHostPid,
|
|
201
|
-
ownerSessionId,
|
|
202
|
-
} = options;
|
|
203
|
-
const argv = [
|
|
204
|
-
...(Array.isArray(shellArgs) && shellArgs.length > 0 ? shellArgs : [shellArg]),
|
|
205
|
-
command,
|
|
206
|
-
].filter((value) => value != null && String(value).length > 0);
|
|
207
|
-
const task = await startNativeTask({
|
|
208
|
-
program: shell,
|
|
209
|
-
argv,
|
|
210
|
-
cwd: workDir,
|
|
211
|
-
env: spawnEnv,
|
|
212
|
-
timeoutMs,
|
|
213
|
-
mergeStderr,
|
|
214
|
-
command,
|
|
215
|
-
shellType,
|
|
216
|
-
clientHostPid,
|
|
217
|
-
ownerSessionId,
|
|
218
|
-
outputLimit: SHELL_JOB_OUTPUT_DISK_CAP,
|
|
219
|
-
});
|
|
220
|
-
trackShellJobRecord(task, { ownerSessionId, clientHostPid });
|
|
221
|
-
return task;
|
|
245
|
+
const wasRunning = detail.status === 'running';
|
|
246
|
+
return { ...attachJobInsights(normalizeShellJobDetail(detail)), killed: wasRunning };
|
|
222
247
|
}
|
|
223
248
|
|
|
224
249
|
export function attachShellJobResourceLease(jobId, lease) {
|
|
@@ -262,13 +287,7 @@ export function cancelBackgroundShellJobWatch(jobId) {
|
|
|
262
287
|
return entry.notifyCtx || null;
|
|
263
288
|
}
|
|
264
289
|
|
|
265
|
-
export function
|
|
266
|
-
const entry = backgroundShellJobWatchers.get(jobId);
|
|
267
|
-
if (!entry || typeof entry.setMonitor !== 'function') return null;
|
|
268
|
-
return entry.setMonitor(intervalMs);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
export function watchBackgroundShellJob(jobId, notifyCtx, { monitorIntervalMs } = {}) {
|
|
290
|
+
export function watchBackgroundShellJob(jobId, notifyCtx) {
|
|
272
291
|
const ctx = notifyCtx && typeof notifyCtx.notifyFn === 'function'
|
|
273
292
|
? normalizeToolNotifyContext(notifyCtx)
|
|
274
293
|
: jobNotifyCtxByJobId.get(jobId);
|
|
@@ -276,53 +295,20 @@ export function watchBackgroundShellJob(jobId, notifyCtx, { monitorIntervalMs }
|
|
|
276
295
|
if (ctx) jobNotifyCtxByJobId.set(jobId, ctx);
|
|
277
296
|
let settled = false;
|
|
278
297
|
let unsubscribe = () => {};
|
|
279
|
-
let stopMonitor = () => false;
|
|
280
|
-
let previousMonitorState = shellMonitorState(attachJobInsights(getNativeTask(jobId)));
|
|
281
|
-
const setMonitor = (value) => {
|
|
282
|
-
stopMonitor();
|
|
283
|
-
const nextIntervalMs = resolveShellMonitorIntervalMs(value);
|
|
284
|
-
previousMonitorState = shellMonitorState(attachJobInsights(getNativeTask(jobId)));
|
|
285
|
-
const task = getBackgroundTask(jobId);
|
|
286
|
-
if (task) {
|
|
287
|
-
task.meta = {
|
|
288
|
-
...(task.meta && typeof task.meta === 'object' ? task.meta : {}),
|
|
289
|
-
monitor_interval_ms: nextIntervalMs,
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
stopMonitor = startShellMonitor({
|
|
293
|
-
intervalMs: nextIntervalMs,
|
|
294
|
-
onTick: () => {
|
|
295
|
-
const detail = attachJobInsights(getNativeTask(jobId));
|
|
296
|
-
if (!detail || detail.status !== 'running') return false;
|
|
297
|
-
const progress = renderShellMonitorProgress(jobId, detail, previousMonitorState);
|
|
298
|
-
const sent = notifyBackgroundTaskProgress(jobId, {
|
|
299
|
-
text: progress.text,
|
|
300
|
-
resultType: 'shell_task_progress',
|
|
301
|
-
status: 'running',
|
|
302
|
-
once: false,
|
|
303
|
-
});
|
|
304
|
-
if (sent) previousMonitorState = progress.state;
|
|
305
|
-
return true;
|
|
306
|
-
},
|
|
307
|
-
});
|
|
308
|
-
return nextIntervalMs;
|
|
309
|
-
};
|
|
310
298
|
const cancel = () => {
|
|
311
299
|
if (settled) return;
|
|
312
300
|
settled = true;
|
|
313
|
-
stopMonitor();
|
|
314
301
|
unsubscribe();
|
|
315
302
|
backgroundShellJobWatchers.delete(jobId);
|
|
316
303
|
};
|
|
317
304
|
const finish = (detail) => {
|
|
318
305
|
if (settled || !detail || detail.status === 'running') return;
|
|
319
306
|
settled = true;
|
|
320
|
-
stopMonitor();
|
|
321
307
|
unsubscribe();
|
|
322
308
|
backgroundShellJobWatchers.delete(jobId);
|
|
323
309
|
jobNotifyCtxByJobId.delete(jobId);
|
|
324
310
|
releaseShellJobResourceLease(jobId);
|
|
325
|
-
const enriched = attachJobInsights(detail);
|
|
311
|
+
const enriched = attachJobInsights(normalizeShellJobDetail(detail));
|
|
326
312
|
const completion = buildShellCompletion(jobId, enriched);
|
|
327
313
|
const completedTask = completeBackgroundTask(jobId, {
|
|
328
314
|
status: completion.taskStatus,
|
|
@@ -357,7 +343,9 @@ export function watchBackgroundShellJob(jobId, notifyCtx, { monitorIntervalMs }
|
|
|
357
343
|
visible = modelVisibleToolCompletionMessage(`${message}\n\n(no output)`, meta);
|
|
358
344
|
}
|
|
359
345
|
if (!visible) return false;
|
|
360
|
-
return enqueuePendingMessage(sessionId, markCompletionEntry(visible
|
|
346
|
+
return enqueuePendingMessage(sessionId, markCompletionEntry(visible, {
|
|
347
|
+
executionId: meta?.execution_id,
|
|
348
|
+
})) > 0;
|
|
361
349
|
},
|
|
362
350
|
logPrefix: 'shell-jobs',
|
|
363
351
|
});
|
|
@@ -365,23 +353,41 @@ export function watchBackgroundShellJob(jobId, notifyCtx, { monitorIntervalMs }
|
|
|
365
353
|
};
|
|
366
354
|
unsubscribe = subscribeNativeTask(jobId, finish);
|
|
367
355
|
if (!settled) {
|
|
368
|
-
|
|
369
|
-
backgroundShellJobWatchers.set(jobId, { cancel, notifyCtx: ctx, setMonitor });
|
|
356
|
+
backgroundShellJobWatchers.set(jobId, { cancel, notifyCtx: ctx });
|
|
370
357
|
}
|
|
371
358
|
queueMicrotask(() => finish(getNativeTask(jobId)));
|
|
372
359
|
}
|
|
373
360
|
|
|
374
|
-
export async function
|
|
361
|
+
export async function trackForegroundShellJob({
|
|
362
|
+
command,
|
|
363
|
+
cwd,
|
|
364
|
+
child,
|
|
365
|
+
jobId,
|
|
366
|
+
clientHostPid,
|
|
367
|
+
ownerSessionId,
|
|
368
|
+
}) {
|
|
369
|
+
return trackNativeForegroundTask({
|
|
370
|
+
child,
|
|
371
|
+
jobId,
|
|
372
|
+
command,
|
|
373
|
+
cwd,
|
|
374
|
+
ownerSessionId,
|
|
375
|
+
clientHostPid,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export async function promoteForegroundShellJob({
|
|
375
380
|
command,
|
|
376
381
|
cwd,
|
|
377
382
|
pid,
|
|
383
|
+
jobId,
|
|
378
384
|
timeoutMs,
|
|
379
385
|
clientHostPid,
|
|
380
386
|
ownerSessionId,
|
|
381
387
|
startedAtMs = 0,
|
|
382
388
|
}) {
|
|
383
|
-
const native = await
|
|
384
|
-
|
|
389
|
+
const native = await promoteNativeTask({
|
|
390
|
+
jobId,
|
|
385
391
|
command,
|
|
386
392
|
cwd,
|
|
387
393
|
timeoutMs,
|
|
@@ -390,8 +396,8 @@ export async function adoptForegroundShellJob({
|
|
|
390
396
|
});
|
|
391
397
|
if (!native) return null;
|
|
392
398
|
demoteBackgroundShellPriority(pid);
|
|
393
|
-
//
|
|
394
|
-
// carries its process-creation time. Neither is when this command started,
|
|
399
|
+
// Promotion refreshes the task at transition time, and a promoted standby
|
|
400
|
+
// shell carries its process-creation time. Neither is when this command started,
|
|
395
401
|
// so the runner's own start moment is registered before anything reads the
|
|
396
402
|
// task — the record, the completion elapsed and the live readouts then all
|
|
397
403
|
// measure the command itself.
|
|
@@ -476,7 +482,9 @@ export async function reconcileRecoveredShellJobCompletions() {
|
|
|
476
482
|
enqueueFallback: (sessionId, message, meta) => {
|
|
477
483
|
const visible = modelVisibleToolCompletionMessage(message, meta);
|
|
478
484
|
if (!visible) return false;
|
|
479
|
-
return enqueuePendingMessage(sessionId, markCompletionEntry(visible
|
|
485
|
+
return enqueuePendingMessage(sessionId, markCompletionEntry(visible, {
|
|
486
|
+
executionId: meta?.execution_id,
|
|
487
|
+
})) > 0;
|
|
480
488
|
},
|
|
481
489
|
logPrefix: 'shell-jobs-recovery',
|
|
482
490
|
});
|
|
@@ -507,6 +515,17 @@ export async function shutdownShellJobs(_reason = 'runtime-close', { scope = nul
|
|
|
507
515
|
jobs.map((task) => waitNativeTask(task.jobId, 1_200).catch(() => null)),
|
|
508
516
|
);
|
|
509
517
|
for (const task of jobs) releaseShellJobResourceLease(task.jobId);
|
|
518
|
+
if (scoped) {
|
|
519
|
+
// The owning session is gone: nothing can query its tasks again, so
|
|
520
|
+
// drop ALL of its retained state — not only the ones that happened to
|
|
521
|
+
// be running when shutdown began. releaseNativeTask refuses a task
|
|
522
|
+
// that is still alive, and disk records still serve a post-restart
|
|
523
|
+
// recovery read.
|
|
524
|
+
for (const task of listNativeTasks()) {
|
|
525
|
+
if (String(task.ownerSessionId || '') !== ownerSessionId) continue;
|
|
526
|
+
try { releaseNativeTask(task.jobId); } catch {}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
510
529
|
if (!scoped) {
|
|
511
530
|
for (const jobId of [...shellJobResourceLeases.keys()]) {
|
|
512
531
|
releaseShellJobResourceLease(jobId);
|
|
@@ -4,22 +4,31 @@ import {
|
|
|
4
4
|
peekShellJob,
|
|
5
5
|
killShellJob,
|
|
6
6
|
cancelBackgroundShellJobWatch,
|
|
7
|
-
configureBackgroundShellJobMonitor,
|
|
8
7
|
clearShellJobNotifyCtx,
|
|
9
8
|
shellJobPublicTaskResult,
|
|
9
|
+
shellJobTaskStatus,
|
|
10
|
+
waitForShellJob,
|
|
10
11
|
} from './shell-jobs.mjs';
|
|
12
|
+
|
|
13
|
+
// Ceiling for confirming that a cancelled task's process really exited. The
|
|
14
|
+
// wait returns the moment the native task settles, so this only bounds a
|
|
15
|
+
// process that ignores the kill.
|
|
16
|
+
const TASK_CANCEL_CONFIRM_MS = 5_000;
|
|
11
17
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
} from './
|
|
18
|
+
TASK_WAIT_TIMEOUT_DEFAULT_MS,
|
|
19
|
+
TASK_WAIT_TIMEOUT_MAX_MS,
|
|
20
|
+
TASK_WAIT_TIMEOUT_MIN_MS,
|
|
21
|
+
} from './builtin-tools.mjs';
|
|
22
|
+
import { getAbortSignalForSession } from '../../session/abort-lookup.mjs';
|
|
16
23
|
import {
|
|
24
|
+
acknowledgeBackgroundTaskCompletion,
|
|
17
25
|
cancelBackgroundTask,
|
|
18
26
|
completeBackgroundTask,
|
|
19
27
|
getBackgroundTask,
|
|
20
28
|
renderBackgroundTask,
|
|
21
29
|
renderBackgroundTaskList,
|
|
22
30
|
} from '../../../../shared/background-tasks.mjs';
|
|
31
|
+
import { recordDeliveredCompletion } from '../../session/manager/delivered-completions.mjs';
|
|
23
32
|
import {
|
|
24
33
|
listShellJobRecords,
|
|
25
34
|
readShellJobRecord,
|
|
@@ -37,7 +46,15 @@ function recoveredTaskMatches(record, options = {}) {
|
|
|
37
46
|
const clientHostPid = Number(context?.clientHostPid) || null;
|
|
38
47
|
if (sessionIds.size === 0 && !clientHostPid) return true;
|
|
39
48
|
if (!record?.ownerSessionId && !record?.ownerHostPid) return true;
|
|
40
|
-
|
|
49
|
+
// A caller WITH session identity is authorized by session identity ONLY.
|
|
50
|
+
// In a pooled host every pane shares one claude.exe pid, so the host pid
|
|
51
|
+
// can neither widen a session mismatch nor stand in for a record that
|
|
52
|
+
// never recorded an owner session — both kept other panes' commands and
|
|
53
|
+
// results visible. The pid path remains for callers that have no session
|
|
54
|
+
// identity at all, where it is the only identity available.
|
|
55
|
+
if (sessionIds.size > 0) {
|
|
56
|
+
return Boolean(record.ownerSessionId) && sessionIds.has(String(record.ownerSessionId));
|
|
57
|
+
}
|
|
41
58
|
return Boolean(clientHostPid && Number(record.ownerHostPid) === clientHostPid);
|
|
42
59
|
}
|
|
43
60
|
|
|
@@ -53,20 +70,15 @@ function renderRecoveredShellTask(record) {
|
|
|
53
70
|
].join('\n');
|
|
54
71
|
}
|
|
55
72
|
|
|
56
|
-
function shellJobToTaskStatus(status) {
|
|
57
|
-
if (status === 'completed') return 'completed';
|
|
58
|
-
if (status === 'cancelled') return 'cancelled';
|
|
59
|
-
if (status === 'running') return 'running';
|
|
60
|
-
return 'failed';
|
|
61
|
-
}
|
|
62
|
-
|
|
63
73
|
function refreshShellTask(taskId, { includeRunning = false } = {}) {
|
|
64
74
|
const job = peekShellJob(taskId);
|
|
65
75
|
if (!job) return null;
|
|
66
76
|
const publicResult = shellJobPublicTaskResult(job);
|
|
67
77
|
if (job.status !== 'running') {
|
|
68
78
|
completeBackgroundTask(taskId, {
|
|
69
|
-
|
|
79
|
+
// Same mapping as the completion notification, so a non-zero exit
|
|
80
|
+
// reads as a completed task in both surfaces.
|
|
81
|
+
status: shellJobTaskStatus(job),
|
|
70
82
|
result: publicResult,
|
|
71
83
|
resultText: JSON.stringify(publicResult, null, 2),
|
|
72
84
|
notify: false,
|
|
@@ -91,6 +103,14 @@ function renderTaskCancelSuccess(taskId, task) {
|
|
|
91
103
|
].join('\n');
|
|
92
104
|
}
|
|
93
105
|
|
|
106
|
+
// Clamp lives next to the caller; the bounds themselves are published by the
|
|
107
|
+
// tool schema, so an out-of-range request is corrected rather than rejected.
|
|
108
|
+
function resolveTaskWaitTimeoutMs(value) {
|
|
109
|
+
const raw = Number(value);
|
|
110
|
+
if (!Number.isFinite(raw) || raw <= 0) return TASK_WAIT_TIMEOUT_DEFAULT_MS;
|
|
111
|
+
return Math.min(TASK_WAIT_TIMEOUT_MAX_MS, Math.max(TASK_WAIT_TIMEOUT_MIN_MS, Math.floor(raw)));
|
|
112
|
+
}
|
|
113
|
+
|
|
94
114
|
export async function executeTaskTool(args, options = {}) {
|
|
95
115
|
const action = typeof args.action === 'string' ? args.action.toLowerCase() : '';
|
|
96
116
|
if (!action) return 'Error: task action is required';
|
|
@@ -114,8 +134,8 @@ export async function executeTaskTool(args, options = {}) {
|
|
|
114
134
|
if (/^sess_/.test(taskId)) {
|
|
115
135
|
return `Error: "${taskId}" is an agent/session id, not a background task_id. Agent tasks deliver completion notifications; use agent list/read only for manual recovery.`;
|
|
116
136
|
}
|
|
117
|
-
if (action !== 'read' && action !== '
|
|
118
|
-
return `Error: task action must be one of list|read|
|
|
137
|
+
if (action !== 'read' && action !== 'wait' && action !== 'cancel') {
|
|
138
|
+
return `Error: task action must be one of list|read|wait|cancel (got ${JSON.stringify(args.action)})`;
|
|
119
139
|
}
|
|
120
140
|
|
|
121
141
|
const task = getBackgroundTask(taskId, { context: options });
|
|
@@ -124,56 +144,108 @@ export async function executeTaskTool(args, options = {}) {
|
|
|
124
144
|
if (!recovered || !recoveredTaskMatches(recovered, options)) {
|
|
125
145
|
return `Error: task not found: ${taskId}`;
|
|
126
146
|
}
|
|
127
|
-
if (action === 'read') {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
147
|
+
if (action === 'read' || action === 'wait') {
|
|
148
|
+
// Nothing to wait ON after a restart: the process is gone, so wait
|
|
149
|
+
// degrades to the recovered snapshot rather than blocking.
|
|
150
|
+
if (!recovered.terminal) return `Error: task state is unavailable after restart: ${taskId}`;
|
|
151
|
+
recordDeliveredCompletion({ executionId: taskId });
|
|
152
|
+
return renderRecoveredShellTask(recovered);
|
|
131
153
|
}
|
|
132
154
|
if (action === 'cancel') {
|
|
133
155
|
return recovered.terminal
|
|
134
156
|
? `Error: task already ${recovered.status}: ${taskId}`
|
|
135
157
|
: `Error: task process control is unavailable after restart: ${taskId}`;
|
|
136
158
|
}
|
|
137
|
-
if (action === 'monitor') {
|
|
138
|
-
return `Error: task monitoring is unavailable after restart: ${taskId}`;
|
|
139
|
-
}
|
|
140
159
|
}
|
|
141
160
|
const isShellTask = task.surface === 'shell';
|
|
142
161
|
|
|
143
|
-
if (action === 'read') {
|
|
162
|
+
if (action === 'read' || action === 'wait') {
|
|
163
|
+
if (action === 'wait') {
|
|
164
|
+
if (!isShellTask) return `Error: task wait is only available for shell tasks: ${taskId}`;
|
|
165
|
+
// The turn's abort signal must cut the wait short; a cancelled turn
|
|
166
|
+
// cannot sit out the remaining ceiling.
|
|
167
|
+
let waitSignal = null;
|
|
168
|
+
try { waitSignal = (await getAbortSignalForSession(options?.sessionId)) || null; }
|
|
169
|
+
catch { waitSignal = null; }
|
|
170
|
+
const waitTimeoutMs = resolveTaskWaitTimeoutMs(args.timeout_ms);
|
|
171
|
+
// A shell task whose descendants outlived the shell process has no
|
|
172
|
+
// native job to subscribe to — the process it belonged to is gone.
|
|
173
|
+
// Its registry promise (resolved by the descendant observer) is the
|
|
174
|
+
// completion event in that case.
|
|
175
|
+
if (!peekShellJob(taskId) && task.status === 'running' && task.promise) {
|
|
176
|
+
await new Promise((resolve) => {
|
|
177
|
+
const timer = setTimeout(resolve, waitTimeoutMs);
|
|
178
|
+
timer.unref?.();
|
|
179
|
+
const done = () => { clearTimeout(timer); resolve(); };
|
|
180
|
+
task.promise.then(done, done);
|
|
181
|
+
if (waitSignal) {
|
|
182
|
+
if (waitSignal.aborted) done();
|
|
183
|
+
else waitSignal.addEventListener('abort', done, { once: true });
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
} else {
|
|
187
|
+
await waitForShellJob(taskId, {
|
|
188
|
+
timeoutMs: waitTimeoutMs,
|
|
189
|
+
signal: waitSignal,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
144
193
|
if (isShellTask) refreshShellTask(taskId, { includeRunning: true });
|
|
145
194
|
const latest = getBackgroundTask(taskId, { context: options }) || task;
|
|
146
195
|
const rendered = renderBackgroundTask(latest, { includeResult: true });
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (action === 'monitor') {
|
|
151
|
-
const intervalMs = args.monitor_interval_ms;
|
|
152
|
-
if (!isValidShellMonitorIntervalMs(intervalMs)) {
|
|
153
|
-
return `Error: task monitor_interval_ms must be 0 or an integer from ${SHELL_MONITOR_INTERVAL_MIN_MS} to ${SHELL_MONITOR_INTERVAL_MAX_MS} ms`;
|
|
196
|
+
if (acknowledgeBackgroundTaskCompletion(taskId, { context: options })) {
|
|
197
|
+
recordDeliveredCompletion({ executionId: taskId });
|
|
154
198
|
}
|
|
155
|
-
|
|
156
|
-
const configured = configureBackgroundShellJobMonitor(taskId, intervalMs);
|
|
157
|
-
if (configured == null) return `Error: shell task is not running: ${taskId}`;
|
|
158
|
-
return [
|
|
159
|
-
'status: running',
|
|
160
|
-
`task_id: ${taskId}`,
|
|
161
|
-
`monitoring: ${configured === 0 ? 'off' : 'on'}`,
|
|
162
|
-
`monitor_interval_ms: ${configured}`,
|
|
163
|
-
].join('\n');
|
|
199
|
+
return rendered;
|
|
164
200
|
}
|
|
165
201
|
|
|
166
202
|
if (action === 'cancel') {
|
|
167
203
|
if (isShellTask) {
|
|
204
|
+
// A tracked task with no native job is still cancellable: the
|
|
205
|
+
// registry's own cancel hook owns it (descendants that outlived
|
|
206
|
+
// their shell). Remember that it WAS live so the outcome is
|
|
207
|
+
// reported as a cancellation rather than "task not found".
|
|
208
|
+
const wasRunning = task.status === 'running';
|
|
168
209
|
const job = killShellJob(taskId);
|
|
210
|
+
if (job) {
|
|
211
|
+
// Confirm the process actually terminated before reporting a
|
|
212
|
+
// terminal status. Removing the watcher on the strength of a
|
|
213
|
+
// delivered signal left a live, unmonitored process behind a
|
|
214
|
+
// false 'cancelled'.
|
|
215
|
+
const confirmed = await waitForShellJob(taskId, { timeoutMs: TASK_CANCEL_CONFIRM_MS });
|
|
216
|
+
if (confirmed && confirmed.status === 'running') {
|
|
217
|
+
// Watcher and notify context stay armed: the eventual exit
|
|
218
|
+
// must still reach the owner.
|
|
219
|
+
return [
|
|
220
|
+
'status: cancel-unconfirmed',
|
|
221
|
+
`task_id: ${taskId}`,
|
|
222
|
+
'the cancellation was delivered but the process has not exited yet. It stays tracked and its completion is still delivered; re-run task cancel or task read to check again.',
|
|
223
|
+
].join('\n');
|
|
224
|
+
}
|
|
225
|
+
if (!confirmed) {
|
|
226
|
+
// No terminal event and no observable task: absence of
|
|
227
|
+
// state is not proof of termination. Stop tracking it so it
|
|
228
|
+
// cannot hang as 'running' forever, but never claim it died.
|
|
229
|
+
cancelBackgroundShellJobWatch(taskId);
|
|
230
|
+
clearShellJobNotifyCtx(taskId);
|
|
231
|
+
cancelBackgroundTask(taskId, 'cancelled by task control; termination unconfirmed');
|
|
232
|
+
return [
|
|
233
|
+
'status: cancel-unconfirmed',
|
|
234
|
+
`task_id: ${taskId}`,
|
|
235
|
+
'the cancellation was delivered but this task is no longer observable, so its termination could not be confirmed. Verify the process directly if it must be gone.',
|
|
236
|
+
].join('\n');
|
|
237
|
+
}
|
|
238
|
+
}
|
|
169
239
|
cancelBackgroundShellJobWatch(taskId);
|
|
170
240
|
clearShellJobNotifyCtx(taskId);
|
|
171
241
|
cancelBackgroundTask(taskId, 'cancelled by task control');
|
|
172
|
-
return job
|
|
242
|
+
return (job || wasRunning)
|
|
243
|
+
? renderTaskCancelSuccess(taskId, getBackgroundTask(taskId, { context: options }) || task)
|
|
244
|
+
: buildJobNotFoundMessage(taskId);
|
|
173
245
|
}
|
|
174
246
|
cancelBackgroundTask(taskId, 'cancelled by task control');
|
|
175
247
|
return renderTaskCancelSuccess(taskId, getBackgroundTask(taskId, { context: options }) || task);
|
|
176
248
|
}
|
|
177
249
|
|
|
178
|
-
return `Error: task action must be one of list|read|
|
|
250
|
+
return `Error: task action must be one of list|read|wait|cancel (got ${JSON.stringify(args.action)})`;
|
|
179
251
|
}
|