lark-remote 0.1.5 → 0.1.7
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/CHANGELOG.md +39 -0
- package/README.en.md +5 -1
- package/README.md +10 -2
- package/dist/bridge/approval-coordinator.d.ts +131 -0
- package/dist/bridge/approval-coordinator.js +409 -0
- package/dist/bridge/inbound-media.d.ts +62 -0
- package/dist/bridge/inbound-media.js +282 -0
- package/dist/bridge/index.d.ts +138 -5
- package/dist/bridge/index.js +569 -65
- package/dist/bridge/queue-manager.d.ts +11 -1
- package/dist/bridge/queue-manager.js +20 -11
- package/dist/card/approval-render.d.ts +16 -0
- package/dist/card/approval-render.js +363 -0
- package/dist/card/bash-card-session.d.ts +2 -24
- package/dist/card/bash-card-session.js +6 -73
- package/dist/card/bash-renderer.js +2 -3
- package/dist/card/card-budget.js +29 -75
- package/dist/card/card-session.d.ts +48 -0
- package/dist/card/card-session.js +86 -0
- package/dist/card/card-shared.d.ts +13 -10
- package/dist/card/card-shared.js +27 -24
- package/dist/card/run-card-session.d.ts +2 -55
- package/dist/card/run-card-session.js +6 -104
- package/dist/card/run-renderer.js +140 -202
- package/dist/card/run-state.d.ts +38 -3
- package/dist/card/run-state.js +221 -11
- package/dist/card/text-truncate.d.ts +13 -12
- package/dist/card/text-truncate.js +13 -63
- package/dist/common/fs.d.ts +8 -0
- package/dist/common/fs.js +17 -0
- package/dist/common/guards.d.ts +0 -20
- package/dist/common/guards.js +0 -43
- package/dist/common/sleep.d.ts +2 -0
- package/dist/common/sleep.js +5 -0
- package/dist/common/truncate.d.ts +39 -0
- package/dist/common/truncate.js +92 -0
- package/dist/config/codex-config.d.ts +1 -1
- package/dist/config/codex-config.js +1 -1
- package/dist/config/dir.js +1 -1
- package/dist/config/index.d.ts +116 -9
- package/dist/config/index.js +122 -29
- package/dist/connector/index.d.ts +74 -2
- package/dist/connector/index.js +119 -1
- package/dist/index.js +139 -99
- package/dist/instance-lock.js +4 -12
- package/dist/order/alias-resolve.d.ts +13 -0
- package/dist/order/alias-resolve.js +22 -0
- package/dist/order/alias-store.d.ts +38 -0
- package/dist/order/alias-store.js +106 -0
- package/dist/persistence/atomic-write.d.ts +8 -2
- package/dist/persistence/atomic-write.js +23 -7
- package/dist/restart.js +2 -1
- package/dist/router/card-helpers.d.ts +63 -0
- package/dist/router/card-helpers.js +146 -0
- package/dist/router/config/claude.js +25 -1
- package/dist/router/config/codex.d.ts +1 -1
- package/dist/router/config/codex.js +33 -5
- package/dist/router/config/common/model-patch.d.ts +22 -0
- package/dist/router/config/common/model-patch.js +26 -0
- package/dist/router/config/common/render.js +9 -4
- package/dist/router/config/index.d.ts +12 -0
- package/dist/router/config/index.js +25 -1
- package/dist/router/config/kimi.d.ts +4 -1
- package/dist/router/config/kimi.js +27 -0
- package/dist/router/config/opencode.js +18 -5
- package/dist/router/config/pi.js +4 -5
- package/dist/router/config/types.d.ts +12 -3
- package/dist/router/index.d.ts +77 -17
- package/dist/router/index.js +455 -549
- package/dist/router/utils.d.ts +4 -0
- package/dist/router/utils.js +9 -2
- package/dist/runner/agent-choices-common.d.ts +16 -0
- package/dist/runner/agent-choices-common.js +31 -0
- package/dist/runner/claude/runner.d.ts +47 -10
- package/dist/runner/claude/runner.js +117 -46
- package/dist/runner/claude/session.d.ts +166 -0
- package/dist/runner/claude/session.js +749 -0
- package/dist/runner/codex/app-server/protocol-types.d.ts +519 -0
- package/dist/runner/codex/app-server/protocol-types.js +56 -0
- package/dist/runner/codex/app-server/runner.d.ts +104 -0
- package/dist/runner/codex/app-server/runner.js +377 -0
- package/dist/runner/codex/app-server/translator.d.ts +89 -0
- package/dist/runner/codex/app-server/translator.js +716 -0
- package/dist/runner/codex/index.d.ts +1 -1
- package/dist/runner/codex/index.js +1 -1
- package/dist/runner/common/acp/protocol-helpers.d.ts +14 -0
- package/dist/runner/common/acp/protocol-helpers.js +21 -0
- package/dist/runner/common/acp/protocol-types.d.ts +238 -0
- package/dist/runner/common/acp/protocol-types.js +75 -0
- package/dist/runner/common/connection-based-runner.d.ts +111 -0
- package/dist/runner/common/connection-based-runner.js +261 -0
- package/dist/runner/common/jsonrpc/client.d.ts +89 -0
- package/dist/runner/common/jsonrpc/client.js +243 -0
- package/dist/runner/common/jsonrpc/connection-manager.d.ts +125 -0
- package/dist/runner/common/jsonrpc/connection-manager.js +205 -0
- package/dist/runner/common/jsonrpc/transport.d.ts +50 -0
- package/dist/runner/common/jsonrpc/transport.js +230 -0
- package/dist/runner/common/jsonrpc/types.d.ts +32 -0
- package/dist/runner/common/jsonrpc/types.js +9 -0
- package/dist/runner/common/process-stopper.d.ts +2 -1
- package/dist/runner/common/process-stopper.js +1 -0
- package/dist/runner/common/runner-utils.d.ts +6 -13
- package/dist/runner/common/runner-utils.js +6 -47
- package/dist/runner/common/spawning-runner.d.ts +24 -70
- package/dist/runner/common/spawning-runner.js +98 -152
- package/dist/runner/common/usage.d.ts +2 -0
- package/dist/runner/common/usage.js +2 -0
- package/dist/runner/index.d.ts +5 -5
- package/dist/runner/index.js +4 -4
- package/dist/runner/kimi/acp/runner.d.ts +106 -0
- package/dist/runner/kimi/acp/runner.js +448 -0
- package/dist/runner/kimi/acp/translator.d.ts +124 -0
- package/dist/runner/kimi/acp/translator.js +336 -0
- package/dist/runner/kimi/index.d.ts +1 -1
- package/dist/runner/kimi/index.js +1 -1
- package/dist/runner/opencode/acp/index.d.ts +1 -0
- package/dist/runner/opencode/acp/index.js +2 -0
- package/dist/runner/opencode/acp/runner.d.ts +121 -0
- package/dist/runner/opencode/acp/runner.js +429 -0
- package/dist/runner/opencode/acp/translator.d.ts +119 -0
- package/dist/runner/opencode/acp/translator.js +345 -0
- package/dist/runner/opencode/index.d.ts +1 -1
- package/dist/runner/opencode/index.js +1 -1
- package/dist/runner/pi/index.d.ts +1 -1
- package/dist/runner/pi/index.js +1 -1
- package/dist/runner/pi/rpc/client.d.ts +52 -0
- package/dist/runner/pi/rpc/client.js +114 -0
- package/dist/runner/pi/rpc/index.d.ts +4 -0
- package/dist/runner/pi/rpc/index.js +4 -0
- package/dist/runner/pi/rpc/protocol-types.d.ts +127 -0
- package/dist/runner/pi/rpc/protocol-types.js +21 -0
- package/dist/runner/pi/rpc/runner.d.ts +80 -0
- package/dist/runner/pi/rpc/runner.js +199 -0
- package/dist/runner/pi/rpc/translator.d.ts +55 -0
- package/dist/runner/pi/rpc/translator.js +258 -0
- package/dist/runner/registry.d.ts +7 -8
- package/dist/runner/registry.js +3 -4
- package/dist/runner/resolve-agent-choices.js +18 -59
- package/dist/runner/sync-agent-choices.js +16 -67
- package/dist/runner/types.d.ts +153 -2
- package/dist/session/claude/session-index.js +2 -3
- package/dist/session/claude/session-reader.js +3 -6
- package/dist/session/claude/sessions.js +6 -13
- package/dist/session/codex/rollout-reader.d.ts +2 -2
- package/dist/session/codex/rollout-reader.js +57 -17
- package/dist/session/codex/sessions.d.ts +1 -1
- package/dist/session/codex/sessions.js +2 -2
- package/dist/session/common/content-blocks.d.ts +7 -0
- package/dist/session/common/content-blocks.js +13 -9
- package/dist/session/common/jsonl.d.ts +8 -25
- package/dist/session/common/jsonl.js +126 -261
- package/dist/session/common/pagination.d.ts +28 -0
- package/dist/session/common/pagination.js +33 -0
- package/dist/session/kimi/sessions.d.ts +34 -1
- package/dist/session/kimi/sessions.js +208 -98
- package/dist/session/opencode/sessions.d.ts +17 -3
- package/dist/session/opencode/sessions.js +29 -29
- package/dist/session/pi/sessions.d.ts +5 -1
- package/dist/session/pi/sessions.js +12 -16
- package/dist/session/session-store.d.ts +11 -1
- package/dist/session/session-store.js +75 -178
- package/dist/update/index.d.ts +2 -2
- package/dist/update/index.js +2 -2
- package/dist/update/install.d.ts +20 -2
- package/dist/update/install.js +51 -7
- package/package.json +1 -1
- package/dist/runner/codex/argv.d.ts +0 -33
- package/dist/runner/codex/argv.js +0 -40
- package/dist/runner/codex/jsonl.d.ts +0 -37
- package/dist/runner/codex/jsonl.js +0 -221
- package/dist/runner/codex/runner.d.ts +0 -63
- package/dist/runner/codex/runner.js +0 -124
- package/dist/runner/common/exec-translator.d.ts +0 -73
- package/dist/runner/common/exec-translator.js +0 -105
- package/dist/runner/kimi/jsonl.d.ts +0 -28
- package/dist/runner/kimi/jsonl.js +0 -143
- package/dist/runner/kimi/runner.d.ts +0 -49
- package/dist/runner/kimi/runner.js +0 -105
- package/dist/runner/opencode/argv.d.ts +0 -32
- package/dist/runner/opencode/argv.js +0 -35
- package/dist/runner/opencode/jsonl.d.ts +0 -68
- package/dist/runner/opencode/jsonl.js +0 -295
- package/dist/runner/opencode/runner.d.ts +0 -61
- package/dist/runner/opencode/runner.js +0 -93
- package/dist/runner/pi/jsonl.d.ts +0 -128
- package/dist/runner/pi/jsonl.js +0 -249
- package/dist/runner/pi/runner.d.ts +0 -36
- package/dist/runner/pi/runner.js +0 -76
- package/dist/session/common/catch-up.d.ts +0 -12
- package/dist/session/common/catch-up.js +0 -31
|
@@ -1,21 +1,14 @@
|
|
|
1
|
-
import type { ChildProcess } from 'node:child_process';
|
|
2
1
|
import type { AgentEvent } from '../types.js';
|
|
3
2
|
/**
|
|
4
|
-
* Return the stdio config ['pipe','pipe','pipe'] used by
|
|
5
|
-
*
|
|
3
|
+
* Return the stdio config ['pipe','pipe','pipe'] used by the runner that
|
|
4
|
+
* writes the prompt to the child's stdin (claude).
|
|
6
5
|
*/
|
|
7
6
|
export declare function pipeAllStdio(): ('ignore' | 'pipe')[];
|
|
8
|
-
/**
|
|
9
|
-
* Write the prompt message to the child's stdin and close it.
|
|
10
|
-
* Used by codex/opencode runners which pass the prompt via stdin
|
|
11
|
-
* rather than argv.
|
|
12
|
-
*/
|
|
13
|
-
export declare function endStdinWithPrompt(proc: ChildProcess, message: string): void;
|
|
14
7
|
/**
|
|
15
8
|
* Build a synthetic system.init event for pre-spawn failure paths.
|
|
16
9
|
*
|
|
17
|
-
* When the runner exits before spawning a process (
|
|
18
|
-
*
|
|
10
|
+
* When the runner exits before spawning a process (e.g. ENOENT spawn failure),
|
|
11
|
+
* no real system.init arrives from stdout. Without a
|
|
19
12
|
* synthetic init, the bridge's pre-init result guard (§9.22) and the run-state
|
|
20
13
|
* reducer's sessionId check would silently drop the error result, leaving the
|
|
21
14
|
* card showing "输出流已结束,但未收到 result 事件" instead of the actual
|
|
@@ -26,8 +19,8 @@ export declare function syntheticInitEvent(sessionId?: string): AgentEvent;
|
|
|
26
19
|
/**
|
|
27
20
|
* Build a standardized auth/error result event for yield.
|
|
28
21
|
*
|
|
29
|
-
* Used
|
|
30
|
-
*
|
|
22
|
+
* Used when initialization or request sending fails (e.g. not logged in,
|
|
23
|
+
* session creation failed). Produces a
|
|
31
24
|
* `result`/`error` event so the bridge can render a card instead of crashing.
|
|
32
25
|
*/
|
|
33
26
|
export declare function authErrorEvent(errorMessage: string, sessionId?: string): AgentEvent;
|
|
@@ -1,56 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Return the stdio config ['pipe','pipe','pipe'] used by
|
|
3
|
-
*
|
|
2
|
+
* Return the stdio config ['pipe','pipe','pipe'] used by the runner that
|
|
3
|
+
* writes the prompt to the child's stdin (claude).
|
|
4
4
|
*/
|
|
5
5
|
export function pipeAllStdio() {
|
|
6
6
|
return ['pipe', 'pipe', 'pipe'];
|
|
7
7
|
}
|
|
8
|
-
/**
|
|
9
|
-
* Write the prompt message to the child's stdin and close it.
|
|
10
|
-
* Used by codex/opencode runners which pass the prompt via stdin
|
|
11
|
-
* rather than argv.
|
|
12
|
-
*/
|
|
13
|
-
export function endStdinWithPrompt(proc, message) {
|
|
14
|
-
const stdin = proc.stdin;
|
|
15
|
-
if (!stdin)
|
|
16
|
-
return;
|
|
17
|
-
// Guard against async EPIPE: proc.stdin.end() may emit an 'error' event
|
|
18
|
-
// asynchronously after the call returns (the child may have already exited),
|
|
19
|
-
// which the synchronous try/catch below cannot catch.
|
|
20
|
-
// Check for .once availability — in test environments stdin may be a mock
|
|
21
|
-
// object that lacks EventEmitter methods.
|
|
22
|
-
if (typeof stdin.once === 'function') {
|
|
23
|
-
stdin.once('error', (err) => {
|
|
24
|
-
if (err.code === 'EPIPE') {
|
|
25
|
-
// Child exited before we could write — not an error, just a race.
|
|
26
|
-
}
|
|
27
|
-
else {
|
|
28
|
-
// Re-throw unexpected errors so they surface as unhandled-exception
|
|
29
|
-
// rather than being silently swallowed by the EventEmitter default handler.
|
|
30
|
-
throw err;
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
try {
|
|
35
|
-
stdin.end(message, 'utf-8');
|
|
36
|
-
}
|
|
37
|
-
catch (err) {
|
|
38
|
-
if (err &&
|
|
39
|
-
typeof err === 'object' &&
|
|
40
|
-
'code' in err &&
|
|
41
|
-
err.code === 'EPIPE') {
|
|
42
|
-
// Child exited before we could write — not an error, just a race.
|
|
43
|
-
}
|
|
44
|
-
else {
|
|
45
|
-
throw err;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
8
|
/**
|
|
50
9
|
* Build a synthetic system.init event for pre-spawn failure paths.
|
|
51
10
|
*
|
|
52
|
-
* When the runner exits before spawning a process (
|
|
53
|
-
*
|
|
11
|
+
* When the runner exits before spawning a process (e.g. ENOENT spawn failure),
|
|
12
|
+
* no real system.init arrives from stdout. Without a
|
|
54
13
|
* synthetic init, the bridge's pre-init result guard (§9.22) and the run-state
|
|
55
14
|
* reducer's sessionId check would silently drop the error result, leaving the
|
|
56
15
|
* card showing "输出流已结束,但未收到 result 事件" instead of the actual
|
|
@@ -70,8 +29,8 @@ export function syntheticInitEvent(sessionId = '') {
|
|
|
70
29
|
/**
|
|
71
30
|
* Build a standardized auth/error result event for yield.
|
|
72
31
|
*
|
|
73
|
-
* Used
|
|
74
|
-
*
|
|
32
|
+
* Used when initialization or request sending fails (e.g. not logged in,
|
|
33
|
+
* session creation failed). Produces a
|
|
75
34
|
* `result`/`error` event so the bridge can render a card instead of crashing.
|
|
76
35
|
*/
|
|
77
36
|
export function authErrorEvent(errorMessage, sessionId = '') {
|
|
@@ -37,8 +37,8 @@ export declare function unregisterExitCleanup(handler: ExitCleanupHandler): void
|
|
|
37
37
|
*
|
|
38
38
|
* `translate()` returns `AgentEvent | AgentEvent[] | null` (single event,
|
|
39
39
|
* multiple events, or filtered-out). `isTerminal()` / `finish()` / `getTerminalError()`
|
|
40
|
-
* / `hasAgentTerminalError()` are folded into the unified result event
|
|
41
|
-
* `
|
|
40
|
+
* / `hasAgentTerminalError()` are folded into the unified result event at the
|
|
41
|
+
* end of `run()`.
|
|
42
42
|
*
|
|
43
43
|
* `getSessionId()` / `getLastUsage()` are optional because ClaudeRunner uses
|
|
44
44
|
* the stateless `translate()` hook (no translator object) and reads neither.
|
|
@@ -56,26 +56,21 @@ export interface RunnerTranslator {
|
|
|
56
56
|
* Abstract base class for runners that spawn a child process per turn.
|
|
57
57
|
*
|
|
58
58
|
* Encapsulates the spawn + completion + cleanup orchestration shared by
|
|
59
|
-
* Claude/
|
|
59
|
+
* Claude/Pi runners. Subclasses override:
|
|
60
60
|
* - buildArgv(opts) → agent-specific CLI flags (required)
|
|
61
|
-
* - createTranslator(opts) → stateful translator (
|
|
61
|
+
* - createTranslator(opts) → stateful translator (pi)
|
|
62
62
|
* OR translate(raw, ctx) → stateless passthrough (claude)
|
|
63
63
|
*
|
|
64
64
|
* Optional hooks (with sensible defaults):
|
|
65
65
|
* - getStdio() → stdio config (default ['ignore','pipe','pipe'])
|
|
66
|
-
* - getSpawnEnv(opts) → env vars (default process.env)
|
|
67
|
-
* - setupStdin(proc, message) → write prompt to stdin (default no-op)
|
|
68
66
|
* - createStreamReader(stdout) → JSONL or readline parser (default createJSONLStream)
|
|
69
|
-
* -
|
|
70
|
-
* - awaitCompletion(proc, p) → wait for exit (default return p; kimi adds 5s race)
|
|
71
|
-
* - awaitSpawnError(proc) → wait for spawn error (default once('error'); kimi adds 5s race)
|
|
72
|
-
* - resolveTranslatorError(t, c) → translatorError precedence (default honors agent errors)
|
|
67
|
+
* - awaitSpawnError(proc) → wait for spawn error (default once('error'))
|
|
73
68
|
*
|
|
74
69
|
* The base class always emits a unified result event at the end of `run()`
|
|
75
70
|
* via `buildResultEvent(...)` — never throws on non-zero exit. This matches
|
|
76
|
-
* the contract pinned by
|
|
77
|
-
*
|
|
78
|
-
*
|
|
71
|
+
* the contract pinned by the agent subclass anchors and the bridge's
|
|
72
|
+
* `runAgentStreamToEnd` error handling (which accepts both thrown errors and
|
|
73
|
+
* yielded error-result events).
|
|
79
74
|
*/
|
|
80
75
|
export declare abstract class SpawningRunner {
|
|
81
76
|
protected currentProcess: ChildProcess | null;
|
|
@@ -84,6 +79,8 @@ export declare abstract class SpawningRunner {
|
|
|
84
79
|
protected readonly spawnHeartbeat: SpawnHeartbeat;
|
|
85
80
|
protected binary: string;
|
|
86
81
|
protected stopGraceMs: number;
|
|
82
|
+
/** Accumulated stderr tail for the current run (filled by spawnChild). */
|
|
83
|
+
protected spawnStderr: string;
|
|
87
84
|
/**
|
|
88
85
|
* Log prefix used in all operational log lines emitted by this runner
|
|
89
86
|
* (spawn, pid file, non-zero exit, stderr, killOrphan, stop cleanup) and
|
|
@@ -131,58 +128,19 @@ export declare abstract class SpawningRunner {
|
|
|
131
128
|
get isRunning(): boolean;
|
|
132
129
|
/**
|
|
133
130
|
* stdio config for spawn. Default: stdin ignored (claude/kimi/pi pass the
|
|
134
|
-
* prompt via argv).
|
|
135
|
-
* they can write the prompt to stdin via `setupStdin`.
|
|
131
|
+
* prompt via argv).
|
|
136
132
|
*/
|
|
137
133
|
protected getStdio(): ('ignore' | 'pipe')[];
|
|
138
|
-
/**
|
|
139
|
-
* Env vars for spawn. Default: process.env. opencode overrides to sync
|
|
140
|
-
* PWD=opts.cwd (opencode's directory detection reads PWD, not cwd).
|
|
141
|
-
*/
|
|
142
|
-
protected getSpawnEnv(_opts: SpawnOptions): NodeJS.ProcessEnv;
|
|
143
|
-
/**
|
|
144
|
-
* Write the prompt to the child's stdin (called after spawn succeeds).
|
|
145
|
-
* Default: no-op. codex/opencode override to `proc.stdin.end(message)`.
|
|
146
|
-
*/
|
|
147
|
-
protected setupStdin(_proc: ChildProcess, _message: string): void;
|
|
148
134
|
/**
|
|
149
135
|
* Create an async iterator over the child's stdout. Default: createJSONLStream
|
|
150
136
|
* (claude/kimi/pi) with P1-4 backpressure enabled (pauseThreshold=100).
|
|
151
|
-
* Subclasses that need to silently skip non-JSON lines should override
|
|
152
|
-
* `handleBadStreamLine` instead of this method.
|
|
153
137
|
*/
|
|
154
138
|
protected createStreamReader(stdout: Readable): AsyncGenerator<unknown>;
|
|
155
|
-
/**
|
|
156
|
-
* Hook called when createJSONLStream encounters a line that cannot be parsed
|
|
157
|
-
* as JSON. Default: log a warning via getLogger().warn (same format as
|
|
158
|
-
* jsonl-stream's built-in warning). Subclasses override to change behavior
|
|
159
|
-
* (e.g. codex/opencode silently skip by providing an empty override).
|
|
160
|
-
*/
|
|
161
|
-
protected handleBadStreamLine(line: string): void;
|
|
162
139
|
/**
|
|
163
140
|
* Create a stateful translator for this run. Default: null (uses the
|
|
164
|
-
* stateless
|
|
165
|
-
* return their translator instance.
|
|
141
|
+
* stateless passthrough). pi overrides to return its translator instance.
|
|
166
142
|
*/
|
|
167
143
|
protected createTranslator(_opts: SpawnOptions): RunnerTranslator | null;
|
|
168
|
-
/**
|
|
169
|
-
* Pre-spawn validation. Return an AgentEvent to yield it and abort run()
|
|
170
|
-
* (e.g. codex's API key check yields an error result and returns).
|
|
171
|
-
* Default: null (no validation).
|
|
172
|
-
*/
|
|
173
|
-
protected validateBeforeRun(_opts: SpawnOptions): AgentEvent | null;
|
|
174
|
-
/**
|
|
175
|
-
* Await process completion. Default: return the completion promise as-is.
|
|
176
|
-
* kimi overrides to add a 5s timeout race + SIGKILL fallback (kimi
|
|
177
|
-
* sometimes hangs after stdout closes).
|
|
178
|
-
*/
|
|
179
|
-
protected awaitCompletion(_proc: ChildProcess, completion: Promise<{
|
|
180
|
-
code: number | null;
|
|
181
|
-
signal: NodeJS.Signals | null;
|
|
182
|
-
}>): Promise<{
|
|
183
|
-
code: number | null;
|
|
184
|
-
signal: NodeJS.Signals | null;
|
|
185
|
-
}>;
|
|
186
144
|
/**
|
|
187
145
|
* Await the spawn 'error' event when proc.pid === undefined. Races a
|
|
188
146
|
* finite timeout (review P2-11) against the 'error' event: Node guarantees
|
|
@@ -194,24 +152,20 @@ export declare abstract class SpawningRunner {
|
|
|
194
152
|
*/
|
|
195
153
|
protected awaitSpawnError(proc: ChildProcess): Promise<Error | undefined>;
|
|
196
154
|
/**
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
* (signal / non-zero code / user stop). The signal/code is the root
|
|
205
|
-
* cause; the early stream-end is a symptom.
|
|
206
|
-
* - otherwise: undefined (signal/code/stoppedByUser drives the message).
|
|
155
|
+
* Spawn the agent binary with the subclass's argv and run the shared spawn
|
|
156
|
+
* lead-in: pid-undefined check, pid-file write, heartbeat start, and stderr
|
|
157
|
+
* accumulation (tail-capped).
|
|
158
|
+
*
|
|
159
|
+
* Throws a `SpawnChildError` (carrying the user-facing spawn failure
|
|
160
|
+
* message) when the process fails to spawn (binary missing / pid undefined).
|
|
161
|
+
* On success returns the spawned child plus the accumulated stderr tail.
|
|
207
162
|
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
163
|
+
* Shared by the base `run()` (yields an authErrorEvent on failure) and
|
|
164
|
+
* ClaudeSession's `doStartProcess` (returns the error message), which would
|
|
165
|
+
* otherwise duplicate ~60 lines of spawn lead-in with two copies of the
|
|
166
|
+
* STDERR_TAIL_BYTES cap.
|
|
210
167
|
*/
|
|
211
|
-
protected
|
|
212
|
-
code: number | null;
|
|
213
|
-
signal: NodeJS.Signals | null;
|
|
214
|
-
}): string | undefined;
|
|
168
|
+
protected spawnChild(opts: SpawnOptions): Promise<ChildProcess>;
|
|
215
169
|
/**
|
|
216
170
|
* Spawn the agent binary with argv built by buildArgv(opts), iterate its
|
|
217
171
|
* stdout stream, and yield translated events. Always emits a unified
|
|
@@ -2,6 +2,7 @@ import { spawn, execFileSync } from 'node:child_process';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import fs from 'node:fs';
|
|
5
|
+
import { silentlyUnlink } from '../../common/fs.js';
|
|
5
6
|
import { getLogger } from '../../logger/index.js';
|
|
6
7
|
import { ProcessStopper } from './process-stopper.js';
|
|
7
8
|
import { SpawnHeartbeat } from './spawn-heartbeat.js';
|
|
@@ -17,6 +18,9 @@ import { DEFAULT_STOP_GRACE_MS } from '../../config/index.js';
|
|
|
17
18
|
const STDERR_TAIL_BYTES = 4000;
|
|
18
19
|
/** Max bytes of stderr surfaced in the non-zero-exit log line. */
|
|
19
20
|
const STDERR_LOG_TAIL = 500;
|
|
21
|
+
/** Error thrown by `spawnChild` when the child process fails to spawn. */
|
|
22
|
+
class SpawnChildError extends Error {
|
|
23
|
+
}
|
|
20
24
|
/**
|
|
21
25
|
* Timeout (ms) for awaiting the spawn 'error' event when proc.pid === undefined
|
|
22
26
|
* (review P2-11). Node guarantees 'error' for ENOENT/EACCES, but some binaries
|
|
@@ -58,26 +62,21 @@ export function unregisterExitCleanup(handler) {
|
|
|
58
62
|
* Abstract base class for runners that spawn a child process per turn.
|
|
59
63
|
*
|
|
60
64
|
* Encapsulates the spawn + completion + cleanup orchestration shared by
|
|
61
|
-
* Claude/
|
|
65
|
+
* Claude/Pi runners. Subclasses override:
|
|
62
66
|
* - buildArgv(opts) → agent-specific CLI flags (required)
|
|
63
|
-
* - createTranslator(opts) → stateful translator (
|
|
67
|
+
* - createTranslator(opts) → stateful translator (pi)
|
|
64
68
|
* OR translate(raw, ctx) → stateless passthrough (claude)
|
|
65
69
|
*
|
|
66
70
|
* Optional hooks (with sensible defaults):
|
|
67
71
|
* - getStdio() → stdio config (default ['ignore','pipe','pipe'])
|
|
68
|
-
* - getSpawnEnv(opts) → env vars (default process.env)
|
|
69
|
-
* - setupStdin(proc, message) → write prompt to stdin (default no-op)
|
|
70
72
|
* - createStreamReader(stdout) → JSONL or readline parser (default createJSONLStream)
|
|
71
|
-
* -
|
|
72
|
-
* - awaitCompletion(proc, p) → wait for exit (default return p; kimi adds 5s race)
|
|
73
|
-
* - awaitSpawnError(proc) → wait for spawn error (default once('error'); kimi adds 5s race)
|
|
74
|
-
* - resolveTranslatorError(t, c) → translatorError precedence (default honors agent errors)
|
|
73
|
+
* - awaitSpawnError(proc) → wait for spawn error (default once('error'))
|
|
75
74
|
*
|
|
76
75
|
* The base class always emits a unified result event at the end of `run()`
|
|
77
76
|
* via `buildResultEvent(...)` — never throws on non-zero exit. This matches
|
|
78
|
-
* the contract pinned by
|
|
79
|
-
*
|
|
80
|
-
*
|
|
77
|
+
* the contract pinned by the agent subclass anchors and the bridge's
|
|
78
|
+
* `runAgentStreamToEnd` error handling (which accepts both thrown errors and
|
|
79
|
+
* yielded error-result events).
|
|
81
80
|
*/
|
|
82
81
|
export class SpawningRunner {
|
|
83
82
|
currentProcess = null;
|
|
@@ -86,6 +85,8 @@ export class SpawningRunner {
|
|
|
86
85
|
spawnHeartbeat;
|
|
87
86
|
binary;
|
|
88
87
|
stopGraceMs;
|
|
88
|
+
/** Accumulated stderr tail for the current run (filled by spawnChild). */
|
|
89
|
+
spawnStderr = '';
|
|
89
90
|
/**
|
|
90
91
|
* Log prefix used in all operational log lines emitted by this runner
|
|
91
92
|
* (spawn, pid file, non-zero exit, stderr, killOrphan, stop cleanup) and
|
|
@@ -130,72 +131,31 @@ export class SpawningRunner {
|
|
|
130
131
|
// --- Hooks (subclasses override as needed) ---
|
|
131
132
|
/**
|
|
132
133
|
* stdio config for spawn. Default: stdin ignored (claude/kimi/pi pass the
|
|
133
|
-
* prompt via argv).
|
|
134
|
-
* they can write the prompt to stdin via `setupStdin`.
|
|
134
|
+
* prompt via argv).
|
|
135
135
|
*/
|
|
136
136
|
getStdio() {
|
|
137
137
|
return ['ignore', 'pipe', 'pipe'];
|
|
138
138
|
}
|
|
139
|
-
/**
|
|
140
|
-
* Env vars for spawn. Default: process.env. opencode overrides to sync
|
|
141
|
-
* PWD=opts.cwd (opencode's directory detection reads PWD, not cwd).
|
|
142
|
-
*/
|
|
143
|
-
getSpawnEnv(_opts) {
|
|
144
|
-
return process.env;
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* Write the prompt to the child's stdin (called after spawn succeeds).
|
|
148
|
-
* Default: no-op. codex/opencode override to `proc.stdin.end(message)`.
|
|
149
|
-
*/
|
|
150
|
-
setupStdin(_proc, _message) {
|
|
151
|
-
// no-op by default
|
|
152
|
-
}
|
|
153
139
|
/**
|
|
154
140
|
* Create an async iterator over the child's stdout. Default: createJSONLStream
|
|
155
141
|
* (claude/kimi/pi) with P1-4 backpressure enabled (pauseThreshold=100).
|
|
156
|
-
* Subclasses that need to silently skip non-JSON lines should override
|
|
157
|
-
* `handleBadStreamLine` instead of this method.
|
|
158
142
|
*/
|
|
159
143
|
createStreamReader(stdout) {
|
|
160
144
|
return createJSONLStream(stdout, {
|
|
161
|
-
onParseError: (line) =>
|
|
145
|
+
onParseError: (line) => {
|
|
146
|
+
getLogger().warn(`[jsonl-stream] failed to parse JSONL: ${line.slice(0, 100)}`);
|
|
147
|
+
},
|
|
162
148
|
pauseThreshold: 100,
|
|
163
149
|
resumeThreshold: 50,
|
|
164
150
|
});
|
|
165
151
|
}
|
|
166
|
-
/**
|
|
167
|
-
* Hook called when createJSONLStream encounters a line that cannot be parsed
|
|
168
|
-
* as JSON. Default: log a warning via getLogger().warn (same format as
|
|
169
|
-
* jsonl-stream's built-in warning). Subclasses override to change behavior
|
|
170
|
-
* (e.g. codex/opencode silently skip by providing an empty override).
|
|
171
|
-
*/
|
|
172
|
-
handleBadStreamLine(line) {
|
|
173
|
-
getLogger().warn(`[jsonl-stream] failed to parse JSONL: ${line.slice(0, 100)}`);
|
|
174
|
-
}
|
|
175
152
|
/**
|
|
176
153
|
* Create a stateful translator for this run. Default: null (uses the
|
|
177
|
-
* stateless
|
|
178
|
-
* return their translator instance.
|
|
154
|
+
* stateless passthrough). pi overrides to return its translator instance.
|
|
179
155
|
*/
|
|
180
156
|
createTranslator(_opts) {
|
|
181
157
|
return null;
|
|
182
158
|
}
|
|
183
|
-
/**
|
|
184
|
-
* Pre-spawn validation. Return an AgentEvent to yield it and abort run()
|
|
185
|
-
* (e.g. codex's API key check yields an error result and returns).
|
|
186
|
-
* Default: null (no validation).
|
|
187
|
-
*/
|
|
188
|
-
validateBeforeRun(_opts) {
|
|
189
|
-
return null;
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* Await process completion. Default: return the completion promise as-is.
|
|
193
|
-
* kimi overrides to add a 5s timeout race + SIGKILL fallback (kimi
|
|
194
|
-
* sometimes hangs after stdout closes).
|
|
195
|
-
*/
|
|
196
|
-
awaitCompletion(_proc, completion) {
|
|
197
|
-
return completion;
|
|
198
|
-
}
|
|
199
159
|
/**
|
|
200
160
|
* Await the spawn 'error' event when proc.pid === undefined. Races a
|
|
201
161
|
* finite timeout (review P2-11) against the 'error' event: Node guarantees
|
|
@@ -214,60 +174,24 @@ export class SpawningRunner {
|
|
|
214
174
|
]);
|
|
215
175
|
}
|
|
216
176
|
/**
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
* always wins — the agent error is the root cause, signal/code is a
|
|
221
|
-
* consequence. E.g. codex `turn.failed` then non-zero exit.
|
|
222
|
-
* - stream-ended-early symptom (translator.getTerminalError() set by
|
|
223
|
-
* finish('failed')): ONLY surfaces when there is no external cause
|
|
224
|
-
* (signal / non-zero code / user stop). The signal/code is the root
|
|
225
|
-
* cause; the early stream-end is a symptom.
|
|
226
|
-
* - otherwise: undefined (signal/code/stoppedByUser drives the message).
|
|
177
|
+
* Spawn the agent binary with the subclass's argv and run the shared spawn
|
|
178
|
+
* lead-in: pid-undefined check, pid-file write, heartbeat start, and stderr
|
|
179
|
+
* accumulation (tail-capped).
|
|
227
180
|
*
|
|
228
|
-
*
|
|
229
|
-
* the
|
|
230
|
-
|
|
231
|
-
resolveTranslatorError(translator, ctx) {
|
|
232
|
-
if (!translator)
|
|
233
|
-
return undefined;
|
|
234
|
-
const agentError = translator.hasAgentTerminalError();
|
|
235
|
-
const hasExternalCause = this.stoppedByUser || ctx.signal !== null || (ctx.code !== null && ctx.code !== 0);
|
|
236
|
-
return agentError || !hasExternalCause ? translator.getTerminalError() : undefined;
|
|
237
|
-
}
|
|
238
|
-
/**
|
|
239
|
-
* Spawn the agent binary with argv built by buildArgv(opts), iterate its
|
|
240
|
-
* stdout stream, and yield translated events. Always emits a unified
|
|
241
|
-
* result event at the end via `buildResultEvent(...)` — never throws on
|
|
242
|
-
* non-zero exit (the bridge accepts both thrown errors and yielded
|
|
243
|
-
* error-result events, and the latter carries richer diagnostic info).
|
|
181
|
+
* Throws a `SpawnChildError` (carrying the user-facing spawn failure
|
|
182
|
+
* message) when the process fails to spawn (binary missing / pid undefined).
|
|
183
|
+
* On success returns the spawned child plus the accumulated stderr tail.
|
|
244
184
|
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
185
|
+
* Shared by the base `run()` (yields an authErrorEvent on failure) and
|
|
186
|
+
* ClaudeSession's `doStartProcess` (returns the error message), which would
|
|
187
|
+
* otherwise duplicate ~60 lines of spawn lead-in with two copies of the
|
|
188
|
+
* STDERR_TAIL_BYTES cap.
|
|
247
189
|
*/
|
|
248
|
-
async
|
|
249
|
-
|
|
250
|
-
getLogger().warn(`[${this.logTag}] run() called while already running, refusing`);
|
|
251
|
-
throw new Error(`${this.binary} process already running`);
|
|
252
|
-
}
|
|
253
|
-
this.stoppedByUser = false;
|
|
254
|
-
this.currentMessage = message;
|
|
255
|
-
// Pre-spawn validation hook (e.g. codex API key check)
|
|
256
|
-
const validationError = this.validateBeforeRun(opts);
|
|
257
|
-
if (validationError) {
|
|
258
|
-
// §9.22: yield synthetic init so the bridge's pre-init result guard and
|
|
259
|
-
// the run-state reducer's sessionId check don't silently drop the error
|
|
260
|
-
// result. Without init, the card would show a generic "输出流已结束"
|
|
261
|
-
// instead of the specific validation error message.
|
|
262
|
-
yield syntheticInitEvent(opts.sessionId);
|
|
263
|
-
yield validationError;
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
const args = this.buildArgv(opts);
|
|
267
|
-
const proc = spawn(this.binary, args, {
|
|
190
|
+
async spawnChild(opts) {
|
|
191
|
+
const proc = spawn(this.binary, this.buildArgv(opts), {
|
|
268
192
|
cwd: opts.cwd,
|
|
269
193
|
stdio: this.getStdio(),
|
|
270
|
-
env:
|
|
194
|
+
env: process.env,
|
|
271
195
|
// Spawn in a new process group so we can kill the entire group
|
|
272
196
|
// when stopping. This ensures child processes (shell wrappers,
|
|
273
197
|
// sub-processes) are also terminated.
|
|
@@ -292,10 +216,56 @@ export class SpawningRunner {
|
|
|
292
216
|
// so the user is not misdiagnosed into reinstalling the binary.
|
|
293
217
|
const baseMsg = `${this.binary} 命令不可用(未找到或不可执行),请检查是否已安装或在 PATH 中`;
|
|
294
218
|
const msg = spawnErr ? `${baseMsg}:${spawnErr.message}` : baseMsg;
|
|
219
|
+
throw new SpawnChildError(msg);
|
|
220
|
+
}
|
|
221
|
+
const pidDir = path.dirname(this.pidFilePath);
|
|
222
|
+
fs.mkdirSync(pidDir, { recursive: true });
|
|
223
|
+
fs.writeFileSync(this.pidFilePath, String(proc.pid), 'utf-8');
|
|
224
|
+
getLogger().info(`[${this.logTag}] wrote pid file ${this.pidFilePath}=${proc.pid}`);
|
|
225
|
+
this.spawnHeartbeat.start({ pid: proc.pid, binary: this.binary, cwd: opts.cwd });
|
|
226
|
+
proc.stdout?.once('data', () => {
|
|
227
|
+
this.spawnHeartbeat.notifyStdout();
|
|
228
|
+
});
|
|
229
|
+
this.spawnStderr = '';
|
|
230
|
+
proc.stderr?.on('data', (chunk) => {
|
|
231
|
+
const text = chunk.toString('utf-8').trim();
|
|
232
|
+
if (text) {
|
|
233
|
+
this.spawnStderr = (this.spawnStderr + '\n' + text).trim().slice(-STDERR_TAIL_BYTES);
|
|
234
|
+
// P2-16: most agent CLIs emit progress/warnings/deprecation notices on
|
|
235
|
+
// stderr, not real errors. Logging each chunk at error level drowns out
|
|
236
|
+
// genuine errors. Downgrade to warn — the accumulated stderr is already
|
|
237
|
+
// surfaced at error level in the non-zero-exit result path.
|
|
238
|
+
getLogger().warn(`[${this.logTag} stderr] ${text}`);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
return proc;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Spawn the agent binary with argv built by buildArgv(opts), iterate its
|
|
245
|
+
* stdout stream, and yield translated events. Always emits a unified
|
|
246
|
+
* result event at the end via `buildResultEvent(...)` — never throws on
|
|
247
|
+
* non-zero exit (the bridge accepts both thrown errors and yielded
|
|
248
|
+
* error-result events, and the latter carries richer diagnostic info).
|
|
249
|
+
*
|
|
250
|
+
* Spawn failures (binary missing, pid undefined) yield an `authErrorEvent`
|
|
251
|
+
* instead of throwing, so the user sees a friendly error card.
|
|
252
|
+
*/
|
|
253
|
+
async *run(message, opts) {
|
|
254
|
+
if (this.isRunning) {
|
|
255
|
+
getLogger().warn(`[${this.logTag}] run() called while already running, refusing`);
|
|
256
|
+
throw new Error(`${this.binary} process already running`);
|
|
257
|
+
}
|
|
258
|
+
this.stoppedByUser = false;
|
|
259
|
+
this.currentMessage = message;
|
|
260
|
+
let proc;
|
|
261
|
+
try {
|
|
262
|
+
proc = await this.spawnChild(opts);
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
295
265
|
// §9.22: yield synthetic init before the error result so bridge/run-state
|
|
296
|
-
// guards don't silently drop it
|
|
266
|
+
// guards don't silently drop it.
|
|
297
267
|
yield syntheticInitEvent(opts.sessionId);
|
|
298
|
-
yield authErrorEvent(
|
|
268
|
+
yield authErrorEvent(err.message);
|
|
299
269
|
return;
|
|
300
270
|
}
|
|
301
271
|
const completion = new Promise((resolve) => {
|
|
@@ -317,29 +287,7 @@ export class SpawningRunner {
|
|
|
317
287
|
resolve({ code, signal });
|
|
318
288
|
});
|
|
319
289
|
});
|
|
320
|
-
|
|
321
|
-
fs.mkdirSync(pidDir, { recursive: true });
|
|
322
|
-
fs.writeFileSync(this.pidFilePath, String(proc.pid), 'utf-8');
|
|
323
|
-
getLogger().info(`[${this.logTag}] wrote pid file ${this.pidFilePath}=${proc.pid}`);
|
|
324
|
-
this.spawnHeartbeat.start({ pid: proc.pid, binary: this.binary, cwd: opts.cwd });
|
|
325
|
-
proc.stdout?.once('data', () => {
|
|
326
|
-
this.spawnHeartbeat.notifyStdout();
|
|
327
|
-
});
|
|
328
|
-
let stderr = '';
|
|
329
|
-
proc.stderr?.on('data', (chunk) => {
|
|
330
|
-
const text = chunk.toString('utf-8').trim();
|
|
331
|
-
if (text) {
|
|
332
|
-
stderr = (stderr + '\n' + text).trim().slice(-STDERR_TAIL_BYTES);
|
|
333
|
-
// P2-16: most agent CLIs emit progress/warnings/deprecation notices on
|
|
334
|
-
// stderr, not real errors. Logging each chunk at error level drowns out
|
|
335
|
-
// genuine errors. Downgrade to warn — the accumulated stderr is already
|
|
336
|
-
// surfaced at error level in the non-zero-exit result path.
|
|
337
|
-
getLogger().warn(`[${this.logTag} stderr] ${text}`);
|
|
338
|
-
}
|
|
339
|
-
});
|
|
340
|
-
// Hook: write prompt to stdin (codex/opencode)
|
|
341
|
-
this.setupStdin(proc, message);
|
|
342
|
-
// Create translator (per-run state) — null for stateless passthrough (claude)
|
|
290
|
+
// Create translator (per-run state) — null for stateless passthrough
|
|
343
291
|
const translator = this.createTranslator(opts);
|
|
344
292
|
// Create stream reader (jsonl or readline)
|
|
345
293
|
const stream = this.createStreamReader(proc.stdout);
|
|
@@ -380,7 +328,7 @@ export class SpawningRunner {
|
|
|
380
328
|
catch (error) {
|
|
381
329
|
getLogger().error(`[${this.logTag}] stream error: ${error}`);
|
|
382
330
|
}
|
|
383
|
-
const { code, signal } = await
|
|
331
|
+
const { code, signal } = await completion;
|
|
384
332
|
// If the stream ended before a terminal event, let the translator
|
|
385
333
|
// record its terminal state (e.g. codex `finish('failed')` stashes
|
|
386
334
|
// "stream ended before a terminal event"). The runner folds this
|
|
@@ -389,18 +337,27 @@ export class SpawningRunner {
|
|
|
389
337
|
const reason = this.stoppedByUser ? 'interrupted' : 'failed';
|
|
390
338
|
translator.finish(reason);
|
|
391
339
|
}
|
|
392
|
-
|
|
340
|
+
// Resolve translatorError precedence (most-specific first):
|
|
341
|
+
// - agent-reported terminal error always wins
|
|
342
|
+
// - stream-ended-early symptom only surfaces with no external cause
|
|
343
|
+
let translatorError;
|
|
344
|
+
if (translator) {
|
|
345
|
+
const agentError = translator.hasAgentTerminalError();
|
|
346
|
+
const hasExternalCause = this.stoppedByUser || signal !== null || (code !== null && code !== 0);
|
|
347
|
+
translatorError =
|
|
348
|
+
agentError || !hasExternalCause ? translator.getTerminalError() : undefined;
|
|
349
|
+
}
|
|
393
350
|
const nonUserError = !this.stoppedByUser && ((code !== null && code !== 0) || signal !== null);
|
|
394
351
|
yield this.buildResultEvent({
|
|
395
352
|
code,
|
|
396
353
|
signal,
|
|
397
|
-
stderr,
|
|
354
|
+
stderr: this.spawnStderr,
|
|
398
355
|
sessionId: translator?.getSessionId?.() ?? '',
|
|
399
356
|
usage: translator?.getLastUsage?.(),
|
|
400
357
|
translatorError,
|
|
401
358
|
});
|
|
402
359
|
if (nonUserError) {
|
|
403
|
-
getLogger().error(`[${this.logTag}] non-zero exit code=${code} signal=${signal} stderr=${
|
|
360
|
+
getLogger().error(`[${this.logTag}] non-zero exit code=${code} signal=${signal} stderr=${this.spawnStderr.slice(-STDERR_LOG_TAIL)}`);
|
|
404
361
|
}
|
|
405
362
|
}
|
|
406
363
|
finally {
|
|
@@ -420,12 +377,7 @@ export class SpawningRunner {
|
|
|
420
377
|
}
|
|
421
378
|
}
|
|
422
379
|
this.currentProcess = null;
|
|
423
|
-
|
|
424
|
-
fs.unlinkSync(this.pidFilePath);
|
|
425
|
-
}
|
|
426
|
-
catch {
|
|
427
|
-
/* ignore — file may not exist if spawn failed before write */
|
|
428
|
-
}
|
|
380
|
+
silentlyUnlink(this.pidFilePath);
|
|
429
381
|
}
|
|
430
382
|
}
|
|
431
383
|
/**
|
|
@@ -444,10 +396,7 @@ export class SpawningRunner {
|
|
|
444
396
|
this.stoppedByUser = true;
|
|
445
397
|
await this.stopper.stop(proc, { immediate: opts?.immediate });
|
|
446
398
|
this.currentProcess = null;
|
|
447
|
-
|
|
448
|
-
fs.unlinkSync(this.pidFilePath);
|
|
449
|
-
}
|
|
450
|
-
catch { }
|
|
399
|
+
silentlyUnlink(this.pidFilePath);
|
|
451
400
|
getLogger().debug(`[${this.logTag}] cleaned pid file ${this.pidFilePath}`);
|
|
452
401
|
}
|
|
453
402
|
killOrphan() {
|
|
@@ -457,7 +406,7 @@ export class SpawningRunner {
|
|
|
457
406
|
const pidStr = fs.readFileSync(this.pidFilePath, 'utf-8').trim();
|
|
458
407
|
const pid = Number(pidStr);
|
|
459
408
|
if (isNaN(pid) || pid <= 0) {
|
|
460
|
-
|
|
409
|
+
silentlyUnlink(this.pidFilePath);
|
|
461
410
|
return;
|
|
462
411
|
}
|
|
463
412
|
// P1-10: pid 复用防护。pid 文件里的 pid 可能是 bridge 崩溃后系统回收复用
|
|
@@ -470,7 +419,7 @@ export class SpawningRunner {
|
|
|
470
419
|
// 统一原则:只有身份匹配才杀;验证失败一律不杀。
|
|
471
420
|
getLogger().warn(`[${this.logTag}] killOrphan: pid ${pid} not confirmed as ${this.binary} ` +
|
|
472
421
|
`(${match}), skipping kill`);
|
|
473
|
-
|
|
422
|
+
silentlyUnlink(this.pidFilePath);
|
|
474
423
|
return;
|
|
475
424
|
}
|
|
476
425
|
// 身份匹配:杀整个进程组(detached:true 下 agent 是组长),与
|
|
@@ -482,7 +431,7 @@ export class SpawningRunner {
|
|
|
482
431
|
catch {
|
|
483
432
|
// process may have exited
|
|
484
433
|
}
|
|
485
|
-
|
|
434
|
+
silentlyUnlink(this.pidFilePath);
|
|
486
435
|
}
|
|
487
436
|
catch {
|
|
488
437
|
// ignore
|
|
@@ -561,10 +510,7 @@ export class SpawningRunner {
|
|
|
561
510
|
}
|
|
562
511
|
catch { }
|
|
563
512
|
}
|
|
564
|
-
|
|
565
|
-
fs.unlinkSync(this.pidFilePath);
|
|
566
|
-
}
|
|
567
|
-
catch { }
|
|
513
|
+
silentlyUnlink(this.pidFilePath);
|
|
568
514
|
}
|
|
569
515
|
/**
|
|
570
516
|
* Build a unified result event from process exit state. Centralizes the
|