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
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Post-exit descendant observation for the shell runner.
|
|
3
|
+
//
|
|
4
|
+
// The question this module answers is "did the command leave anything
|
|
5
|
+
// RUNNING?", and it answers it by looking at PROCESSES — never at the command
|
|
6
|
+
// text. Nothing here parses, classifies or rewrites a single byte of what the
|
|
7
|
+
// caller typed; the shell receives the command exactly as written and the
|
|
8
|
+
// decision is taken afterwards from what the operating system reports.
|
|
9
|
+
//
|
|
10
|
+
// Evidence per platform, both produced by the existing spawn infrastructure:
|
|
11
|
+
// POSIX — native/mixdog-spawn spawns every shell with `process_group(0)`,
|
|
12
|
+
// so the command owns a process group whose id is the shell pid.
|
|
13
|
+
// Once the shell has exited and been reaped, ANY remaining member
|
|
14
|
+
// of that group is a descendant it left behind, and one signal to
|
|
15
|
+
// the negative pgid reaches all of them.
|
|
16
|
+
// win32 — there is no process group to signal, but the shell's stdout and
|
|
17
|
+
// stderr pipes are INHERITED by its descendants: if they are still
|
|
18
|
+
// held when the shell process itself has exited, something of this
|
|
19
|
+
// command is alive. That observation gates a single batched
|
|
20
|
+
// process-table read (shared with mcp/child-tree.mjs) which names
|
|
21
|
+
// the survivors that are still linked to the shell pid.
|
|
22
|
+
import { collectDescendantProcesses, killProcessTrees } from '../../mcp/child-tree.mjs';
|
|
23
|
+
|
|
24
|
+
const isWin = process.platform === 'win32';
|
|
25
|
+
|
|
26
|
+
// The spawn server waits up to 2 s for the shell's stdio pumps to drain after
|
|
27
|
+
// the root process exits and then reports the exit regardless (main.rs
|
|
28
|
+
// run_spawn). A close that lands at/after this threshold therefore means the
|
|
29
|
+
// pipes were STILL held by another process at that deadline, not that draining
|
|
30
|
+
// was slow — a normal command closes within milliseconds of its exit.
|
|
31
|
+
export const STDIO_HELD_AFTER_EXIT_MS = 1_500;
|
|
32
|
+
|
|
33
|
+
// Windows attaches a console host to the hidden console the shell runs in. It
|
|
34
|
+
// is a real child of the shell and it lives exactly as long as console clients
|
|
35
|
+
// remain, which makes it a liveness WITNESS for descendants that pid-level
|
|
36
|
+
// tracking can no longer reach — but it is never a kill target: terminating it
|
|
37
|
+
// leaves the actual worker running (verified) and would only destroy the
|
|
38
|
+
// witness.
|
|
39
|
+
const CONSOLE_HOST = /^conhost\.exe$/i;
|
|
40
|
+
|
|
41
|
+
export function pidAlive(pid) {
|
|
42
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
43
|
+
try {
|
|
44
|
+
process.kill(pid, 0);
|
|
45
|
+
return true;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
// EPERM: exists but is not ours to signal. ESRCH: gone.
|
|
48
|
+
return error?.code === 'EPERM';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function groupAlive(pgid) {
|
|
53
|
+
if (!Number.isInteger(pgid) || pgid <= 0) return false;
|
|
54
|
+
try {
|
|
55
|
+
process.kill(-pgid, 0);
|
|
56
|
+
return true;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
return error?.code === 'EPERM';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function delay(ms) {
|
|
63
|
+
return new Promise((resolve) => {
|
|
64
|
+
const timer = setTimeout(resolve, ms);
|
|
65
|
+
timer.unref?.();
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Observe what the finished command left running.
|
|
70
|
+
*
|
|
71
|
+
* Returns null when nothing is left (the normal case — the caller then
|
|
72
|
+
* reports a plain completion), or a handle describing the survivors:
|
|
73
|
+
* groupPid POSIX process group that still has members
|
|
74
|
+
* killPids pids a cancellation can signal directly
|
|
75
|
+
* watchPids pids whose liveness answers "is this task still running?"
|
|
76
|
+
* reachable false when survivors are observable but no longer addressable
|
|
77
|
+
* by pid (Windows re-parents nothing: a process whose parent
|
|
78
|
+
* exited keeps a dangling parent id, so an intermediate that
|
|
79
|
+
* already exited breaks the link). */
|
|
80
|
+
export async function probeShellDescendants({ pid, stdioHeld = false } = {}) {
|
|
81
|
+
const root = Number(pid) || 0;
|
|
82
|
+
if (root <= 0) return null;
|
|
83
|
+
if (!isWin) {
|
|
84
|
+
if (!groupAlive(root)) return null;
|
|
85
|
+
return { groupPid: root, killPids: [], watchPids: [], reachable: true };
|
|
86
|
+
}
|
|
87
|
+
if (!stdioHeld) return null;
|
|
88
|
+
const found = await collectDescendantProcesses(root).catch(() => []);
|
|
89
|
+
const live = found.filter((entry) => pidAlive(entry.pid));
|
|
90
|
+
if (live.length === 0) return null;
|
|
91
|
+
const killPids = live.filter((entry) => !CONSOLE_HOST.test(entry.name)).map((entry) => entry.pid);
|
|
92
|
+
return {
|
|
93
|
+
groupPid: null,
|
|
94
|
+
killPids,
|
|
95
|
+
watchPids: live.map((entry) => entry.pid),
|
|
96
|
+
reachable: killPids.length > 0,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** True while the observed survivors are still running. */
|
|
101
|
+
export function descendantsAlive(handle) {
|
|
102
|
+
if (!handle) return false;
|
|
103
|
+
if (handle.groupPid) return groupAlive(handle.groupPid);
|
|
104
|
+
return handle.watchPids.some((pid) => pidAlive(pid));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Resolve once every observed survivor has exited. Poll-based on purpose:
|
|
108
|
+
* these processes are not our children any more, so there is no exit event to
|
|
109
|
+
* subscribe to — only their liveness can be observed. */
|
|
110
|
+
export async function waitForShellDescendants(handle, { pollMs = 1_000, signal = null } = {}) {
|
|
111
|
+
while (descendantsAlive(handle)) {
|
|
112
|
+
if (signal?.aborted) return false;
|
|
113
|
+
await delay(pollMs);
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Terminate the survivors. POSIX signals the whole process group, so the kill
|
|
119
|
+
* is complete by construction; Windows kills every reachable tree and then
|
|
120
|
+
* RE-OBSERVES, so the caller reports what actually died instead of what was
|
|
121
|
+
* signalled. */
|
|
122
|
+
export async function killShellDescendants(handle) {
|
|
123
|
+
if (!handle) return { terminated: true, survivors: [] };
|
|
124
|
+
if (handle.groupPid) {
|
|
125
|
+
try { process.kill(-handle.groupPid, 'SIGTERM'); } catch { /* already gone */ }
|
|
126
|
+
await delay(200);
|
|
127
|
+
if (groupAlive(handle.groupPid)) {
|
|
128
|
+
try { process.kill(-handle.groupPid, 'SIGKILL'); } catch { /* already gone */ }
|
|
129
|
+
await delay(100);
|
|
130
|
+
}
|
|
131
|
+
return { terminated: !groupAlive(handle.groupPid), survivors: [] };
|
|
132
|
+
}
|
|
133
|
+
await killProcessTrees(handle.killPids).catch(() => {});
|
|
134
|
+
const survivors = handle.watchPids.filter((pid) => pidAlive(pid));
|
|
135
|
+
return { terminated: survivors.length === 0, survivors };
|
|
136
|
+
}
|
|
@@ -14,6 +14,9 @@ import {
|
|
|
14
14
|
} from '../builtin.mjs';
|
|
15
15
|
import { atomicWrite } from '../builtin/atomic-write.mjs';
|
|
16
16
|
import { isSpecialFileStat } from '../builtin/device-paths.mjs';
|
|
17
|
+
import { getReadSnapshot } from '../builtin/read-snapshot-runtime.mjs';
|
|
18
|
+
import { snapshotCoversFullFile } from '../builtin/snapshot-helpers.mjs';
|
|
19
|
+
import { hashText } from '../builtin/hash-utils.mjs';
|
|
17
20
|
import { markCodeGraphDirtyPaths } from '../code-graph-state.mjs';
|
|
18
21
|
import {
|
|
19
22
|
classifyEntry,
|
|
@@ -37,6 +40,9 @@ import {
|
|
|
37
40
|
findFirstFailingUnifiedHunk,
|
|
38
41
|
findUnifiedHunkMatch,
|
|
39
42
|
collectUnifiedNewLines,
|
|
43
|
+
collectUnifiedOpEntries,
|
|
44
|
+
terminatorsForUnifiedOps,
|
|
45
|
+
localTerminatorForWindow,
|
|
40
46
|
assertSafeReplacementPlan,
|
|
41
47
|
firstFailingUnifiedHunkLineDetail,
|
|
42
48
|
firstMeaningfulUnifiedHunkLine,
|
|
@@ -47,6 +53,14 @@ import {
|
|
|
47
53
|
normalizeTypographic,
|
|
48
54
|
splitBufferLinesForPatch,
|
|
49
55
|
splitTextLinesForPatch,
|
|
56
|
+
cloneTextLinesForPatch,
|
|
57
|
+
spliceTextLinesForPatch,
|
|
58
|
+
joinTextLinesForPatch,
|
|
59
|
+
setFinalNewlineForPatch,
|
|
60
|
+
patchTargetEncodingError,
|
|
61
|
+
detectDominantEol,
|
|
62
|
+
decodePatchTargetBuffer,
|
|
63
|
+
encodePatchTargetContent,
|
|
50
64
|
} from './matcher.mjs';
|
|
51
65
|
|
|
52
66
|
function formatNativeFailureContext(parsed, basePath, failedPath = '', options = {}) {
|
|
@@ -98,6 +112,50 @@ function formatNativeFailureContext(parsed, basePath, failedPath = '', options =
|
|
|
98
112
|
// caller MUST surface throws as `Error: ...` strings.
|
|
99
113
|
export async function dispatchNativePatch({ entries, basePath, nativePatchStr, fuzz, rejectPartial, dryRun, readStateScope, signal, parsed }) {
|
|
100
114
|
const nativeStart = performance.now();
|
|
115
|
+
// Pre-write identity of every target. The read-snapshot recorder needs it to
|
|
116
|
+
// decide whether the session's earlier full read still described this file
|
|
117
|
+
// when the patch landed — otherwise an external change made before the patch
|
|
118
|
+
// would stay hidden behind "[file unchanged]".
|
|
119
|
+
const preMutationStats = new Map();
|
|
120
|
+
// Encoding gate, same rule as the JS writer: the native engine rewrites files
|
|
121
|
+
// as UTF-8, so a target that is not valid UTF-8 must be refused rather than
|
|
122
|
+
// silently transcoded. (UTF-16 targets are routed away before this point.)
|
|
123
|
+
for (const entry of entries || []) {
|
|
124
|
+
if (entry.kind === 'create' || entry.kind === 'delete') continue;
|
|
125
|
+
const encodingError = patchTargetEncodingError(entry.fullPath, entry.displayPath);
|
|
126
|
+
if (encodingError) return `Error: ${encodingError}`;
|
|
127
|
+
}
|
|
128
|
+
if (!dryRun) {
|
|
129
|
+
for (const entry of entries || []) {
|
|
130
|
+
try { preMutationStats.set(entry.fullPath, lstatSync(entry.fullPath)); } catch { /* absent target */ }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Body-knowledge proof for the "[file unchanged]" fast path. A stat captured
|
|
134
|
+
// before the engine runs cannot vouch for the bytes it actually patched, so
|
|
135
|
+
// predict the post-apply content from bytes we verify against the session's
|
|
136
|
+
// read snapshot; after the apply, the engine's own content hash must equal
|
|
137
|
+
// that prediction. Only computed where the fast path could be inherited.
|
|
138
|
+
const bodyProofs = new Map();
|
|
139
|
+
if (!dryRun && readStateScope) {
|
|
140
|
+
for (const entry of entries || []) {
|
|
141
|
+
if (entry.kind === 'create' || entry.kind === 'delete') continue;
|
|
142
|
+
const prior = getReadSnapshot(entry.fullPath, readStateScope);
|
|
143
|
+
if (!prior?.contentHash) continue;
|
|
144
|
+
if (prior.bodyDelivered !== true && !snapshotCoversFullFile(prior)) continue;
|
|
145
|
+
try {
|
|
146
|
+
const { text } = decodePatchTargetBuffer(readFileSync(entry.fullPath), entry.displayPath);
|
|
147
|
+
if (hashText(text) !== prior.contentHash) continue; // already stale — claim nothing
|
|
148
|
+
const parsedEntry = findParsedForRow(entry, parsed, basePath);
|
|
149
|
+
if (!parsedEntry) continue;
|
|
150
|
+
const updated = applyUnifiedHunksToLines(splitTextLinesForPatch(text), parsedEntry.hunks || [], {
|
|
151
|
+
fuzz: Number.isFinite(fuzz) ? fuzz : 0,
|
|
152
|
+
displayPath: entry.displayPath,
|
|
153
|
+
eol: detectDominantEol(text),
|
|
154
|
+
});
|
|
155
|
+
bodyProofs.set(entry.fullPath, hashText(joinTextLinesForPatch(updated)));
|
|
156
|
+
} catch { /* no proof — the fast path stays off for this file */ }
|
|
157
|
+
}
|
|
158
|
+
}
|
|
101
159
|
let stats;
|
|
102
160
|
try {
|
|
103
161
|
stats = await runServerApply(basePath, nativePatchStr, { fuzz, rejectPartial, dryRun, signal });
|
|
@@ -147,11 +205,33 @@ export async function dispatchNativePatch({ entries, basePath, nativePatchStr, f
|
|
|
147
205
|
if (entry.kind === 'delete') {
|
|
148
206
|
clearReadSnapshotForPath(entry.fullPath, readStateScope);
|
|
149
207
|
} else {
|
|
208
|
+
const contentHash = stats.contentHashes?.[i] || null;
|
|
209
|
+
// Only a matching prediction proves the engine patched the bytes this
|
|
210
|
+
// session had read; otherwise the pre-mutation stat is not evidence.
|
|
211
|
+
const proof = bodyProofs.get(entry.fullPath);
|
|
212
|
+
const provenSameBytes = !!proof && !!contentHash && proof === contentHash;
|
|
213
|
+
// ONE consistent observation of the result, stat FIRST: a write that
|
|
214
|
+
// lands after the stat breaks the hash (claim dropped); one that lands
|
|
215
|
+
// after the read leaves the recorded stat older than the file, so the
|
|
216
|
+
// fast path fails closed at read time. Without that pairing an external
|
|
217
|
+
// write between the apply and this record could inherit bodyDelivered.
|
|
218
|
+
let observedStat = null;
|
|
219
|
+
let observationConsistent = false;
|
|
220
|
+
if (provenSameBytes) {
|
|
221
|
+
try {
|
|
222
|
+
observedStat = lstatSync(entry.fullPath);
|
|
223
|
+
const observed = decodePatchTargetBuffer(readFileSync(entry.fullPath), entry.displayPath);
|
|
224
|
+
observationConsistent = hashText(observed.text) === contentHash;
|
|
225
|
+
} catch {
|
|
226
|
+
observationConsistent = false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
150
229
|
const snapshotMeta = {
|
|
151
230
|
source: 'apply_patch_native',
|
|
152
231
|
isPartialView: false,
|
|
232
|
+
preMutationStat: observationConsistent ? (preMutationStats.get(entry.fullPath) || null) : null,
|
|
153
233
|
};
|
|
154
|
-
|
|
234
|
+
if (observationConsistent && observedStat) snapshotMeta.st = observedStat;
|
|
155
235
|
if (contentHash) snapshotMeta.contentHash = contentHash;
|
|
156
236
|
recordReadSnapshotForPath(entry.fullPath, readStateScope, snapshotMeta);
|
|
157
237
|
}
|
|
@@ -211,34 +291,6 @@ function entryPathKey(fullPath) {
|
|
|
211
291
|
return process.platform === 'win32' ? String(fullPath || '').toLowerCase() : String(fullPath || '');
|
|
212
292
|
}
|
|
213
293
|
|
|
214
|
-
function joinTextLinesForPatch(lines) {
|
|
215
|
-
const eol = lines?.eol || '\n';
|
|
216
|
-
const body = (lines || []).join(eol);
|
|
217
|
-
return lines?.hasFinalNewline !== false ? `${body}${eol}` : body;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// The file's dominant line terminator. splitTextLinesForPatch() folds CRLF into
|
|
221
|
-
// LF, so without this an out-of-base CRLF file would be rewritten as LF.
|
|
222
|
-
function detectDominantEol(text) {
|
|
223
|
-
const body = String(text ?? '');
|
|
224
|
-
if (body.includes('\r') && !body.includes('\n')) return '\r';
|
|
225
|
-
let lf = 0;
|
|
226
|
-
let crlf = 0;
|
|
227
|
-
for (let i = 0; i < body.length; i++) {
|
|
228
|
-
if (body[i] !== '\n') continue;
|
|
229
|
-
lf++;
|
|
230
|
-
if (i > 0 && body[i - 1] === '\r') crlf++;
|
|
231
|
-
}
|
|
232
|
-
return lf > 0 && crlf * 2 >= lf ? '\r\n' : '\n';
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function cloneTextLinesForPatch(sourceLines, eol) {
|
|
236
|
-
const lines = [...(sourceLines || [])];
|
|
237
|
-
lines.hasFinalNewline = sourceLines?.hasFinalNewline !== false;
|
|
238
|
-
lines.eol = eol || sourceLines?.eol || '\n';
|
|
239
|
-
return lines;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
294
|
// Which side of the hunk, if any, carries an explicit
|
|
243
295
|
// `` marker. Both transitions must be honoured:
|
|
244
296
|
// a marker on the NEW side drops the terminator, a marker on the OLD side
|
|
@@ -300,6 +352,10 @@ function applyUnifiedHunksToLines(sourceLines, hunks, { fuzz, displayPath, eol }
|
|
|
300
352
|
start: match.start,
|
|
301
353
|
oldLen: match.end - match.start,
|
|
302
354
|
newLines,
|
|
355
|
+
// Op sequence of THIS hunk: the only sound source for terminator
|
|
356
|
+
// inheritance (context keeps its own, an add takes the delete it
|
|
357
|
+
// replaces). Content alignment cannot express interior context or moves.
|
|
358
|
+
ops: collectUnifiedOpEntries(hunk),
|
|
303
359
|
intent,
|
|
304
360
|
atEof: match.end >= lines.length,
|
|
305
361
|
});
|
|
@@ -309,12 +365,20 @@ function applyUnifiedHunksToLines(sourceLines, hunks, { fuzz, displayPath, eol }
|
|
|
309
365
|
assertSafeReplacementPlan(replacements, `apply_patch: ${displayPath}`);
|
|
310
366
|
for (let i = replacements.length - 1; i >= 0; i--) {
|
|
311
367
|
const rep = replacements[i];
|
|
312
|
-
|
|
368
|
+
const oldTerms = Array.isArray(lines.terminators)
|
|
369
|
+
? lines.terminators.slice(rep.start, rep.start + rep.oldLen)
|
|
370
|
+
: [];
|
|
371
|
+
const newTerms = terminatorsForUnifiedOps(
|
|
372
|
+
rep.ops,
|
|
373
|
+
oldTerms,
|
|
374
|
+
localTerminatorForWindow(lines, rep.start, rep.oldLen),
|
|
375
|
+
);
|
|
376
|
+
spliceTextLinesForPatch(lines, rep.start, rep.oldLen, rep.newLines, newTerms);
|
|
313
377
|
// Explicit markers win in BOTH directions; a hunk that is silent about the
|
|
314
378
|
// terminator leaves the file's existing state untouched (native parity).
|
|
315
379
|
if (!rep.atEof) continue;
|
|
316
|
-
if (rep.intent.newNoNewline) lines
|
|
317
|
-
else if (rep.intent.oldNoNewline) lines
|
|
380
|
+
if (rep.intent.newNoNewline) setFinalNewlineForPatch(lines, false);
|
|
381
|
+
else if (rep.intent.oldNoNewline) setFinalNewlineForPatch(lines, true);
|
|
318
382
|
}
|
|
319
383
|
return lines;
|
|
320
384
|
}
|
|
@@ -401,21 +465,46 @@ async function applyJsParsedEntry(entry, basePath, { dryRun, fuzzy, readStateSco
|
|
|
401
465
|
return { kind, fullPath, displayPath, added, removed };
|
|
402
466
|
}
|
|
403
467
|
|
|
404
|
-
lstatRegularPatchFile(fullPath, displayPath);
|
|
405
|
-
|
|
406
|
-
|
|
468
|
+
const preMutationStat = lstatRegularPatchFile(fullPath, displayPath);
|
|
469
|
+
// Rewrite path: decode by BOM, re-encode into the same codec, and keep every
|
|
470
|
+
// untouched line's own terminator (spliceTextLinesForPatch).
|
|
471
|
+
// `toString('utf8')` here transcoded non-UTF-8 files and mangled UTF-16.
|
|
472
|
+
const { text: rawText, enc: rawEnc } = decodePatchTargetBuffer(readFileSync(fullPath), displayPath);
|
|
407
473
|
const sourceLines = splitTextLinesForPatch(rawText);
|
|
408
474
|
const updatedLines = applyUnifiedHunksToLines(sourceLines, entry.hunks || [], {
|
|
409
475
|
fuzz: fuzzy ? 2 : 0,
|
|
410
476
|
displayPath,
|
|
411
477
|
eol: detectDominantEol(rawText),
|
|
412
478
|
});
|
|
413
|
-
const content = joinTextLinesForPatch(updatedLines);
|
|
479
|
+
const content = encodePatchTargetContent(joinTextLinesForPatch(updatedLines), rawEnc);
|
|
414
480
|
if (!dryRun) {
|
|
415
|
-
|
|
481
|
+
// The expected-target snapshot closes the read→write window: if anything
|
|
482
|
+
// rewrote this file after we read it, the write fails instead of silently
|
|
483
|
+
// clobbering it (and instead of inheriting a stale "body delivered" claim).
|
|
484
|
+
try {
|
|
485
|
+
await atomicWrite(fullPath, content, {
|
|
486
|
+
sessionId: readStateScope,
|
|
487
|
+
expectedTargetSnapshot: preMutationStat ? {
|
|
488
|
+
exists: true,
|
|
489
|
+
size: preMutationStat.size,
|
|
490
|
+
mtimeMs: preMutationStat.mtimeMs,
|
|
491
|
+
ctimeMs: preMutationStat.ctimeMs,
|
|
492
|
+
ino: preMutationStat.ino,
|
|
493
|
+
} : undefined,
|
|
494
|
+
});
|
|
495
|
+
} catch (err) {
|
|
496
|
+
if (err?.code === 'ESTALE_TARGET') {
|
|
497
|
+
throw new Error(`apply_patch: ${normalizeOutputPath(displayPath)} changed on disk during the patch; re-read it and retry`);
|
|
498
|
+
}
|
|
499
|
+
throw err;
|
|
500
|
+
}
|
|
416
501
|
invalidateBuiltinResultCache([fullPath]);
|
|
417
502
|
markCodeGraphDirtyPaths([fullPath]);
|
|
418
|
-
recordReadSnapshotForPath(fullPath, readStateScope, {
|
|
503
|
+
recordReadSnapshotForPath(fullPath, readStateScope, {
|
|
504
|
+
source: 'apply_patch_js',
|
|
505
|
+
isPartialView: false,
|
|
506
|
+
preMutationStat,
|
|
507
|
+
});
|
|
419
508
|
}
|
|
420
509
|
const { added, removed } = countHunkChanges(entry.hunks);
|
|
421
510
|
return { kind, fullPath, displayPath, added, removed };
|