lark-remote 0.1.0
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 +52 -0
- package/LICENSE +21 -0
- package/README.en.md +162 -0
- package/README.md +161 -0
- package/dist/binder.d.ts +57 -0
- package/dist/binder.js +105 -0
- package/dist/bridge/index.d.ts +338 -0
- package/dist/bridge/index.js +1281 -0
- package/dist/bridge/queue-manager.d.ts +277 -0
- package/dist/bridge/queue-manager.js +729 -0
- package/dist/card/bash-card-session.d.ts +78 -0
- package/dist/card/bash-card-session.js +174 -0
- package/dist/card/bash-renderer.d.ts +24 -0
- package/dist/card/bash-renderer.js +187 -0
- package/dist/card/card-budget.d.ts +31 -0
- package/dist/card/card-budget.js +280 -0
- package/dist/card/card-session.d.ts +83 -0
- package/dist/card/card-session.js +185 -0
- package/dist/card/card-shared.d.ts +28 -0
- package/dist/card/card-shared.js +99 -0
- package/dist/card/collapsible.d.ts +26 -0
- package/dist/card/collapsible.js +87 -0
- package/dist/card/run-card-session.d.ts +78 -0
- package/dist/card/run-card-session.js +165 -0
- package/dist/card/run-renderer.d.ts +66 -0
- package/dist/card/run-renderer.js +841 -0
- package/dist/card/run-state.d.ts +103 -0
- package/dist/card/run-state.js +313 -0
- package/dist/card/text-truncate.d.ts +56 -0
- package/dist/card/text-truncate.js +184 -0
- package/dist/card/time.d.ts +1 -0
- package/dist/card/time.js +14 -0
- package/dist/card/tool-render.d.ts +25 -0
- package/dist/card/tool-render.js +176 -0
- package/dist/cli.d.ts +28 -0
- package/dist/cli.js +89 -0
- package/dist/common/guards.d.ts +37 -0
- package/dist/common/guards.js +65 -0
- package/dist/config/codex-config.d.ts +113 -0
- package/dist/config/codex-config.js +428 -0
- package/dist/config/dir.d.ts +17 -0
- package/dist/config/dir.js +81 -0
- package/dist/config/index.d.ts +219 -0
- package/dist/config/index.js +388 -0
- package/dist/config/kimi-config.d.ts +33 -0
- package/dist/config/kimi-config.js +111 -0
- package/dist/config/opencode-config.d.ts +20 -0
- package/dist/config/opencode-config.js +124 -0
- package/dist/config/pi-config.d.ts +49 -0
- package/dist/config/pi-config.js +151 -0
- package/dist/config/wizard.d.ts +12 -0
- package/dist/config/wizard.js +84 -0
- package/dist/connector/file-limits.d.ts +7 -0
- package/dist/connector/file-limits.js +8 -0
- package/dist/connector/index.d.ts +86 -0
- package/dist/connector/index.js +340 -0
- package/dist/error-classification.d.ts +15 -0
- package/dist/error-classification.js +24 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +580 -0
- package/dist/instance-lock.d.ts +30 -0
- package/dist/instance-lock.js +184 -0
- package/dist/logger/index.d.ts +57 -0
- package/dist/logger/index.js +178 -0
- package/dist/order/index.d.ts +27 -0
- package/dist/order/index.js +90 -0
- package/dist/persistence/atomic-write.d.ts +10 -0
- package/dist/persistence/atomic-write.js +65 -0
- package/dist/persistence/load-json-file.d.ts +9 -0
- package/dist/persistence/load-json-file.js +23 -0
- package/dist/restart.d.ts +24 -0
- package/dist/restart.js +73 -0
- package/dist/router/card-helpers.d.ts +28 -0
- package/dist/router/card-helpers.js +65 -0
- package/dist/router/config/claude.d.ts +14 -0
- package/dist/router/config/claude.js +72 -0
- package/dist/router/config/codex.d.ts +22 -0
- package/dist/router/config/codex.js +100 -0
- package/dist/router/config/common/render.d.ts +16 -0
- package/dist/router/config/common/render.js +158 -0
- package/dist/router/config/index.d.ts +18 -0
- package/dist/router/config/index.js +38 -0
- package/dist/router/config/kimi.d.ts +21 -0
- package/dist/router/config/kimi.js +63 -0
- package/dist/router/config/opencode.d.ts +12 -0
- package/dist/router/config/opencode.js +44 -0
- package/dist/router/config/pi.d.ts +12 -0
- package/dist/router/config/pi.js +50 -0
- package/dist/router/config/types.d.ts +43 -0
- package/dist/router/config/types.js +8 -0
- package/dist/router/index.d.ts +318 -0
- package/dist/router/index.js +2715 -0
- package/dist/router/order-exec-dispatch.d.ts +30 -0
- package/dist/router/order-exec-dispatch.js +48 -0
- package/dist/router/utils.d.ts +44 -0
- package/dist/router/utils.js +116 -0
- package/dist/runner/bash/index.d.ts +4 -0
- package/dist/runner/bash/index.js +5 -0
- package/dist/runner/bash/runner.d.ts +55 -0
- package/dist/runner/bash/runner.js +213 -0
- package/dist/runner/claude/index.d.ts +4 -0
- package/dist/runner/claude/index.js +5 -0
- package/dist/runner/claude/runner.d.ts +29 -0
- package/dist/runner/claude/runner.js +83 -0
- package/dist/runner/codex/argv.d.ts +33 -0
- package/dist/runner/codex/argv.js +40 -0
- package/dist/runner/codex/index.d.ts +1 -0
- package/dist/runner/codex/index.js +2 -0
- package/dist/runner/codex/jsonl.d.ts +37 -0
- package/dist/runner/codex/jsonl.js +221 -0
- package/dist/runner/codex/runner.d.ts +65 -0
- package/dist/runner/codex/runner.js +124 -0
- package/dist/runner/common/exec-translator.d.ts +73 -0
- package/dist/runner/common/exec-translator.js +105 -0
- package/dist/runner/common/jsonl-stream.d.ts +39 -0
- package/dist/runner/common/jsonl-stream.js +203 -0
- package/dist/runner/common/process-stopper.d.ts +28 -0
- package/dist/runner/common/process-stopper.js +79 -0
- package/dist/runner/common/runner-utils.d.ts +21 -0
- package/dist/runner/common/runner-utils.js +65 -0
- package/dist/runner/common/spawn-heartbeat.d.ts +33 -0
- package/dist/runner/common/spawn-heartbeat.js +51 -0
- package/dist/runner/common/spawning-runner.d.ts +321 -0
- package/dist/runner/common/spawning-runner.js +634 -0
- package/dist/runner/common/usage.d.ts +31 -0
- package/dist/runner/common/usage.js +29 -0
- package/dist/runner/index.d.ts +19 -0
- package/dist/runner/index.js +24 -0
- package/dist/runner/kimi/index.d.ts +1 -0
- package/dist/runner/kimi/index.js +2 -0
- package/dist/runner/kimi/jsonl.d.ts +28 -0
- package/dist/runner/kimi/jsonl.js +143 -0
- package/dist/runner/kimi/runner.d.ts +50 -0
- package/dist/runner/kimi/runner.js +105 -0
- package/dist/runner/opencode/argv.d.ts +32 -0
- package/dist/runner/opencode/argv.js +35 -0
- package/dist/runner/opencode/index.d.ts +1 -0
- package/dist/runner/opencode/index.js +2 -0
- package/dist/runner/opencode/jsonl.d.ts +68 -0
- package/dist/runner/opencode/jsonl.js +295 -0
- package/dist/runner/opencode/runner.d.ts +63 -0
- package/dist/runner/opencode/runner.js +93 -0
- package/dist/runner/pi/index.d.ts +1 -0
- package/dist/runner/pi/index.js +2 -0
- package/dist/runner/pi/jsonl.d.ts +128 -0
- package/dist/runner/pi/jsonl.js +249 -0
- package/dist/runner/pi/runner.d.ts +37 -0
- package/dist/runner/pi/runner.js +76 -0
- package/dist/runner/registry.d.ts +54 -0
- package/dist/runner/registry.js +75 -0
- package/dist/runner/resolve-agent-choices.d.ts +7 -0
- package/dist/runner/resolve-agent-choices.js +76 -0
- package/dist/runner/sync-agent-choices.d.ts +6 -0
- package/dist/runner/sync-agent-choices.js +73 -0
- package/dist/runner/types.d.ts +250 -0
- package/dist/runner/types.js +8 -0
- package/dist/session/claude/index.d.ts +1 -0
- package/dist/session/claude/index.js +2 -0
- package/dist/session/claude/session-reader.d.ts +34 -0
- package/dist/session/claude/session-reader.js +46 -0
- package/dist/session/claude/sessions.d.ts +53 -0
- package/dist/session/claude/sessions.js +528 -0
- package/dist/session/codex/index.d.ts +1 -0
- package/dist/session/codex/index.js +2 -0
- package/dist/session/codex/rollout-reader.d.ts +88 -0
- package/dist/session/codex/rollout-reader.js +473 -0
- package/dist/session/codex/sessions.d.ts +21 -0
- package/dist/session/codex/sessions.js +40 -0
- package/dist/session/common/catch-up.d.ts +12 -0
- package/dist/session/common/catch-up.js +31 -0
- package/dist/session/common/constants.d.ts +11 -0
- package/dist/session/common/constants.js +12 -0
- package/dist/session/common/content-blocks.d.ts +37 -0
- package/dist/session/common/content-blocks.js +68 -0
- package/dist/session/common/jsonl.d.ts +77 -0
- package/dist/session/common/jsonl.js +351 -0
- package/dist/session/common/usage-accumulator.d.ts +58 -0
- package/dist/session/common/usage-accumulator.js +72 -0
- package/dist/session/index.d.ts +7 -0
- package/dist/session/index.js +11 -0
- package/dist/session/kimi/index.d.ts +1 -0
- package/dist/session/kimi/index.js +2 -0
- package/dist/session/kimi/sessions.d.ts +81 -0
- package/dist/session/kimi/sessions.js +527 -0
- package/dist/session/opencode/index.d.ts +1 -0
- package/dist/session/opencode/index.js +2 -0
- package/dist/session/opencode/sessions.d.ts +81 -0
- package/dist/session/opencode/sessions.js +388 -0
- package/dist/session/pi/index.d.ts +1 -0
- package/dist/session/pi/index.js +2 -0
- package/dist/session/pi/sessions.d.ts +27 -0
- package/dist/session/pi/sessions.js +399 -0
- package/dist/session/registry.d.ts +12 -0
- package/dist/session/registry.js +20 -0
- package/dist/session/session-store.d.ts +86 -0
- package/dist/session/session-store.js +450 -0
- package/dist/startup-contact.d.ts +18 -0
- package/dist/startup-contact.js +51 -0
- package/dist/workspace/index.d.ts +19 -0
- package/dist/workspace/index.js +53 -0
- package/package.json +93 -0
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
import { spawn, execFileSync } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import { getLogger } from '../../logger/index.js';
|
|
6
|
+
import { ProcessStopper } from './process-stopper.js';
|
|
7
|
+
import { SpawnHeartbeat } from './spawn-heartbeat.js';
|
|
8
|
+
import { createJSONLStream } from './jsonl-stream.js';
|
|
9
|
+
import { authErrorEvent } from './runner-utils.js';
|
|
10
|
+
import { DEFAULT_STOP_GRACE_MS } from '../../config/index.js';
|
|
11
|
+
/**
|
|
12
|
+
* Magic-number constants (Clean Code P3-1, G25 Replace Magic Numbers with
|
|
13
|
+
* Named Constants). Centralized here so the truncation budgets are visible
|
|
14
|
+
* and adjustable in one place rather than scattered as raw literals.
|
|
15
|
+
*/
|
|
16
|
+
/** Max bytes of stderr retained for the result-event error message. */
|
|
17
|
+
const STDERR_TAIL_BYTES = 4000;
|
|
18
|
+
/** Max bytes of stderr surfaced in the non-zero-exit log line. */
|
|
19
|
+
const STDERR_LOG_TAIL = 500;
|
|
20
|
+
/**
|
|
21
|
+
* Timeout (ms) for awaiting the spawn 'error' event when proc.pid === undefined
|
|
22
|
+
* (review P2-11). Node guarantees 'error' for ENOENT/EACCES, but some binaries
|
|
23
|
+
* fail silently; the race keeps a silent failure from hanging run() forever.
|
|
24
|
+
* Matches the 5s race kimi's override used before it was hoisted to the base.
|
|
25
|
+
*/
|
|
26
|
+
const SPAWN_ERROR_TIMEOUT_MS = 5000;
|
|
27
|
+
const registeredRunners = new Set();
|
|
28
|
+
let exitListenersInstalled = false;
|
|
29
|
+
function cleanupRegisteredRunners() {
|
|
30
|
+
for (const runner of registeredRunners) {
|
|
31
|
+
runner.cleanupOnExit();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function installExitListenersOnce() {
|
|
35
|
+
if (exitListenersInstalled)
|
|
36
|
+
return;
|
|
37
|
+
exitListenersInstalled = true;
|
|
38
|
+
process.on('exit', cleanupRegisteredRunners);
|
|
39
|
+
process.on('SIGINT', () => {
|
|
40
|
+
cleanupRegisteredRunners();
|
|
41
|
+
process.exit(130);
|
|
42
|
+
});
|
|
43
|
+
process.on('SIGTERM', () => {
|
|
44
|
+
cleanupRegisteredRunners();
|
|
45
|
+
process.exit(143);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/** Register a process-exit cleanup handler (installs listeners at most once). */
|
|
49
|
+
export function registerExitCleanup(handler) {
|
|
50
|
+
installExitListenersOnce();
|
|
51
|
+
registeredRunners.add(handler);
|
|
52
|
+
}
|
|
53
|
+
/** Remove a handler so it can be GC'd (agent slots / finished bash runs). */
|
|
54
|
+
export function unregisterExitCleanup(handler) {
|
|
55
|
+
registeredRunners.delete(handler);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Abstract base class for runners that spawn a child process per turn.
|
|
59
|
+
*
|
|
60
|
+
* Encapsulates the spawn + completion + cleanup orchestration shared by
|
|
61
|
+
* Claude/Codex/OpenCode/Pi/Kimi runners. Subclasses override:
|
|
62
|
+
* - buildArgv(opts) → agent-specific CLI flags (required)
|
|
63
|
+
* - createTranslator(opts) → stateful translator (codex/opencode/pi/kimi)
|
|
64
|
+
* OR translate(raw, ctx) → stateless passthrough (claude)
|
|
65
|
+
*
|
|
66
|
+
* Optional hooks (with sensible defaults):
|
|
67
|
+
* - 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
|
+
* - createStreamReader(stdout) → JSONL or readline parser (default createJSONLStream)
|
|
71
|
+
* - validateBeforeRun(opts) → pre-spawn validation (default no-op)
|
|
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)
|
|
75
|
+
*
|
|
76
|
+
* The base class always emits a unified result event at the end of `run()`
|
|
77
|
+
* via `buildResultEvent(...)` — never throws on non-zero exit. This matches
|
|
78
|
+
* the contract pinned by codex/opencode/pi/kimi subclass anchors and the
|
|
79
|
+
* bridge's `runAgentStreamToEnd` error handling (which accepts both thrown
|
|
80
|
+
* errors and yielded error-result events).
|
|
81
|
+
*/
|
|
82
|
+
export class SpawningRunner {
|
|
83
|
+
currentProcess = null;
|
|
84
|
+
pidFilePath;
|
|
85
|
+
stopper;
|
|
86
|
+
spawnHeartbeat;
|
|
87
|
+
binary;
|
|
88
|
+
stopGraceMs;
|
|
89
|
+
/**
|
|
90
|
+
* Log prefix used in all operational log lines emitted by this runner
|
|
91
|
+
* (spawn, pid file, non-zero exit, stderr, killOrphan, stop cleanup) and
|
|
92
|
+
* as the SpawnHeartbeat label. Subclasses pass their own tag (e.g.
|
|
93
|
+
* 'claude-runner') so operators can grep agent-specific logs; the default
|
|
94
|
+
* 'spawning-runner' keeps the base-class anchor tests' neutral identity.
|
|
95
|
+
*/
|
|
96
|
+
logTag;
|
|
97
|
+
/**
|
|
98
|
+
* The `message` argument from the most recent `run(message, opts)` call.
|
|
99
|
+
* Set in `run()` before `buildArgv(opts)` is invoked so subclasses can
|
|
100
|
+
* embed the message in their agent-specific argv (e.g. claude's `-p
|
|
101
|
+
* <message>`). This avoids changing the `buildArgv(opts)` signature,
|
|
102
|
+
* which would break existing anchor subclasses.
|
|
103
|
+
*/
|
|
104
|
+
currentMessage = '';
|
|
105
|
+
/**
|
|
106
|
+
* Whether the current run was interrupted by user-initiated stop().
|
|
107
|
+
* Set to true by stop() when a running process is being terminated;
|
|
108
|
+
* reset to false at the top of run(). `resolveTranslatorError()` and
|
|
109
|
+
* `buildResultEvent()` read this to decide result event subtype
|
|
110
|
+
* (error vs success) and the error message precedence.
|
|
111
|
+
*/
|
|
112
|
+
stoppedByUser = false;
|
|
113
|
+
constructor(opts) {
|
|
114
|
+
// Neutral default — empty string fails loudly at spawn time if a caller
|
|
115
|
+
// forgets to configure binary. 'claude' would be wrong (biases the base
|
|
116
|
+
// class toward one agent). Subclasses pass their own binary.
|
|
117
|
+
this.binary = opts.binary ?? '';
|
|
118
|
+
this.stopGraceMs = opts.stopGraceMs ?? DEFAULT_STOP_GRACE_MS;
|
|
119
|
+
this.logTag = opts.logTag ?? 'spawning-runner';
|
|
120
|
+
const pidDir = opts.pidDir ?? path.join(os.homedir(), '.lark-remote');
|
|
121
|
+
const workspaceSuffix = `-${opts.workspace.replace(/[^a-zA-Z0-9]/g, '_')}`;
|
|
122
|
+
const pidFilePrefix = opts.pidFilePrefix ?? 'spawning';
|
|
123
|
+
this.pidFilePath = path.join(pidDir, `${pidFilePrefix}${workspaceSuffix}.pid`);
|
|
124
|
+
this.stopper = new ProcessStopper({ graceMs: this.stopGraceMs });
|
|
125
|
+
this.spawnHeartbeat = new SpawnHeartbeat(opts.spawnHeartbeatMs ?? 30_000, this.logTag);
|
|
126
|
+
}
|
|
127
|
+
get isRunning() {
|
|
128
|
+
return (this.currentProcess !== null &&
|
|
129
|
+
this.currentProcess.exitCode === null &&
|
|
130
|
+
this.currentProcess.signalCode === null);
|
|
131
|
+
}
|
|
132
|
+
// --- Hooks (subclasses override as needed) ---
|
|
133
|
+
/**
|
|
134
|
+
* stdio config for spawn. Default: stdin ignored (claude/kimi/pi pass the
|
|
135
|
+
* prompt via argv). codex/opencode override to ['pipe','pipe','pipe'] so
|
|
136
|
+
* they can write the prompt to stdin via `setupStdin`.
|
|
137
|
+
*/
|
|
138
|
+
getStdio() {
|
|
139
|
+
return ['ignore', 'pipe', 'pipe'];
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Env vars for spawn. Default: process.env. opencode overrides to sync
|
|
143
|
+
* PWD=opts.cwd (opencode's directory detection reads PWD, not cwd).
|
|
144
|
+
*/
|
|
145
|
+
getSpawnEnv(_opts) {
|
|
146
|
+
return process.env;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Write the prompt to the child's stdin (called after spawn succeeds).
|
|
150
|
+
* Default: no-op. codex/opencode override to `proc.stdin.end(message)`.
|
|
151
|
+
*/
|
|
152
|
+
setupStdin(_proc, _message) {
|
|
153
|
+
// no-op by default
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Create an async iterator over the child's stdout. Default: createJSONLStream
|
|
157
|
+
* (claude/kimi/pi) with P1-4 backpressure enabled (pauseThreshold=100).
|
|
158
|
+
* Subclasses that need to silently skip non-JSON lines should override
|
|
159
|
+
* `handleBadStreamLine` instead of this method.
|
|
160
|
+
*/
|
|
161
|
+
createStreamReader(stdout) {
|
|
162
|
+
return createJSONLStream(stdout, {
|
|
163
|
+
onParseError: (line) => this.handleBadStreamLine(line),
|
|
164
|
+
pauseThreshold: 100,
|
|
165
|
+
resumeThreshold: 50,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Hook called when createJSONLStream encounters a line that cannot be parsed
|
|
170
|
+
* as JSON. Default: log a warning via getLogger().warn (same format as
|
|
171
|
+
* jsonl-stream's built-in warning). Subclasses override to change behavior
|
|
172
|
+
* (e.g. codex/opencode silently skip by providing an empty override).
|
|
173
|
+
*/
|
|
174
|
+
handleBadStreamLine(line) {
|
|
175
|
+
getLogger().warn(`[jsonl-stream] failed to parse JSONL: ${line.slice(0, 100)}`);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Create a stateful translator for this run. Default: null (uses the
|
|
179
|
+
* stateless `translate(raw, ctx)` hook). codex/opencode/pi/kimi override to
|
|
180
|
+
* return their translator instance.
|
|
181
|
+
*/
|
|
182
|
+
createTranslator(_opts) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Pre-spawn validation. Return an AgentEvent to yield it and abort run()
|
|
187
|
+
* (e.g. codex's API key check yields an error result and returns).
|
|
188
|
+
* Default: null (no validation).
|
|
189
|
+
*/
|
|
190
|
+
validateBeforeRun(_opts) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Await process completion. Default: return the completion promise as-is.
|
|
195
|
+
* kimi overrides to add a 5s timeout race + SIGKILL fallback (kimi
|
|
196
|
+
* sometimes hangs after stdout closes).
|
|
197
|
+
*/
|
|
198
|
+
awaitCompletion(_proc, completion) {
|
|
199
|
+
return completion;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Await the spawn 'error' event when proc.pid === undefined. Races a
|
|
203
|
+
* finite timeout (review P2-11) against the 'error' event: Node guarantees
|
|
204
|
+
* 'error' for ENOENT/EACCES, but some binaries fail silently without ever
|
|
205
|
+
* emitting it (kimi's CLI is documented to do so). Without the race, run()
|
|
206
|
+
* hangs forever → the workspace serial queue never settles → permanent
|
|
207
|
+
* deadlock, and /stop can't recover (ProcessStopper returns early when
|
|
208
|
+
* pid === undefined). The timeout keeps the deadlock bounded.
|
|
209
|
+
*/
|
|
210
|
+
awaitSpawnError(proc) {
|
|
211
|
+
return Promise.race([
|
|
212
|
+
new Promise((resolve) => {
|
|
213
|
+
proc.once('error', (err) => resolve(err));
|
|
214
|
+
}),
|
|
215
|
+
new Promise((resolve) => setTimeout(() => resolve(undefined), SPAWN_ERROR_TIMEOUT_MS)),
|
|
216
|
+
]);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Resolve the translatorError to pass to buildResultEvent. Default
|
|
220
|
+
* precedence (most-specific first):
|
|
221
|
+
* - agent-reported terminal error (translator.hasAgentTerminalError()):
|
|
222
|
+
* always wins — the agent error is the root cause, signal/code is a
|
|
223
|
+
* consequence. E.g. codex `turn.failed` then non-zero exit.
|
|
224
|
+
* - stream-ended-early symptom (translator.getTerminalError() set by
|
|
225
|
+
* finish('failed')): ONLY surfaces when there is no external cause
|
|
226
|
+
* (signal / non-zero code / user stop). The signal/code is the root
|
|
227
|
+
* cause; the early stream-end is a symptom.
|
|
228
|
+
* - otherwise: undefined (signal/code/stoppedByUser drives the message).
|
|
229
|
+
*
|
|
230
|
+
* Subclasses can override for custom precedence but the default matches
|
|
231
|
+
* the contract pinned by codex/opencode subclass anchors.
|
|
232
|
+
*/
|
|
233
|
+
resolveTranslatorError(translator, ctx) {
|
|
234
|
+
if (!translator)
|
|
235
|
+
return undefined;
|
|
236
|
+
const agentError = translator.hasAgentTerminalError();
|
|
237
|
+
const hasExternalCause = this.stoppedByUser || ctx.signal !== null || (ctx.code !== null && ctx.code !== 0);
|
|
238
|
+
return agentError || !hasExternalCause ? translator.getTerminalError() : undefined;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Spawn the agent binary with argv built by buildArgv(opts), iterate its
|
|
242
|
+
* stdout stream, and yield translated events. Always emits a unified
|
|
243
|
+
* result event at the end via `buildResultEvent(...)` — never throws on
|
|
244
|
+
* non-zero exit (the bridge accepts both thrown errors and yielded
|
|
245
|
+
* error-result events, and the latter carries richer diagnostic info).
|
|
246
|
+
*
|
|
247
|
+
* Spawn failures (binary missing, pid undefined) yield an `authErrorEvent`
|
|
248
|
+
* instead of throwing, so the user sees a friendly error card.
|
|
249
|
+
*/
|
|
250
|
+
async *run(message, opts) {
|
|
251
|
+
if (this.isRunning) {
|
|
252
|
+
getLogger().warn(`[${this.logTag}] run() called while already running, refusing`);
|
|
253
|
+
throw new Error(`${this.binary} process already running`);
|
|
254
|
+
}
|
|
255
|
+
this.stoppedByUser = false;
|
|
256
|
+
this.currentMessage = message;
|
|
257
|
+
// Pre-spawn validation hook (e.g. codex API key check)
|
|
258
|
+
const validationError = this.validateBeforeRun(opts);
|
|
259
|
+
if (validationError) {
|
|
260
|
+
yield validationError;
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const args = this.buildArgv(opts);
|
|
264
|
+
const proc = spawn(this.binary, args, {
|
|
265
|
+
cwd: opts.cwd,
|
|
266
|
+
stdio: this.getStdio(),
|
|
267
|
+
env: this.getSpawnEnv(opts),
|
|
268
|
+
// Spawn in a new process group so we can kill the entire group
|
|
269
|
+
// when stopping. This ensures child processes (shell wrappers,
|
|
270
|
+
// sub-processes) are also terminated.
|
|
271
|
+
detached: true,
|
|
272
|
+
});
|
|
273
|
+
this.currentProcess = proc;
|
|
274
|
+
getLogger().info(`[${this.logTag}] spawn pid=${proc.pid ?? '(none)'} binary=${this.binary} ` +
|
|
275
|
+
`cwd=${opts.cwd} sessionId=${opts.sessionId ?? '(none)'}`);
|
|
276
|
+
if (proc.pid === undefined) {
|
|
277
|
+
const spawnErr = await this.awaitSpawnError(proc);
|
|
278
|
+
if (spawnErr) {
|
|
279
|
+
getLogger().error(`[${this.logTag}] spawn failed: ${spawnErr.message} binary=${this.binary} cwd=${opts.cwd}`);
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
getLogger().error(`[${this.logTag}] spawn failed: timeout waiting for error event`);
|
|
283
|
+
}
|
|
284
|
+
this.currentProcess = null;
|
|
285
|
+
this.spawnHeartbeat.clear();
|
|
286
|
+
// P2-13: surface the real spawn error cause instead of a fixed "binary
|
|
287
|
+
// not found" message. ENOENT (binary missing) keeps the friendly hint,
|
|
288
|
+
// but EMFILE/EACCES/ENOMEM/bad-cwd failures must name the actual reason
|
|
289
|
+
// so the user is not misdiagnosed into reinstalling the binary.
|
|
290
|
+
const baseMsg = `${this.binary} 命令不可用(未找到或不可执行),请检查是否已安装或在 PATH 中`;
|
|
291
|
+
const msg = spawnErr ? `${baseMsg}:${spawnErr.message}` : baseMsg;
|
|
292
|
+
yield authErrorEvent(msg);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const completion = new Promise((resolve) => {
|
|
296
|
+
proc.once('error', (err) => {
|
|
297
|
+
this.spawnHeartbeat.clear();
|
|
298
|
+
getLogger().error(`[${this.logTag}] spawn failed: ${err.message} binary=${this.binary} cwd=${opts.cwd}`);
|
|
299
|
+
// Resolve (not reject) so the unified result event is emitted
|
|
300
|
+
// via buildResultEvent instead of throwing past the finally
|
|
301
|
+
// into the bridge's catch (which would leak the pid file).
|
|
302
|
+
// P2-12: a post-spawn 'error' (e.g. kill failure, broken pipe)
|
|
303
|
+
// MUST surface as an error result, not success. Previously
|
|
304
|
+
// {code:null,signal:null} was classified as success by
|
|
305
|
+
// buildResultEvent; code=1 forces the error branch so the
|
|
306
|
+
// failure is never silently swallowed as a successful run.
|
|
307
|
+
resolve({ code: 1, signal: null });
|
|
308
|
+
});
|
|
309
|
+
proc.once('close', (code, signal) => {
|
|
310
|
+
this.spawnHeartbeat.clear();
|
|
311
|
+
resolve({ code, signal });
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
const pidDir = path.dirname(this.pidFilePath);
|
|
315
|
+
fs.mkdirSync(pidDir, { recursive: true });
|
|
316
|
+
fs.writeFileSync(this.pidFilePath, String(proc.pid), 'utf-8');
|
|
317
|
+
getLogger().info(`[${this.logTag}] wrote pid file ${this.pidFilePath}=${proc.pid}`);
|
|
318
|
+
this.spawnHeartbeat.start({ pid: proc.pid, binary: this.binary, cwd: opts.cwd });
|
|
319
|
+
proc.stdout?.once('data', () => {
|
|
320
|
+
this.spawnHeartbeat.notifyStdout();
|
|
321
|
+
});
|
|
322
|
+
let stderr = '';
|
|
323
|
+
proc.stderr?.on('data', (chunk) => {
|
|
324
|
+
const text = chunk.toString('utf-8').trim();
|
|
325
|
+
if (text) {
|
|
326
|
+
stderr = (stderr + '\n' + text).trim().slice(-STDERR_TAIL_BYTES);
|
|
327
|
+
// P2-16: most agent CLIs emit progress/warnings/deprecation notices on
|
|
328
|
+
// stderr, not real errors. Logging each chunk at error level drowns out
|
|
329
|
+
// genuine errors. Downgrade to warn — the accumulated stderr is already
|
|
330
|
+
// surfaced at error level in the non-zero-exit result path.
|
|
331
|
+
getLogger().warn(`[${this.logTag} stderr] ${text}`);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
// Hook: write prompt to stdin (codex/opencode)
|
|
335
|
+
this.setupStdin(proc, message);
|
|
336
|
+
// Create translator (per-run state) — null for stateless passthrough (claude)
|
|
337
|
+
const translator = this.createTranslator(opts);
|
|
338
|
+
// Create stream reader (jsonl or readline)
|
|
339
|
+
const stream = this.createStreamReader(proc.stdout);
|
|
340
|
+
try {
|
|
341
|
+
// Stream loop wrapped in try/catch so a stream error (e.g. stdout
|
|
342
|
+
// 'error' event propagated through the async generator) does not
|
|
343
|
+
// bypass the completion await and result event emission. Matches
|
|
344
|
+
// kimi's long-standing behavior; safe for all runners because the
|
|
345
|
+
// completion promise still resolves via proc 'close'.
|
|
346
|
+
try {
|
|
347
|
+
for await (const rawEvent of stream) {
|
|
348
|
+
let translated;
|
|
349
|
+
if (translator) {
|
|
350
|
+
// Per-event error isolation: a translator throw must not kill
|
|
351
|
+
// the stream (pi's accumulator could throw on malformed input).
|
|
352
|
+
try {
|
|
353
|
+
translated = translator.translate(rawEvent);
|
|
354
|
+
}
|
|
355
|
+
catch (err) {
|
|
356
|
+
getLogger().warn(`[${this.logTag}] translator error: ${err.message}, raw: ${JSON.stringify(rawEvent).slice(0, 100)}`);
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
translated = this.translate(rawEvent, { message, opts });
|
|
362
|
+
}
|
|
363
|
+
if (translated === null)
|
|
364
|
+
continue;
|
|
365
|
+
if (Array.isArray(translated)) {
|
|
366
|
+
for (const e of translated)
|
|
367
|
+
yield e;
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
yield translated;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
getLogger().error(`[${this.logTag}] stream error: ${error}`);
|
|
376
|
+
}
|
|
377
|
+
const { code, signal } = await this.awaitCompletion(proc, completion);
|
|
378
|
+
// If the stream ended before a terminal event, let the translator
|
|
379
|
+
// record its terminal state (e.g. codex `finish('failed')` stashes
|
|
380
|
+
// "stream ended before a terminal event"). The runner folds this
|
|
381
|
+
// into buildResultEvent below.
|
|
382
|
+
if (translator && !translator.isTerminal()) {
|
|
383
|
+
const reason = this.stoppedByUser ? 'interrupted' : 'failed';
|
|
384
|
+
translator.finish(reason);
|
|
385
|
+
}
|
|
386
|
+
const translatorError = this.resolveTranslatorError(translator, { code, signal });
|
|
387
|
+
const nonUserError = !this.stoppedByUser && ((code !== null && code !== 0) || signal !== null);
|
|
388
|
+
yield this.buildResultEvent({
|
|
389
|
+
code,
|
|
390
|
+
signal,
|
|
391
|
+
stderr,
|
|
392
|
+
sessionId: translator?.getSessionId?.() ?? '',
|
|
393
|
+
usage: translator?.getLastUsage?.(),
|
|
394
|
+
translatorError,
|
|
395
|
+
});
|
|
396
|
+
if (nonUserError) {
|
|
397
|
+
getLogger().error(`[${this.logTag}] non-zero exit code=${code} signal=${signal} stderr=${stderr.slice(-STDERR_LOG_TAIL)}`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
finally {
|
|
401
|
+
this.spawnHeartbeat.clear();
|
|
402
|
+
// P1-11: 消费者提前关闭生成器(for-await 循环体抛错/break 触发的 .return())
|
|
403
|
+
// 会跳过 completion await 直接进 finally——此时子进程可能仍在运行。若不杀,
|
|
404
|
+
// 它会成为 stop()(currentProcess 已置 null)、killOrphan()(pid 文件已删)、
|
|
405
|
+
// exit handler(同一字段)都够不到的孤儿黑洞,且 stdout 管道背压会把它永久
|
|
406
|
+
// 阻塞。正常完成路径进程已退出,stop 是 no-op,零成本。
|
|
407
|
+
const proc = this.currentProcess;
|
|
408
|
+
if (proc && proc.exitCode === null && proc.signalCode === null) {
|
|
409
|
+
try {
|
|
410
|
+
await this.stopper.stop(proc, { immediate: true });
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
/* ignore */
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
this.currentProcess = null;
|
|
417
|
+
try {
|
|
418
|
+
fs.unlinkSync(this.pidFilePath);
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
/* ignore — file may not exist if spawn failed before write */
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Stop the current process if one is running. Delegates the actual
|
|
427
|
+
* SIGTERM → grace → SIGKILL sequence to `this.stopper.stop(proc, opts)`
|
|
428
|
+
* (see `src/runner/common/process-stopper.ts`), forwarding the `immediate`
|
|
429
|
+
* flag verbatim so all 5 subclasses inherit identical stop semantics.
|
|
430
|
+
*/
|
|
431
|
+
async stop(opts) {
|
|
432
|
+
const proc = this.currentProcess;
|
|
433
|
+
if (!proc || proc.exitCode !== null || proc.signalCode !== null) {
|
|
434
|
+
this.spawnHeartbeat.clear();
|
|
435
|
+
this.currentProcess = null;
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
this.stoppedByUser = true;
|
|
439
|
+
await this.stopper.stop(proc, { immediate: opts?.immediate });
|
|
440
|
+
this.currentProcess = null;
|
|
441
|
+
try {
|
|
442
|
+
fs.unlinkSync(this.pidFilePath);
|
|
443
|
+
}
|
|
444
|
+
catch { }
|
|
445
|
+
getLogger().debug(`[${this.logTag}] cleaned pid file ${this.pidFilePath}`);
|
|
446
|
+
}
|
|
447
|
+
killOrphan() {
|
|
448
|
+
if (!fs.existsSync(this.pidFilePath))
|
|
449
|
+
return;
|
|
450
|
+
try {
|
|
451
|
+
const pidStr = fs.readFileSync(this.pidFilePath, 'utf-8').trim();
|
|
452
|
+
const pid = Number(pidStr);
|
|
453
|
+
if (isNaN(pid) || pid <= 0) {
|
|
454
|
+
fs.unlinkSync(this.pidFilePath);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
// P1-10: pid 复用防护。pid 文件里的 pid 可能是 bridge 崩溃后系统回收复用
|
|
458
|
+
// 给了无关进程——kill(pid, 0) 存活探测无法区分,必须先验证进程身份。
|
|
459
|
+
const match = this.matchPidToBinary(pid);
|
|
460
|
+
if (match !== 'match') {
|
|
461
|
+
// 'gone':ps 查不到/报错 → 进程已消失,陈旧追踪物自愈清除(不杀)。
|
|
462
|
+
// 'mismatch':身份不匹配 → pid 已被无关进程占用,绝不能杀;文件是
|
|
463
|
+
// 陈旧垃圾,一并清除。
|
|
464
|
+
// 统一原则:只有身份匹配才杀;验证失败一律不杀。
|
|
465
|
+
getLogger().warn(`[${this.logTag}] killOrphan: pid ${pid} not confirmed as ${this.binary} ` +
|
|
466
|
+
`(${match}), skipping kill`);
|
|
467
|
+
fs.unlinkSync(this.pidFilePath);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
// 身份匹配:杀整个进程组(detached:true 下 agent 是组长),与
|
|
471
|
+
// ProcessStopper 的 kill(-pid) 语义对齐,子进程不会孤儿化(P1-12)。
|
|
472
|
+
getLogger().info(`[${this.logTag}] killing orphan process group ${-pid}`);
|
|
473
|
+
try {
|
|
474
|
+
process.kill(-pid, 'SIGTERM');
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
// process may have exited
|
|
478
|
+
}
|
|
479
|
+
fs.unlinkSync(this.pidFilePath);
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
// ignore
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* P1-10: verify the process behind a stale pid file is actually ours before
|
|
487
|
+
* sending any signal. `ps -o command=` (not `comm=`) is used because agent
|
|
488
|
+
* binaries may be bash-wrapped scripts whose comm is the interpreter name
|
|
489
|
+
* (`/bin/bash`); the full command line contains the binary path/basename.
|
|
490
|
+
*
|
|
491
|
+
* Returns:
|
|
492
|
+
* 'match' — ps succeeded and the command line contains this.binary
|
|
493
|
+
* (absolute path) or its basename (PATH lookup).
|
|
494
|
+
* 'mismatch' — ps succeeded but the command line is clearly a different
|
|
495
|
+
* process (pid was recycled to an unrelated process).
|
|
496
|
+
* 'unknown' — ps failed / process gone / output unreadable: cannot tell.
|
|
497
|
+
*/
|
|
498
|
+
matchPidToBinary(pid) {
|
|
499
|
+
try {
|
|
500
|
+
const out = execFileSync('ps', ['-p', String(pid), '-o', 'command='], {
|
|
501
|
+
encoding: 'utf-8',
|
|
502
|
+
}).trim();
|
|
503
|
+
if (!out || out.includes('<defunct>'))
|
|
504
|
+
return 'gone';
|
|
505
|
+
const binary = this.binary;
|
|
506
|
+
if (!binary)
|
|
507
|
+
return 'gone';
|
|
508
|
+
const needle = binary.includes('/') ? binary : path.basename(binary);
|
|
509
|
+
return out.includes(needle) ? 'match' : 'mismatch';
|
|
510
|
+
}
|
|
511
|
+
catch {
|
|
512
|
+
// ps 失败(进程不存在/权限)→ 视作 gone:不杀,让陈旧文件自愈
|
|
513
|
+
return 'gone';
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Register this runner with the process-level exit dispatcher (singleton).
|
|
518
|
+
* Process listeners are installed at most once; subsequent calls only add
|
|
519
|
+
* the instance to the internal Set (idempotent — no listener accumulation).
|
|
520
|
+
* Bridge evicts the (cwd, kind) slot after each run; call
|
|
521
|
+
* `unregisterExitHandlers()` there so the instance can be GC'd.
|
|
522
|
+
*/
|
|
523
|
+
registerExitHandlers() {
|
|
524
|
+
registerExitCleanup(this);
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Remove this runner from the process-level exit dispatcher. Called when
|
|
528
|
+
* the bridge evicts the runner cache slot (finalizeRun / interruptCurrentRun
|
|
529
|
+
* / clearRunners) so the instance is no longer retained by the dispatcher.
|
|
530
|
+
*/
|
|
531
|
+
unregisterExitHandlers() {
|
|
532
|
+
unregisterExitCleanup(this);
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Current number of runners registered with the process-level exit
|
|
536
|
+
* dispatcher. Diagnostics / test-support introspection (P1-1 anchor asserts
|
|
537
|
+
* the count returns to baseline after bridge eviction).
|
|
538
|
+
*/
|
|
539
|
+
static getRegisteredExitHandlerCount() {
|
|
540
|
+
return registeredRunners.size;
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Shared process-exit cleanup: SIGTERM the still-running child process and
|
|
544
|
+
* remove the pid file. Called by the singleton dispatcher for every
|
|
545
|
+
* registered runner; previously duplicated verbatim in all 5 runner
|
|
546
|
+
* subclasses.
|
|
547
|
+
*/
|
|
548
|
+
cleanupOnExit() {
|
|
549
|
+
if (this.currentProcess && this.currentProcess.exitCode === null) {
|
|
550
|
+
try {
|
|
551
|
+
// P1-12: 只杀组长会让组内子进程(工具调用起的后台进程)reparent 成孤儿。
|
|
552
|
+
// 复用 ProcessStopper 的 immediate 组杀(SIGTERM+SIGKILL 同步发出,
|
|
553
|
+
// 进程退出路径 fire-and-forget 足够;正常进程退出时是 no-op)。
|
|
554
|
+
void this.stopper.stop(this.currentProcess, { immediate: true });
|
|
555
|
+
}
|
|
556
|
+
catch { }
|
|
557
|
+
}
|
|
558
|
+
try {
|
|
559
|
+
fs.unlinkSync(this.pidFilePath);
|
|
560
|
+
}
|
|
561
|
+
catch { }
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Build a unified result event from process exit state. Centralizes the
|
|
565
|
+
* result-event semantics specified in Candidate 2:
|
|
566
|
+
* - stoppedByUser → error ("interrupted by user")
|
|
567
|
+
* - translatorError set → error (translator-supplied message, e.g. codex
|
|
568
|
+
* `turn.failed` or "stream ended before a terminal event")
|
|
569
|
+
* - code !== 0 && code !== null → error (exit code + stderr tail)
|
|
570
|
+
* - signal !== null → error (killed by signal)
|
|
571
|
+
* - otherwise → success (with optional usage)
|
|
572
|
+
*
|
|
573
|
+
* Error message precedence (most-specific first):
|
|
574
|
+
* - stoppedByUser: "{binary} interrupted by user"
|
|
575
|
+
* - translatorError: verbatim translator-supplied message
|
|
576
|
+
* - signal: "{binary} killed by signal {signal}{stderr tail}"
|
|
577
|
+
* - non-zero code: "{binary} exited code={code}{stderr tail}"
|
|
578
|
+
*
|
|
579
|
+
* @param opts.code - Process exit code (null if killed by signal)
|
|
580
|
+
* @param opts.signal - Process signal code (null if exited normally)
|
|
581
|
+
* @param opts.stderr - Accumulated stderr (tail included in code/signal errors)
|
|
582
|
+
* @param opts.sessionId - Session ID to include in the result event
|
|
583
|
+
* @param opts.usage - Token usage to include on success
|
|
584
|
+
* @param opts.translatorError - Agent-specific terminal error message (e.g.
|
|
585
|
+
* codex turn.failed, or "stream ended early")
|
|
586
|
+
* @returns AgentEvent with type='result'
|
|
587
|
+
*/
|
|
588
|
+
buildResultEvent(opts) {
|
|
589
|
+
const isError = this.stoppedByUser ||
|
|
590
|
+
opts.translatorError !== undefined ||
|
|
591
|
+
(opts.code !== null && opts.code !== 0) ||
|
|
592
|
+
opts.signal !== null;
|
|
593
|
+
const sessionId = opts.sessionId ?? '';
|
|
594
|
+
if (isError) {
|
|
595
|
+
let errorMessage;
|
|
596
|
+
const stderrTail = opts.stderr ? opts.stderr.slice(-STDERR_LOG_TAIL) : '';
|
|
597
|
+
if (this.stoppedByUser) {
|
|
598
|
+
errorMessage = `${this.binary} interrupted by user`;
|
|
599
|
+
}
|
|
600
|
+
else if (opts.translatorError !== undefined) {
|
|
601
|
+
errorMessage = opts.translatorError;
|
|
602
|
+
}
|
|
603
|
+
else if (opts.signal !== null) {
|
|
604
|
+
errorMessage = `${this.binary} killed by signal ${opts.signal}${stderrTail ? `: ${stderrTail}` : ''}`;
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
errorMessage = `${this.binary} exited code=${opts.code}${stderrTail ? `: ${stderrTail}` : ''}`;
|
|
608
|
+
}
|
|
609
|
+
return {
|
|
610
|
+
type: 'result',
|
|
611
|
+
subtype: 'error',
|
|
612
|
+
session_id: sessionId,
|
|
613
|
+
errorMessage,
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
return {
|
|
617
|
+
type: 'result',
|
|
618
|
+
subtype: 'success',
|
|
619
|
+
session_id: sessionId,
|
|
620
|
+
...(opts.usage ? { usage: opts.usage } : {}),
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Stateless passthrough translator hook. Used by ClaudeRunner
|
|
625
|
+
* which does not need per-run translation state. Subclasses that return
|
|
626
|
+
* a translator from `createTranslator(opts)` leave this as the default
|
|
627
|
+
* no-op — the base `run()` loop only calls `translate()` when
|
|
628
|
+
* `createTranslator()` returns null.
|
|
629
|
+
*/
|
|
630
|
+
translate(_rawEvent, _ctx) {
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
//# sourceMappingURL=spawning-runner.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalized usage extracted from a result event.
|
|
3
|
+
*
|
|
4
|
+
* @module runner/common/usage
|
|
5
|
+
*/
|
|
6
|
+
import type { TokenUsage } from '../types.js';
|
|
7
|
+
/** Normalized, camelCase view of a `TokenUsage` payload.
|
|
8
|
+
*
|
|
9
|
+
* **Naming rule** — canonical field names (`cache_read_tokens`,
|
|
10
|
+
* `cache_creation_tokens`) take priority over Anthropic native naming
|
|
11
|
+
* (`cache_read_input_tokens`, `cache_creation_input_tokens`). When both are
|
|
12
|
+
* present the canonical value wins; when only the native name exists it is
|
|
13
|
+
* used as a fallback.
|
|
14
|
+
*
|
|
15
|
+
* **contextLength rule** — `total_tokens` (the agent-declared total) takes
|
|
16
|
+
* priority. When absent, contextLength is reconstructed as
|
|
17
|
+
* `input + cacheRead + cacheCreation + output` (missing cache components
|
|
18
|
+
* default to 0).
|
|
19
|
+
*/
|
|
20
|
+
interface NormalizedResultUsage {
|
|
21
|
+
inputTokens: number;
|
|
22
|
+
outputTokens: number;
|
|
23
|
+
cacheReadTokens?: number;
|
|
24
|
+
cacheCreationTokens?: number;
|
|
25
|
+
totalTokens?: number;
|
|
26
|
+
/** Reconstructed or agent-declared context length (see JSDoc above). */
|
|
27
|
+
contextLength: number;
|
|
28
|
+
}
|
|
29
|
+
/** Extract and normalize usage fields from a result event payload. */
|
|
30
|
+
export declare function normalizeResultUsage(usage: TokenUsage): NormalizedResultUsage;
|
|
31
|
+
export {};
|