mixdog 0.9.147 → 0.9.149
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 +5 -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-codex-identity.test.mjs +32 -9
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +78 -33
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +21 -0
- 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 +20 -11
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +112 -36
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +220 -50
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +112 -9
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +130 -60
- 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 +69 -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/ask-session.mjs +43 -5
- 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/session-lifecycle.mjs +2 -0
- 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 +46 -9
- 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-integrity.test.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +180 -31
- 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/pristine-execution-contract.json +0 -1
- 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/session-runtime/session-lifecycle.mjs +9 -0
- 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 +52 -70
- 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
|
@@ -11,14 +11,125 @@ import { ensureSpawnBinary, findCachedSpawnBinary } from '../spawn-binary-fetche
|
|
|
11
11
|
|
|
12
12
|
const RESTART_BACKOFF_MS = 30_000;
|
|
13
13
|
const SERVER_READY_TIMEOUT_MS = 5_000;
|
|
14
|
+
const REQUIRED_LIFECYCLE_CAPS = Object.freeze([
|
|
15
|
+
'trackedForeground',
|
|
16
|
+
'promoteTask',
|
|
17
|
+
'cancelOwner',
|
|
18
|
+
]);
|
|
14
19
|
|
|
15
20
|
let _server = null;
|
|
16
21
|
let _binaryPath = undefined;
|
|
17
22
|
let _lastFailureAt = 0;
|
|
18
23
|
const _tasks = new Map();
|
|
19
|
-
const _pidToRequest = new Map();
|
|
20
24
|
const _taskEvents = new EventEmitter();
|
|
21
25
|
|
|
26
|
+
// Retention of SETTLED tasks. A completed task stays queryable so a late
|
|
27
|
+
// `task read` still answers, but the spawn server's `task_released` event is
|
|
28
|
+
// the only thing that used to drop it — a server that never sends one (old
|
|
29
|
+
// binary, crash, cancelled owner) grew _tasks without bound for the daemon's
|
|
30
|
+
// whole life. Terminal entries now carry their own TTL plus a hard cap, and
|
|
31
|
+
// releaseNativeTask() gives owners an explicit release.
|
|
32
|
+
const RETAINED_TASK_TTL_MS = 10 * 60_000;
|
|
33
|
+
const RETAINED_TASK_LIMIT = 256;
|
|
34
|
+
const RETAINED_TASK_SWEEP_MS = 60_000;
|
|
35
|
+
const _terminalAtMs = new Map();
|
|
36
|
+
let _retentionSweepTimer = null;
|
|
37
|
+
|
|
38
|
+
// The spawn server keeps its OWN retained-jobs map, so a client-side drop must
|
|
39
|
+
// tell it to drop the same entry — otherwise the leak just moves one process
|
|
40
|
+
// over. Older servers without the capability simply keep their cap-based
|
|
41
|
+
// pruning.
|
|
42
|
+
function _requestServerTaskRelease(jobId) {
|
|
43
|
+
const server = _server;
|
|
44
|
+
if (!server?.ready || server.caps?.releaseTask !== true) return false;
|
|
45
|
+
try {
|
|
46
|
+
server.child.stdin.write(`${JSON.stringify({ id: ++server.sequence, releaseTask: jobId })}\n`);
|
|
47
|
+
return true;
|
|
48
|
+
} catch { return false; }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function _stopRetentionSweep() {
|
|
52
|
+
if (!_retentionSweepTimer) return;
|
|
53
|
+
clearInterval(_retentionSweepTimer);
|
|
54
|
+
_retentionSweepTimer = null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Retention must not depend on new task events arriving: a daemon that goes
|
|
58
|
+
// quiet right after its last command would otherwise hold that entry (and the
|
|
59
|
+
// server's) forever. The sweep runs on its own unref'd cadence and stops
|
|
60
|
+
// itself as soon as nothing is retained.
|
|
61
|
+
function _startRetentionSweep() {
|
|
62
|
+
if (_retentionSweepTimer || _terminalAtMs.size === 0) return;
|
|
63
|
+
_retentionSweepTimer = setInterval(() => {
|
|
64
|
+
_sweepRetainedTasks();
|
|
65
|
+
if (_terminalAtMs.size === 0) _stopRetentionSweep();
|
|
66
|
+
}, RETAINED_TASK_SWEEP_MS);
|
|
67
|
+
_retentionSweepTimer.unref?.();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function _dropTaskEntry(jobId, { notifyServer = true } = {}) {
|
|
71
|
+
_tasks.delete(jobId);
|
|
72
|
+
_terminalAtMs.delete(jobId);
|
|
73
|
+
_startedAtOverrides.delete(jobId);
|
|
74
|
+
if (notifyServer) _requestServerTaskRelease(jobId);
|
|
75
|
+
if (_terminalAtMs.size === 0) _stopRetentionSweep();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function _sweepRetainedTasks(now = Date.now()) {
|
|
79
|
+
for (const [jobId, at] of _terminalAtMs) {
|
|
80
|
+
if (now - at >= RETAINED_TASK_TTL_MS) _dropTaskEntry(jobId);
|
|
81
|
+
}
|
|
82
|
+
// Insertion-ordered: the oldest terminal entries go first.
|
|
83
|
+
let overflow = _terminalAtMs.size - RETAINED_TASK_LIMIT;
|
|
84
|
+
if (overflow <= 0) return;
|
|
85
|
+
for (const jobId of [..._terminalAtMs.keys()]) {
|
|
86
|
+
if (overflow-- <= 0) break;
|
|
87
|
+
_dropTaskEntry(jobId);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function _markTerminalTask(jobId, status) {
|
|
92
|
+
if (status === 'running') {
|
|
93
|
+
_terminalAtMs.delete(jobId);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (!_terminalAtMs.has(jobId)) _terminalAtMs.set(jobId, Date.now());
|
|
97
|
+
_sweepRetainedTasks();
|
|
98
|
+
_startRetentionSweep();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Drop a settled task's retained state. Running tasks are never released
|
|
102
|
+
* (cancel them first); returns true when an entry was actually dropped. */
|
|
103
|
+
export function releaseNativeTask(jobId) {
|
|
104
|
+
const key = String(jobId || '').trim();
|
|
105
|
+
const task = key ? _tasks.get(key) : null;
|
|
106
|
+
if (!task || task.status === 'running') return false;
|
|
107
|
+
_dropTaskEntry(key);
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function nativeSpawnCompatibilityError(server) {
|
|
112
|
+
if (!server?.ready) return null;
|
|
113
|
+
const missingCaps = REQUIRED_LIFECYCLE_CAPS.filter((cap) => server.caps?.[cap] !== true);
|
|
114
|
+
if (missingCaps.length === 0) return null;
|
|
115
|
+
const error = new Error(
|
|
116
|
+
`native spawn server is incompatible; missing required lifecycle capabilities: ${missingCaps.join(', ')}`,
|
|
117
|
+
);
|
|
118
|
+
error.code = 'NATIVE_SPAWN_INCOMPATIBLE';
|
|
119
|
+
error.missingCaps = missingCaps;
|
|
120
|
+
return error;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function assertNativeSpawnCompatibility(server) {
|
|
124
|
+
const error = nativeSpawnCompatibilityError(server);
|
|
125
|
+
if (error) throw error;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function _assertNativeSpawnCapabilitiesForTest(caps = {}) {
|
|
129
|
+
assertNativeSpawnCompatibility({ ready: true, caps });
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
22
133
|
// Warm-standby correction. The spawn server stamps a task with the moment its
|
|
23
134
|
// PROCESS was created, which for a pre-spawned standby shell is up to the
|
|
24
135
|
// standby TTL (2 min) before the command was ever fed to it — every consumer
|
|
@@ -80,6 +191,7 @@ function acceptTask(message) {
|
|
|
80
191
|
const task = normalizeTask(message?.task);
|
|
81
192
|
if (!task) return null;
|
|
82
193
|
_tasks.set(task.jobId, task);
|
|
194
|
+
_markTerminalTask(task.jobId, task.status);
|
|
83
195
|
_taskEvents.emit(task.jobId, task);
|
|
84
196
|
return task;
|
|
85
197
|
}
|
|
@@ -223,7 +335,7 @@ export function setNativeSpawnRequestIdle(child, idle = true) {
|
|
|
223
335
|
return true;
|
|
224
336
|
}
|
|
225
337
|
|
|
226
|
-
function _teardown(error) {
|
|
338
|
+
function _teardown(error, { kill = true } = {}) {
|
|
227
339
|
const server = _server;
|
|
228
340
|
_server = null;
|
|
229
341
|
_lastFailureAt = Date.now();
|
|
@@ -242,9 +354,12 @@ function _teardown(error) {
|
|
|
242
354
|
finishedAt: new Date().toISOString(),
|
|
243
355
|
};
|
|
244
356
|
_tasks.set(jobId, failed);
|
|
357
|
+
_markTerminalTask(jobId, failed.status);
|
|
245
358
|
_taskEvents.emit(jobId, { ...failed });
|
|
246
359
|
}
|
|
247
|
-
|
|
360
|
+
if (kill) {
|
|
361
|
+
try { server.child.kill(); } catch {}
|
|
362
|
+
}
|
|
248
363
|
}
|
|
249
364
|
|
|
250
365
|
export async function shutdownNativeSpawnServer(reason = 'process-exit', timeoutMs = 1_000) {
|
|
@@ -255,8 +370,13 @@ export async function shutdownNativeSpawnServer(reason = 'process-exit', timeout
|
|
|
255
370
|
child.once('exit', () => resolve(true));
|
|
256
371
|
child.once('error', () => resolve(true));
|
|
257
372
|
});
|
|
373
|
+
// Graceful stop first: detach the module handle and fail pending requests,
|
|
374
|
+
// but do NOT kill the server here. Closing stdin is its shutdown signal, and
|
|
375
|
+
// it needs that window to terminate the process GROUPS it owns (POSIX) —
|
|
376
|
+
// killing the server first preempted that cleanup and orphaned children.
|
|
377
|
+
// SIGKILL below is the fallback for a server that misses the window.
|
|
378
|
+
_teardown(new Error(`native spawn server shutdown (${reason})`), { kill: false });
|
|
258
379
|
try { child.stdin?.end?.(); } catch {}
|
|
259
|
-
_teardown(new Error(`native spawn server shutdown (${reason})`));
|
|
260
380
|
let timer;
|
|
261
381
|
const stopped = await Promise.race([
|
|
262
382
|
exited,
|
|
@@ -305,7 +425,13 @@ function _ensureServer() {
|
|
|
305
425
|
settleReady(true);
|
|
306
426
|
return;
|
|
307
427
|
}
|
|
308
|
-
|
|
428
|
+
const event = String(message?.event || '');
|
|
429
|
+
if (event === 'task_released') {
|
|
430
|
+
const jobId = String(message?.jobId || '');
|
|
431
|
+
if (jobId) _dropTaskEntry(jobId, { notifyServer: false });
|
|
432
|
+
} else if (event.startsWith('task_')) {
|
|
433
|
+
acceptTask(message);
|
|
434
|
+
}
|
|
309
435
|
const pending = server.pending.get(Number(message?.id));
|
|
310
436
|
if (!pending) return;
|
|
311
437
|
pending.onMessage(message);
|
|
@@ -333,7 +459,10 @@ export async function warmNativeSpawnServer() {
|
|
|
333
459
|
}
|
|
334
460
|
|
|
335
461
|
export async function ensureNativeSpawnServer() {
|
|
336
|
-
if (_server?.ready)
|
|
462
|
+
if (_server?.ready) {
|
|
463
|
+
assertNativeSpawnCompatibility(_server);
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
337
466
|
let binary = _resolveBinary();
|
|
338
467
|
if (!binary) {
|
|
339
468
|
binary = await ensureSpawnBinary(getPluginData());
|
|
@@ -367,12 +496,14 @@ export async function ensureNativeSpawnServer() {
|
|
|
367
496
|
});
|
|
368
497
|
}
|
|
369
498
|
}
|
|
499
|
+
assertNativeSpawnCompatibility(server);
|
|
370
500
|
return true;
|
|
371
501
|
}
|
|
372
502
|
|
|
373
503
|
export function tryNativeSpawn({ shell, argv, spawnOptions = {}, cwd } = {}) {
|
|
374
504
|
const server = _ensureServer();
|
|
375
505
|
if (!server) return null;
|
|
506
|
+
assertNativeSpawnCompatibility(server);
|
|
376
507
|
const id = ++server.sequence;
|
|
377
508
|
const fake = new NativeSpawnChild(id, (cancelId) => {
|
|
378
509
|
try { server.child.stdin.write(`${JSON.stringify({ cancel: cancelId })}\n`); } catch {}
|
|
@@ -382,6 +513,12 @@ export function tryNativeSpawn({ shell, argv, spawnOptions = {}, cwd } = {}) {
|
|
|
382
513
|
return true;
|
|
383
514
|
} catch { return false; }
|
|
384
515
|
});
|
|
516
|
+
// File capture engages only when the RUNNING server advertises it and the
|
|
517
|
+
// caller supplied both paths. An older server would ignore the unknown
|
|
518
|
+
// fields and keep piping, so the capability gate decides rather than hope.
|
|
519
|
+
const _fileCapture = server.caps?.fileCapture === true
|
|
520
|
+
&& Boolean(spawnOptions.stdoutPath)
|
|
521
|
+
&& Boolean(spawnOptions.stderrPath);
|
|
385
522
|
const request = {
|
|
386
523
|
id,
|
|
387
524
|
program: String(shell || ''),
|
|
@@ -394,6 +531,10 @@ export function tryNativeSpawn({ shell, argv, spawnOptions = {}, cwd } = {}) {
|
|
|
394
531
|
outputLimit: Math.max(0, Number(spawnOptions.outputLimit) || 0),
|
|
395
532
|
mergeStderr: spawnOptions.mergeStderr === true,
|
|
396
533
|
rawOutput: spawnOptions.rawOutput === true,
|
|
534
|
+
...(_fileCapture ? {
|
|
535
|
+
stdoutPath: String(spawnOptions.stdoutPath),
|
|
536
|
+
stderrPath: String(spawnOptions.stderrPath),
|
|
537
|
+
} : {}),
|
|
397
538
|
...(spawnOptions.stdinPipe === true ? { stdinPipe: true } : {}),
|
|
398
539
|
command: spawnOptions.command || undefined,
|
|
399
540
|
shellType: spawnOptions.shellType || undefined,
|
|
@@ -417,7 +558,6 @@ export function tryNativeSpawn({ shell, argv, spawnOptions = {}, cwd } = {}) {
|
|
|
417
558
|
const event = String(message?.event || '');
|
|
418
559
|
if (event === 'spawned') {
|
|
419
560
|
fake.pid = Number(message.pid) || undefined;
|
|
420
|
-
if (fake.pid) _pidToRequest.set(fake.pid, id);
|
|
421
561
|
fake.emit('spawn');
|
|
422
562
|
return;
|
|
423
563
|
}
|
|
@@ -440,7 +580,6 @@ export function tryNativeSpawn({ shell, argv, spawnOptions = {}, cwd } = {}) {
|
|
|
440
580
|
}
|
|
441
581
|
else if (event === 'exit') {
|
|
442
582
|
server.pending.delete(id);
|
|
443
|
-
if (fake.pid) _pidToRequest.delete(fake.pid);
|
|
444
583
|
fake.exitCode = message.code == null ? null : Number(message.code);
|
|
445
584
|
fake.signalCode = message.signal || null;
|
|
446
585
|
fake._closed = true;
|
|
@@ -463,7 +602,8 @@ export function tryNativeSpawn({ shell, argv, spawnOptions = {}, cwd } = {}) {
|
|
|
463
602
|
}
|
|
464
603
|
return {
|
|
465
604
|
child: fake,
|
|
466
|
-
|
|
605
|
+
fileCapture: _fileCapture,
|
|
606
|
+
attachErrorHandler(handler) {
|
|
467
607
|
fake.on('error', handler);
|
|
468
608
|
},
|
|
469
609
|
};
|
|
@@ -474,6 +614,20 @@ export function getNativeTask(jobId) {
|
|
|
474
614
|
return task ? { ...task } : null;
|
|
475
615
|
}
|
|
476
616
|
|
|
617
|
+
/** Whether the live server can hand a child its own capture-file fds.
|
|
618
|
+
* Callers ask BEFORE opening those files so a server without the capability
|
|
619
|
+
* never leaves an empty pair behind.
|
|
620
|
+
*
|
|
621
|
+
* win32 EXCEPTION: there is no process group to interrogate there, so a
|
|
622
|
+
* command's surviving descendants are observed by the stdout/stderr pipes
|
|
623
|
+
* they INHERITED still being held after the shell itself exited (see
|
|
624
|
+
* lib/shell-descendants.mjs). File capture removes exactly that evidence.
|
|
625
|
+
* POSIX loses nothing — its survivor check reads the process group. */
|
|
626
|
+
export function nativeSpawnFileCaptureReady() {
|
|
627
|
+
if (process.platform === 'win32') return false;
|
|
628
|
+
return _server?.ready === true && _server?.caps?.fileCapture === true;
|
|
629
|
+
}
|
|
630
|
+
|
|
477
631
|
export function listNativeTasks() {
|
|
478
632
|
return [..._tasks.values()].map((task) => ({ ...task }));
|
|
479
633
|
}
|
|
@@ -494,23 +648,81 @@ export function cancelNativeTask(jobId) {
|
|
|
494
648
|
return task;
|
|
495
649
|
}
|
|
496
650
|
|
|
497
|
-
|
|
651
|
+
/** Cancel every running task owned by one session.
|
|
652
|
+
*
|
|
653
|
+
* Reports what the kill could actually prove:
|
|
654
|
+
* cancelled tasks that were running when the cancel was issued
|
|
655
|
+
* unconfirmed jobIds whose cancel reached NOTHING — no live spawn-server
|
|
656
|
+
* link, so the process stays observable but unaddressable from
|
|
657
|
+
* this host (the Windows survivor case)
|
|
658
|
+
* confirmed false while any unconfirmed entry exists
|
|
659
|
+
* Delivery to the owning spawn server counts as confirmed (it holds the
|
|
660
|
+
* handle and completes the kill); an undelivered signal never does. */
|
|
661
|
+
export function cancelNativeTasks({ ownerSessionId = null } = {}) {
|
|
662
|
+
const owner = String(ownerSessionId || '').trim();
|
|
663
|
+
if (!owner) return { cancelled: 0, unconfirmed: [], confirmed: true };
|
|
664
|
+
const running = [..._tasks.values()].filter((task) =>
|
|
665
|
+
task.status === 'running' && String(task.ownerSessionId || '') === owner);
|
|
666
|
+
const server = _server;
|
|
667
|
+
let ownerCancelDelivered = false;
|
|
668
|
+
if (server?.caps?.cancelOwner === true) {
|
|
669
|
+
try {
|
|
670
|
+
server.child.stdin.write(`${JSON.stringify({
|
|
671
|
+
id: ++server.sequence,
|
|
672
|
+
cancelOwnerSession: owner,
|
|
673
|
+
})}\n`);
|
|
674
|
+
ownerCancelDelivered = true;
|
|
675
|
+
} catch { ownerCancelDelivered = false; }
|
|
676
|
+
}
|
|
677
|
+
// Also issue direct per-task cancellation for entries already visible in
|
|
678
|
+
// this client so teardown does not wait on the owner-wide command round trip.
|
|
679
|
+
// A teardown never BOOTS a spawn server just to cancel: with the link gone
|
|
680
|
+
// there is nothing left for the signal to travel through, and pretending
|
|
681
|
+
// otherwise is what turned an unkillable survivor into a false `cancelled`.
|
|
682
|
+
const unconfirmed = [];
|
|
683
|
+
for (const task of running) {
|
|
684
|
+
let delivered = ownerCancelDelivered;
|
|
685
|
+
if (server) {
|
|
686
|
+
try { delivered = Boolean(cancelNativeTask(task.jobId)) || delivered; } catch { /* undelivered */ }
|
|
687
|
+
}
|
|
688
|
+
if (!delivered) unconfirmed.push(task.jobId);
|
|
689
|
+
}
|
|
690
|
+
return { cancelled: running.length, unconfirmed, confirmed: unconfirmed.length === 0 };
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Completion-event wait. Resolves the moment the task leaves `running`, so the
|
|
694
|
+
// timeout is a ceiling and never a fixed delay. An abort signal resolves with
|
|
695
|
+
// the CURRENT state instead of leaving the caller blocked: a cancelled turn
|
|
696
|
+
// must return immediately, not serve out the remaining ceiling.
|
|
697
|
+
export function waitNativeTask(jobId, timeoutMs = 30_000, signal = null) {
|
|
498
698
|
const key = String(jobId || '');
|
|
499
699
|
const current = getNativeTask(key);
|
|
500
700
|
if (!current || current.status !== 'running') return Promise.resolve(current);
|
|
701
|
+
if (signal?.aborted) return Promise.resolve(current);
|
|
501
702
|
return new Promise((resolve) => {
|
|
502
703
|
let timer = null;
|
|
503
|
-
|
|
504
|
-
|
|
704
|
+
let onAbort = null;
|
|
705
|
+
let settled = false;
|
|
706
|
+
const finish = (task) => {
|
|
707
|
+
if (settled) return;
|
|
708
|
+
settled = true;
|
|
505
709
|
if (timer) clearTimeout(timer);
|
|
710
|
+
if (onAbort && signal) {
|
|
711
|
+
try { signal.removeEventListener('abort', onAbort); } catch {}
|
|
712
|
+
}
|
|
506
713
|
unsubscribe();
|
|
507
714
|
resolve(task ? { ...task } : null);
|
|
508
715
|
};
|
|
716
|
+
const done = (task) => {
|
|
717
|
+
if (task?.status === 'running') return;
|
|
718
|
+
finish(task);
|
|
719
|
+
};
|
|
509
720
|
const unsubscribe = subscribeNativeTask(key, done);
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
}
|
|
721
|
+
if (signal) {
|
|
722
|
+
onAbort = () => finish(getNativeTask(key));
|
|
723
|
+
try { signal.addEventListener('abort', onAbort, { once: true }); } catch {}
|
|
724
|
+
}
|
|
725
|
+
timer = setTimeout(() => finish(getNativeTask(key)), Math.max(1, Number(timeoutMs) || 30_000));
|
|
514
726
|
timer.unref?.();
|
|
515
727
|
});
|
|
516
728
|
}
|
|
@@ -572,19 +784,10 @@ export async function startNativeTask({
|
|
|
572
784
|
});
|
|
573
785
|
}
|
|
574
786
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
timeoutMs = 0,
|
|
580
|
-
shellType = null,
|
|
581
|
-
ownerSessionId = null,
|
|
582
|
-
clientHostPid = null,
|
|
583
|
-
jobId = `job_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
584
|
-
} = {}) {
|
|
585
|
-
const requestId = _pidToRequest.get(Number(pid));
|
|
586
|
-
const server = _ensureServer();
|
|
587
|
-
if (!server || !requestId) return null;
|
|
787
|
+
function requestNativeTaskState(jobId, payload, errorLabel) {
|
|
788
|
+
const key = String(jobId || '').trim();
|
|
789
|
+
const server = _server;
|
|
790
|
+
if (!server || !key) return null;
|
|
588
791
|
const id = ++server.sequence;
|
|
589
792
|
return new Promise((resolve, reject) => {
|
|
590
793
|
let timer = null;
|
|
@@ -597,14 +800,14 @@ export function adoptNativeTaskByPid({
|
|
|
597
800
|
};
|
|
598
801
|
server.pending.set(id, {
|
|
599
802
|
fail(error) {
|
|
600
|
-
finish(null, error instanceof Error ? error : new Error(String(error ||
|
|
803
|
+
finish(null, error instanceof Error ? error : new Error(String(error || `${errorLabel} failed`)));
|
|
601
804
|
},
|
|
602
805
|
onMessage(message) {
|
|
603
806
|
const event = String(message?.event || '');
|
|
604
|
-
if (event === 'task_started') {
|
|
605
|
-
finish(getNativeTask(
|
|
807
|
+
if (event === 'task_started' || event === 'task_status') {
|
|
808
|
+
finish(getNativeTask(key));
|
|
606
809
|
} else if (event === 'error') {
|
|
607
|
-
const error = new Error(String(message.message ||
|
|
810
|
+
const error = new Error(String(message.message || `${errorLabel} failed`));
|
|
608
811
|
if (message.code) error.code = String(message.code);
|
|
609
812
|
finish(null, error);
|
|
610
813
|
}
|
|
@@ -612,28 +815,64 @@ export function adoptNativeTaskByPid({
|
|
|
612
815
|
});
|
|
613
816
|
_setServerReferenced(server, true);
|
|
614
817
|
try {
|
|
615
|
-
server.child.stdin.write(`${JSON.stringify({
|
|
616
|
-
id,
|
|
617
|
-
adopt: requestId,
|
|
618
|
-
jobId,
|
|
619
|
-
timeoutMs,
|
|
620
|
-
command,
|
|
621
|
-
cwd,
|
|
622
|
-
shellType,
|
|
623
|
-
ownerSessionId,
|
|
624
|
-
clientHostPid,
|
|
625
|
-
})}\n`);
|
|
818
|
+
server.child.stdin.write(`${JSON.stringify({ id, ...payload })}\n`);
|
|
626
819
|
} catch (error) {
|
|
627
820
|
finish(null, error);
|
|
628
821
|
return;
|
|
629
822
|
}
|
|
630
823
|
timer = setTimeout(() => {
|
|
631
|
-
finish(null, Object.assign(new Error(
|
|
824
|
+
finish(null, Object.assign(new Error(`${errorLabel} timeout`), { code: 'ETIMEDOUT' }));
|
|
632
825
|
}, 5_000);
|
|
633
826
|
timer.unref?.();
|
|
634
827
|
});
|
|
635
828
|
}
|
|
636
829
|
|
|
830
|
+
export function trackNativeForegroundTask({
|
|
831
|
+
child,
|
|
832
|
+
jobId,
|
|
833
|
+
command = '',
|
|
834
|
+
cwd = '',
|
|
835
|
+
shellType = null,
|
|
836
|
+
ownerSessionId = null,
|
|
837
|
+
clientHostPid = null,
|
|
838
|
+
} = {}) {
|
|
839
|
+
const requestId = Number(child?._id) || 0;
|
|
840
|
+
const server = _server;
|
|
841
|
+
if (!server || server.caps?.trackedForeground !== true || !requestId) return null;
|
|
842
|
+
return requestNativeTaskState(jobId, {
|
|
843
|
+
track: requestId,
|
|
844
|
+
jobId,
|
|
845
|
+
command,
|
|
846
|
+
cwd,
|
|
847
|
+
shellType,
|
|
848
|
+
ownerSessionId,
|
|
849
|
+
clientHostPid,
|
|
850
|
+
}, 'native task tracking');
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
export function promoteNativeTask({
|
|
854
|
+
jobId,
|
|
855
|
+
command = '',
|
|
856
|
+
cwd = '',
|
|
857
|
+
timeoutMs = 0,
|
|
858
|
+
shellType = null,
|
|
859
|
+
ownerSessionId = null,
|
|
860
|
+
clientHostPid = null,
|
|
861
|
+
} = {}) {
|
|
862
|
+
const key = String(jobId || '').trim();
|
|
863
|
+
const server = _server;
|
|
864
|
+
if (server?.caps?.promoteTask !== true || !getNativeTask(key)) return null;
|
|
865
|
+
return requestNativeTaskState(key, {
|
|
866
|
+
promoteTask: key,
|
|
867
|
+
timeoutMs,
|
|
868
|
+
command,
|
|
869
|
+
cwd,
|
|
870
|
+
shellType,
|
|
871
|
+
ownerSessionId,
|
|
872
|
+
clientHostPid,
|
|
873
|
+
}, 'native task promotion');
|
|
874
|
+
}
|
|
875
|
+
|
|
637
876
|
// Capability handshake: true only after the connected spawn server announced
|
|
638
877
|
// stdinPipe support in its ready line. An older binary silently ignores the
|
|
639
878
|
// unknown stdinPipe field and gives the child a null stdin — a standby taken
|
|
@@ -648,10 +887,21 @@ export function _resetNativeSpawnClientForTest() {
|
|
|
648
887
|
_binaryPath = undefined;
|
|
649
888
|
_lastFailureAt = 0;
|
|
650
889
|
_tasks.clear();
|
|
651
|
-
|
|
890
|
+
_terminalAtMs.clear();
|
|
891
|
+
_stopRetentionSweep();
|
|
892
|
+
_startedAtOverrides.clear();
|
|
652
893
|
_taskEvents.removeAllListeners();
|
|
653
894
|
}
|
|
654
895
|
|
|
896
|
+
/** Test-only: seed a task record exactly as a spawn-server report would, so
|
|
897
|
+
* cancellation/teardown paths can be exercised without a live server. */
|
|
898
|
+
export function _seedNativeTaskForTest(task) {
|
|
899
|
+
const normalized = normalizeTask(task);
|
|
900
|
+
if (!normalized) return null;
|
|
901
|
+
_tasks.set(normalized.jobId, normalized);
|
|
902
|
+
return normalized;
|
|
903
|
+
}
|
|
904
|
+
|
|
655
905
|
export function _setNativeSpawnBinaryForTest(binaryPath) {
|
|
656
906
|
const path = String(binaryPath || '');
|
|
657
907
|
if (!path || !existsSync(path)) throw new Error(`native spawn test binary not found: ${path}`);
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { existsSync, mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
7
|
+
|
|
8
|
+
import { execShellCommand } from '../shell-command.mjs';
|
|
9
|
+
import { retireShellJobRecord } from '../builtin/lib/shell-job-records.mjs';
|
|
10
|
+
import {
|
|
11
|
+
_assertNativeSpawnCapabilitiesForTest,
|
|
12
|
+
_resetNativeSpawnClientForTest,
|
|
13
|
+
_setNativeSpawnBinaryForTest,
|
|
14
|
+
cancelNativeTasks,
|
|
15
|
+
listNativeTasks,
|
|
16
|
+
shutdownNativeSpawnServer,
|
|
17
|
+
waitNativeTask,
|
|
18
|
+
} from './native-spawn-client.mjs';
|
|
19
|
+
|
|
20
|
+
test('native spawn rejects binaries without the required lifecycle protocol', () => {
|
|
21
|
+
assert.throws(
|
|
22
|
+
() => _assertNativeSpawnCapabilitiesForTest({
|
|
23
|
+
trackedForeground: true,
|
|
24
|
+
promoteTask: true,
|
|
25
|
+
}),
|
|
26
|
+
(error) => error?.code === 'NATIVE_SPAWN_INCOMPATIBLE'
|
|
27
|
+
&& error.missingCaps?.includes('cancelOwner'),
|
|
28
|
+
);
|
|
29
|
+
assert.equal(_assertNativeSpawnCapabilitiesForTest({
|
|
30
|
+
trackedForeground: true,
|
|
31
|
+
promoteTask: true,
|
|
32
|
+
cancelOwner: true,
|
|
33
|
+
}), true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('foreground shell has stable owner identity before promotion', { timeout: 15_000 }, async (t) => {
|
|
37
|
+
const binary = resolve(
|
|
38
|
+
'native/mixdog-spawn/target/debug',
|
|
39
|
+
process.platform === 'win32' ? 'mixdog-spawn.exe' : 'mixdog-spawn',
|
|
40
|
+
);
|
|
41
|
+
if (!existsSync(binary)) {
|
|
42
|
+
t.skip(`native spawn test binary not found: ${binary}`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const root = mkdtempSync(join(tmpdir(), 'mixdog-native-owner-'));
|
|
46
|
+
const previousDataDir = process.env.MIXDOG_DATA_DIR;
|
|
47
|
+
process.env.MIXDOG_DATA_DIR = root;
|
|
48
|
+
const ownerSessionId = `sess_native_owner_${process.pid}`;
|
|
49
|
+
let jobId = null;
|
|
50
|
+
const admission = {
|
|
51
|
+
async acquire() {
|
|
52
|
+
return {
|
|
53
|
+
signal: null,
|
|
54
|
+
async detachDependency() {},
|
|
55
|
+
async release() {},
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
try {
|
|
60
|
+
_setNativeSpawnBinaryForTest(binary);
|
|
61
|
+
const running = execShellCommand({
|
|
62
|
+
shell: process.execPath,
|
|
63
|
+
shellArg: '',
|
|
64
|
+
command: 'native stable foreground ownership test',
|
|
65
|
+
directArgv: ['-e', 'setTimeout(() => {}, 10000)'],
|
|
66
|
+
env: process.env,
|
|
67
|
+
cwd: process.cwd(),
|
|
68
|
+
timeoutMs: 5_000,
|
|
69
|
+
autoBackgroundMs: 250,
|
|
70
|
+
backgroundOnTimeout: true,
|
|
71
|
+
promotedTimeoutMs: 5_000,
|
|
72
|
+
backgroundDeadlineMs: 5_000,
|
|
73
|
+
ownerSessionId,
|
|
74
|
+
clientHostPid: process.pid,
|
|
75
|
+
admission,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
let foreground = null;
|
|
79
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
80
|
+
foreground = listNativeTasks().find((task) =>
|
|
81
|
+
task.status === 'running' && task.ownerSessionId === ownerSessionId);
|
|
82
|
+
if (foreground) break;
|
|
83
|
+
await delay(10);
|
|
84
|
+
}
|
|
85
|
+
assert.ok(foreground, 'foreground process was not owner-tracked at spawn');
|
|
86
|
+
jobId = foreground.jobId;
|
|
87
|
+
|
|
88
|
+
const result = await running;
|
|
89
|
+
assert.equal(result.backgrounded, true);
|
|
90
|
+
assert.equal(result.jobId, jobId);
|
|
91
|
+
|
|
92
|
+
assert.ok(cancelNativeTasks({ ownerSessionId }).cancelled >= 1);
|
|
93
|
+
const terminal = await waitNativeTask(jobId, 3_000);
|
|
94
|
+
assert.notEqual(terminal?.status, 'running');
|
|
95
|
+
assert.equal(terminal?.status, 'cancelled');
|
|
96
|
+
} finally {
|
|
97
|
+
if (jobId) retireShellJobRecord(jobId);
|
|
98
|
+
await shutdownNativeSpawnServer('test-finish').catch(() => {});
|
|
99
|
+
_resetNativeSpawnClientForTest();
|
|
100
|
+
if (previousDataDir === undefined) delete process.env.MIXDOG_DATA_DIR;
|
|
101
|
+
else process.env.MIXDOG_DATA_DIR = previousDataDir;
|
|
102
|
+
rmSync(root, { recursive: true, force: true });
|
|
103
|
+
}
|
|
104
|
+
});
|