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,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Abstract base class for exec-stream translators (codex, opencode, pi, kimi).
|
|
3
|
+
*
|
|
4
|
+
* Encapsulates the shared state machine and guard logic that was duplicated
|
|
5
|
+
* across every concrete translator:
|
|
6
|
+
* - Terminal-state tracking (idempotent `finish()`)
|
|
7
|
+
* - `translate()` guard (terminal → []; non-record / non-string-type → anomaly)
|
|
8
|
+
* - `recordUnknownEvent()` helper (warn-logs unknown event types)
|
|
9
|
+
* - Session / usage / error getters
|
|
10
|
+
*
|
|
11
|
+
* Subclasses provide:
|
|
12
|
+
* - `logTag` — prefix for warn logs
|
|
13
|
+
* - `streamEndedMessage()` — agent-specific "stream ended early" message
|
|
14
|
+
* - `translateEvent(raw)` — agent-specific event dispatch
|
|
15
|
+
*
|
|
16
|
+
* @see SpawningRunner for the runner that consumes translators
|
|
17
|
+
*/
|
|
18
|
+
import { getLogger } from '../../logger/index.js';
|
|
19
|
+
import { isRecord } from '../../common/guards.js';
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Abstract base class
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
export class ExecTranslator {
|
|
24
|
+
// --- Shared state ---
|
|
25
|
+
terminal = false;
|
|
26
|
+
lastError;
|
|
27
|
+
lastUsage;
|
|
28
|
+
terminalErrorMessage;
|
|
29
|
+
terminalErrorFromAgent = false;
|
|
30
|
+
sessionId;
|
|
31
|
+
startedTools = new Set();
|
|
32
|
+
// --- translate() guard (shared) ---
|
|
33
|
+
/** Translate a single parsed ndjson line into zero or more `AgentEvent`s.
|
|
34
|
+
* P3-3: filter paths (terminal / non-record / non-string-type) return `null`
|
|
35
|
+
* instead of `[]` to avoid a throwaway array allocation per non-emitting
|
|
36
|
+
* event on codex/opencode hot paths. The consumer (SpawningRunner) already
|
|
37
|
+
* handles `null` via `Array.isArray` + null guards. */
|
|
38
|
+
translate(raw) {
|
|
39
|
+
if (this.terminal)
|
|
40
|
+
return null;
|
|
41
|
+
if (!isRecord(raw) || typeof raw.type !== 'string') {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return this.translateEvent(raw);
|
|
45
|
+
}
|
|
46
|
+
// --- recordUnknownEvent helper ---
|
|
47
|
+
/** Warn-log an unknown event type and return `null` (P3-3: no throwaway []). */
|
|
48
|
+
recordUnknownEvent(type) {
|
|
49
|
+
getLogger().warn(`${this.logTag} unknown event type: ${type}`);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
// --- finish() (shared, idempotent) ---
|
|
53
|
+
/**
|
|
54
|
+
* Called when the stream ends without a natural terminal event.
|
|
55
|
+
*
|
|
56
|
+
* - Marks the translator terminal (idempotent — second call returns `[]`).
|
|
57
|
+
* - For `reason='failed'`, stores `terminalErrorMessage` so the runner can
|
|
58
|
+
* surface it via `buildResultEvent({translatorError})`.
|
|
59
|
+
* - Returns `[]` — the runner is responsible for emitting the result event.
|
|
60
|
+
*/
|
|
61
|
+
finish(reason = 'failed') {
|
|
62
|
+
if (this.terminal)
|
|
63
|
+
return [];
|
|
64
|
+
this.terminal = true;
|
|
65
|
+
if (reason === 'failed') {
|
|
66
|
+
const detail = this.lastError ? `: ${this.lastError}` : '';
|
|
67
|
+
this.terminalErrorMessage = `${this.streamEndedMessage()}${detail}`;
|
|
68
|
+
}
|
|
69
|
+
// interrupted / timeout: no terminalErrorMessage — the runner's
|
|
70
|
+
// stoppedByUser flag (set by base class stop()) drives the result subtype.
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
// --- Getters (shared) ---
|
|
74
|
+
/** Whether a terminal event has already been seen. */
|
|
75
|
+
isTerminal() {
|
|
76
|
+
return this.terminal;
|
|
77
|
+
}
|
|
78
|
+
/** Session ID captured from the agent stream (for the result event). */
|
|
79
|
+
getSessionId() {
|
|
80
|
+
return this.sessionId;
|
|
81
|
+
}
|
|
82
|
+
/** Token usage captured from the terminal event (for the success result event). */
|
|
83
|
+
getLastUsage() {
|
|
84
|
+
return this.lastUsage;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Terminal error message captured from an agent-reported error or
|
|
88
|
+
* `finish('failed')`. Returns `undefined` if no terminal error occurred.
|
|
89
|
+
*/
|
|
90
|
+
getTerminalError() {
|
|
91
|
+
return this.terminalErrorMessage;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* True iff `getTerminalError()` was populated by an agent-reported terminal
|
|
95
|
+
* error (e.g. codex `turn.failed`). The runner uses this to pick the correct
|
|
96
|
+
* root cause for `buildResultEvent`: agent errors override signal/non-zero-code
|
|
97
|
+
* (the agent error is the root cause), but stream-ended-early errors
|
|
98
|
+
* (`finish('failed')`) yield to signal/non-zero-code (the signal is the root
|
|
99
|
+
* cause and the early stream-end is a symptom).
|
|
100
|
+
*/
|
|
101
|
+
hasAgentTerminalError() {
|
|
102
|
+
return this.terminalErrorFromAgent;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=exec-translator.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { AgentEvent } from '../types.js';
|
|
2
|
+
import type { Readable } from 'node:stream';
|
|
3
|
+
/** Options for {@link createJSONLStream}. */
|
|
4
|
+
interface JSONLStreamOptions {
|
|
5
|
+
onParseError?: (line: string) => void;
|
|
6
|
+
/**
|
|
7
|
+
* Pause stdout once `lineQueue` exceeds this depth (P1-4 backpressure).
|
|
8
|
+
* Default 100 (matches production; tests may pass a smaller value).
|
|
9
|
+
*/
|
|
10
|
+
pauseThreshold?: number;
|
|
11
|
+
/**
|
|
12
|
+
* Resume stdout once `lineQueue` drains to or below this depth. Must be
|
|
13
|
+
* <= `pauseThreshold`. Only meaningful when `pauseThreshold > 0`.
|
|
14
|
+
*/
|
|
15
|
+
resumeThreshold?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Parse JSONL (newline-delimited JSON) from a readable stream.
|
|
19
|
+
*
|
|
20
|
+
* Handles:
|
|
21
|
+
* - Partial lines at buffer boundaries
|
|
22
|
+
* - Trailing incomplete lines (flush on stream end/close)
|
|
23
|
+
* - JSON parse errors: when `options.onParseError` is provided, bad lines are
|
|
24
|
+
* forwarded to the callback; otherwise a warning is logged via getLogger().warn
|
|
25
|
+
* and the line is skipped.
|
|
26
|
+
* - Optional backpressure (P1-4): when `pauseThreshold` is set, stdout is
|
|
27
|
+
* paused once the parsed-line queue exceeds the threshold and resumed once
|
|
28
|
+
* the consumer drains it back below `resumeThreshold`, bounding memory in
|
|
29
|
+
* long runs where the producer (agent stdout) outpaces the consumer (card
|
|
30
|
+
* rendering + SDK patch).
|
|
31
|
+
*
|
|
32
|
+
* This is the core stream parsing logic shared by Claude, Pi, and other
|
|
33
|
+
* agents that emit JSONL output.
|
|
34
|
+
*
|
|
35
|
+
* Note: timestamp generation is delegated to each runner's translate method
|
|
36
|
+
* (统一方案:所有 runner 自己生成 timestamp).
|
|
37
|
+
*/
|
|
38
|
+
export declare function createJSONLStream(stdout: Readable, options?: JSONLStreamOptions): AsyncGenerator<AgentEvent>;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { getLogger } from '../../logger/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parse JSONL (newline-delimited JSON) from a readable stream.
|
|
4
|
+
*
|
|
5
|
+
* Handles:
|
|
6
|
+
* - Partial lines at buffer boundaries
|
|
7
|
+
* - Trailing incomplete lines (flush on stream end/close)
|
|
8
|
+
* - JSON parse errors: when `options.onParseError` is provided, bad lines are
|
|
9
|
+
* forwarded to the callback; otherwise a warning is logged via getLogger().warn
|
|
10
|
+
* and the line is skipped.
|
|
11
|
+
* - Optional backpressure (P1-4): when `pauseThreshold` is set, stdout is
|
|
12
|
+
* paused once the parsed-line queue exceeds the threshold and resumed once
|
|
13
|
+
* the consumer drains it back below `resumeThreshold`, bounding memory in
|
|
14
|
+
* long runs where the producer (agent stdout) outpaces the consumer (card
|
|
15
|
+
* rendering + SDK patch).
|
|
16
|
+
*
|
|
17
|
+
* This is the core stream parsing logic shared by Claude, Pi, and other
|
|
18
|
+
* agents that emit JSONL output.
|
|
19
|
+
*
|
|
20
|
+
* Note: timestamp generation is delegated to each runner's translate method
|
|
21
|
+
* (统一方案:所有 runner 自己生成 timestamp).
|
|
22
|
+
*/
|
|
23
|
+
export function createJSONLStream(stdout, options) {
|
|
24
|
+
const lineQueue = [];
|
|
25
|
+
// P3-1: accumulate partial-line fragments in an array instead of repeatedly
|
|
26
|
+
// string-concatenating a growing `partialLine`. A huge single JSON line
|
|
27
|
+
// (large tool_result / text_end) spanning many chunks caused O(n²) copies:
|
|
28
|
+
// each chunk rebuilt `partialLine + text` over the whole accumulated prefix.
|
|
29
|
+
// With a fragment array, we join only once per chunk (at the line boundary)
|
|
30
|
+
// and push complete lines; the residual tail stays as array fragments until
|
|
31
|
+
// the next newline or stream end. Net allocation: O(n) instead of O(n²).
|
|
32
|
+
const partialFragments = [];
|
|
33
|
+
// P2-14: accumulated byte length of partialFragments. Backpressure (above)
|
|
34
|
+
// only counts COMPLETE lines in lineQueue; a never-newline stream (malformed
|
|
35
|
+
// output, binary garbage) accumulates fragments without ever completing a
|
|
36
|
+
// line, so pause never fires and memory grows unbounded. Cap the partial
|
|
37
|
+
// line at 10MB: once exceeded, flush it as a bad line via onParseError and
|
|
38
|
+
// reset the buffer, bounding peak memory for a single pathological line.
|
|
39
|
+
const PARTIAL_BYTE_LIMIT = 10 * 1024 * 1024;
|
|
40
|
+
let partialBytes = 0;
|
|
41
|
+
const completeLines = [];
|
|
42
|
+
let done = false;
|
|
43
|
+
let resolveNext = null;
|
|
44
|
+
// P1-4 backpressure thresholds. Default matches production (100).
|
|
45
|
+
const pauseThreshold = options?.pauseThreshold ?? 100;
|
|
46
|
+
// Default low-water mark is half the high-water mark for meaningful hysteresis
|
|
47
|
+
// (avoids per-line pause/resume thrashing when callers don't set it explicitly).
|
|
48
|
+
let resumeThreshold = options?.resumeThreshold ?? Math.max(0, Math.floor(pauseThreshold / 2));
|
|
49
|
+
// Clamp misconfiguration: resumeThreshold > pauseThreshold would resume at queue
|
|
50
|
+
// high water then immediately re-pause on the next line → thrash (P1-4 review nit).
|
|
51
|
+
if (resumeThreshold > pauseThreshold)
|
|
52
|
+
resumeThreshold = pauseThreshold;
|
|
53
|
+
let paused = false;
|
|
54
|
+
function notify() {
|
|
55
|
+
if (resolveNext) {
|
|
56
|
+
resolveNext();
|
|
57
|
+
resolveNext = null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Flush any trailing partial line and mark the stream finished. Shared by the
|
|
61
|
+
// 'end'/'close'/'error' handlers so flush logic lives in one place (P1-4 review nit).
|
|
62
|
+
function flushAndFinish() {
|
|
63
|
+
if (partialFragments.length > 0) {
|
|
64
|
+
const trimmed = partialFragments.join('').trim();
|
|
65
|
+
if (trimmed)
|
|
66
|
+
lineQueue.push(trimmed);
|
|
67
|
+
partialFragments.length = 0;
|
|
68
|
+
partialBytes = 0;
|
|
69
|
+
}
|
|
70
|
+
done = true;
|
|
71
|
+
notify();
|
|
72
|
+
}
|
|
73
|
+
// P2-14: flush an oversize partial line as a bad line. When partialFragments
|
|
74
|
+
// exceeds the byte cap, the partial line is treated as malformed (a single
|
|
75
|
+
// JSON line should never reach 10MB) and reported via onParseError rather
|
|
76
|
+
// than accumulated indefinitely. Resets the buffer so a pathological stream
|
|
77
|
+
// cannot leak memory line after line.
|
|
78
|
+
function flushOversizePartial() {
|
|
79
|
+
if (partialFragments.length === 0)
|
|
80
|
+
return;
|
|
81
|
+
const line = partialFragments.join('');
|
|
82
|
+
partialFragments.length = 0;
|
|
83
|
+
partialBytes = 0;
|
|
84
|
+
const trimmed = line.trim();
|
|
85
|
+
if (!trimmed)
|
|
86
|
+
return;
|
|
87
|
+
if (options?.onParseError) {
|
|
88
|
+
options.onParseError(trimmed);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
getLogger().warn(`[jsonl-stream] dropped oversize partial line (${line.length} bytes)`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Apply backpressure after pushing new lines: pause stdout when the queue
|
|
95
|
+
// exceeds the high-water mark so the producer stops feeding until the
|
|
96
|
+
// consumer drains it. P1-4 bounds memory in long runs.
|
|
97
|
+
function maybePause() {
|
|
98
|
+
if (pauseThreshold > 0 && !paused && lineQueue.length > pauseThreshold) {
|
|
99
|
+
paused = true;
|
|
100
|
+
stdout.pause();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Release backpressure after consuming a line: resume stdout once the queue
|
|
104
|
+
// drains to the low-water mark so the producer can feed again.
|
|
105
|
+
function maybeResume() {
|
|
106
|
+
if (paused && lineQueue.length <= resumeThreshold) {
|
|
107
|
+
paused = false;
|
|
108
|
+
stdout.resume();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
stdout.on('data', (chunk) => {
|
|
112
|
+
const text = chunk.toString('utf-8');
|
|
113
|
+
// P3-1: split on newlines. The first segment continues the pending partial
|
|
114
|
+
// line (joined with accumulated fragments — one join per chunk, not per
|
|
115
|
+
// byte); subsequent segments are complete lines. The last segment is the
|
|
116
|
+
// new partial tail (kept as a single fragment).
|
|
117
|
+
const segments = text.split('\n');
|
|
118
|
+
if (segments.length === 1) {
|
|
119
|
+
// No newline in this chunk: the whole chunk is a partial fragment.
|
|
120
|
+
partialFragments.push(segments[0]);
|
|
121
|
+
partialBytes += segments[0].length;
|
|
122
|
+
// P2-14: cap the partial line. A never-newline stream would otherwise
|
|
123
|
+
// accumulate fragments without bound (backpressure only counts complete
|
|
124
|
+
// lines). Flush as a bad line once the byte cap is exceeded.
|
|
125
|
+
if (partialBytes > PARTIAL_BYTE_LIMIT) {
|
|
126
|
+
flushOversizePartial();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
// First segment completes the pending line.
|
|
131
|
+
partialFragments.push(segments[0]);
|
|
132
|
+
partialBytes += segments[0].length;
|
|
133
|
+
completeLines.push(partialFragments.join(''));
|
|
134
|
+
partialFragments.length = 0;
|
|
135
|
+
partialBytes = 0;
|
|
136
|
+
// Middle segments are each complete lines.
|
|
137
|
+
for (let i = 1; i < segments.length - 1; i++) {
|
|
138
|
+
completeLines.push(segments[i]);
|
|
139
|
+
}
|
|
140
|
+
// Last segment is the new partial tail.
|
|
141
|
+
partialFragments.push(segments[segments.length - 1]);
|
|
142
|
+
partialBytes = segments[segments.length - 1].length;
|
|
143
|
+
// P2-14: the trailing partial tail can itself exceed the cap if a single
|
|
144
|
+
// chunk is larger than 10MB with no internal newline.
|
|
145
|
+
if (partialBytes > PARTIAL_BYTE_LIMIT) {
|
|
146
|
+
flushOversizePartial();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Push complete lines into the queue (skipping blank/whitespace-only).
|
|
150
|
+
for (const line of completeLines) {
|
|
151
|
+
const trimmed = line.trim();
|
|
152
|
+
if (trimmed)
|
|
153
|
+
lineQueue.push(trimmed);
|
|
154
|
+
}
|
|
155
|
+
completeLines.length = 0;
|
|
156
|
+
notify();
|
|
157
|
+
maybePause();
|
|
158
|
+
});
|
|
159
|
+
stdout.on('end', () => {
|
|
160
|
+
flushAndFinish();
|
|
161
|
+
});
|
|
162
|
+
stdout.on('close', () => {
|
|
163
|
+
flushAndFinish();
|
|
164
|
+
});
|
|
165
|
+
// P1-4: handle stdout 'error' so the generator terminates instead of
|
|
166
|
+
// hanging forever on the `await new Promise` below. Without this, a stdout
|
|
167
|
+
// error (e.g. broken pipe, runner crash) leaves the consumer awaiting a
|
|
168
|
+
// notify that never comes → bridge serial queue blocks permanently. We do
|
|
169
|
+
// not re-throw the error through the generator (the runner's `try/catch`
|
|
170
|
+
// around `for await` and the proc 'close' path already surface failures via
|
|
171
|
+
// the unified result event); we only guarantee the generator ends. The error
|
|
172
|
+
// is logged for diagnosis.
|
|
173
|
+
stdout.on('error', (err) => {
|
|
174
|
+
getLogger().warn(`[jsonl-stream] stdout error: ${err.message}`);
|
|
175
|
+
flushAndFinish();
|
|
176
|
+
});
|
|
177
|
+
async function* generator() {
|
|
178
|
+
while (true) {
|
|
179
|
+
while (lineQueue.length > 0) {
|
|
180
|
+
const line = lineQueue.shift();
|
|
181
|
+
try {
|
|
182
|
+
yield JSON.parse(line);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
if (options?.onParseError) {
|
|
186
|
+
options.onParseError(line);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
getLogger().warn(`[jsonl-stream] failed to parse JSONL: ${line.slice(0, 100)}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
maybeResume();
|
|
193
|
+
}
|
|
194
|
+
if (done)
|
|
195
|
+
return;
|
|
196
|
+
await new Promise((r) => {
|
|
197
|
+
resolveNext = r;
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return generator();
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=jsonl-stream.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ChildProcess } from 'node:child_process';
|
|
2
|
+
interface ProcessStopperOptions {
|
|
3
|
+
graceMs: number;
|
|
4
|
+
}
|
|
5
|
+
interface StopOptions {
|
|
6
|
+
immediate?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Unified process stop logic: SIGTERM → grace period → SIGKILL.
|
|
10
|
+
*
|
|
11
|
+
* Extracts the common pattern shared by ClaudeRunner and BashProcessRunner
|
|
12
|
+
* so both use identical stop semantics.
|
|
13
|
+
*/
|
|
14
|
+
export declare class ProcessStopper {
|
|
15
|
+
private graceMs;
|
|
16
|
+
constructor(opts: ProcessStopperOptions);
|
|
17
|
+
/**
|
|
18
|
+
* Stop a child process: SIGTERM → wait grace → SIGKILL.
|
|
19
|
+
*
|
|
20
|
+
* Uses negative PID to kill the entire process group, ensuring child
|
|
21
|
+
* processes (shell wrappers, sub-processes) are also terminated.
|
|
22
|
+
*
|
|
23
|
+
* Pass `immediate: true` to skip the grace window — SIGTERM and SIGKILL
|
|
24
|
+
* are sent back-to-back. Safe to call on an already-exited process.
|
|
25
|
+
*/
|
|
26
|
+
stop(proc: ChildProcess, opts?: StopOptions): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { getLogger } from '../../logger/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Unified process stop logic: SIGTERM → grace period → SIGKILL.
|
|
4
|
+
*
|
|
5
|
+
* Extracts the common pattern shared by ClaudeRunner and BashProcessRunner
|
|
6
|
+
* so both use identical stop semantics.
|
|
7
|
+
*/
|
|
8
|
+
export class ProcessStopper {
|
|
9
|
+
graceMs;
|
|
10
|
+
constructor(opts) {
|
|
11
|
+
this.graceMs = opts.graceMs;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Stop a child process: SIGTERM → wait grace → SIGKILL.
|
|
15
|
+
*
|
|
16
|
+
* Uses negative PID to kill the entire process group, ensuring child
|
|
17
|
+
* processes (shell wrappers, sub-processes) are also terminated.
|
|
18
|
+
*
|
|
19
|
+
* Pass `immediate: true` to skip the grace window — SIGTERM and SIGKILL
|
|
20
|
+
* are sent back-to-back. Safe to call on an already-exited process.
|
|
21
|
+
*/
|
|
22
|
+
async stop(proc, opts) {
|
|
23
|
+
if (!proc || proc.exitCode !== null || proc.signalCode !== null) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const pid = proc.pid;
|
|
27
|
+
if (pid === undefined) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
// Use negative PID to kill the entire process group.
|
|
31
|
+
// This ensures child processes (shell wrappers, sub-processes) are also killed.
|
|
32
|
+
// If the process is not a group leader, kill(-pid) behaves same as kill(pid).
|
|
33
|
+
const pgid = -pid;
|
|
34
|
+
const immediate = opts?.immediate === true;
|
|
35
|
+
getLogger().debug(`[process-stopper] sending SIGTERM to pgid=${pgid} immediate=${immediate}`);
|
|
36
|
+
try {
|
|
37
|
+
process.kill(pgid, 'SIGTERM');
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
/* process may have exited */
|
|
41
|
+
}
|
|
42
|
+
if (immediate) {
|
|
43
|
+
// Don't wait — send SIGKILL back-to-back. Even if proc just exited
|
|
44
|
+
// from SIGTERM, kill(SIGKILL) on a dead pid throws ESRCH which we ignore.
|
|
45
|
+
try {
|
|
46
|
+
process.kill(pgid, 'SIGKILL');
|
|
47
|
+
}
|
|
48
|
+
catch { }
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
// Wait for exit with grace period. Use `once` so repeated stop() calls
|
|
52
|
+
// don't accumulate listeners on the proc. P2-15: save the grace timer id
|
|
53
|
+
// and clear it once the race settles — otherwise the timer lingers after
|
|
54
|
+
// an early exit, keeping the event loop alive and accumulating across
|
|
55
|
+
// frequent stop() calls.
|
|
56
|
+
let graceTimer;
|
|
57
|
+
const exited = await Promise.race([
|
|
58
|
+
new Promise((resolve) => {
|
|
59
|
+
if (proc.exitCode !== null || proc.signalCode !== null)
|
|
60
|
+
return resolve(true);
|
|
61
|
+
proc.once('exit', () => resolve(true));
|
|
62
|
+
}),
|
|
63
|
+
new Promise((resolve) => {
|
|
64
|
+
graceTimer = setTimeout(() => resolve(false), this.graceMs);
|
|
65
|
+
}),
|
|
66
|
+
]);
|
|
67
|
+
if (graceTimer)
|
|
68
|
+
clearTimeout(graceTimer);
|
|
69
|
+
if (!exited) {
|
|
70
|
+
getLogger().info(`[process-stopper] process group ${pgid} did not exit within grace period, sending SIGKILL`);
|
|
71
|
+
try {
|
|
72
|
+
process.kill(pgid, 'SIGKILL');
|
|
73
|
+
}
|
|
74
|
+
catch { }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=process-stopper.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ChildProcess } from 'node:child_process';
|
|
2
|
+
import type { AgentEvent } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Return the stdio config ['pipe','pipe','pipe'] used by runners that need
|
|
5
|
+
* to write the prompt to the child's stdin (codex/opencode).
|
|
6
|
+
*/
|
|
7
|
+
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
|
+
/**
|
|
15
|
+
* Build a standardized auth/error result event for yield.
|
|
16
|
+
*
|
|
17
|
+
* Used by CodexExecRunner and OpencodeExecRunner when initialization or request
|
|
18
|
+
* sending fails (e.g. not logged in, session creation failed). Produces a
|
|
19
|
+
* `result`/`error` event so the bridge can render a card instead of crashing.
|
|
20
|
+
*/
|
|
21
|
+
export declare function authErrorEvent(errorMessage: string, sessionId?: string): AgentEvent;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Return the stdio config ['pipe','pipe','pipe'] used by runners that need
|
|
3
|
+
* to write the prompt to the child's stdin (codex/opencode).
|
|
4
|
+
*/
|
|
5
|
+
export function pipeAllStdio() {
|
|
6
|
+
return ['pipe', 'pipe', 'pipe'];
|
|
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
|
+
/**
|
|
50
|
+
* Build a standardized auth/error result event for yield.
|
|
51
|
+
*
|
|
52
|
+
* Used by CodexExecRunner and OpencodeExecRunner when initialization or request
|
|
53
|
+
* sending fails (e.g. not logged in, session creation failed). Produces a
|
|
54
|
+
* `result`/`error` event so the bridge can render a card instead of crashing.
|
|
55
|
+
*/
|
|
56
|
+
export function authErrorEvent(errorMessage, sessionId = '') {
|
|
57
|
+
return {
|
|
58
|
+
type: 'result',
|
|
59
|
+
subtype: 'error',
|
|
60
|
+
session_id: sessionId,
|
|
61
|
+
errorMessage,
|
|
62
|
+
timestamp: new Date().toISOString(),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=runner-utils.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spawn-stage heartbeat: warns when a spawned process emits no stdout within
|
|
3
|
+
* a configured window. Covers the gap between spawn and the first stdout
|
|
4
|
+
* chunk (OAuth prompt, stdio fd misroute, cwd not reachable) where the
|
|
5
|
+
* idle watchdog has not started yet.
|
|
6
|
+
*
|
|
7
|
+
* Extracted from ClaudeRunner and CodexRunner which had identical patterns
|
|
8
|
+
* (field trio + setTimeout + three clearTimeout call sites).
|
|
9
|
+
*
|
|
10
|
+
* Only logs a WARN — never auto-stops. Spawn-stage issues usually need user
|
|
11
|
+
* intervention (re-login OAuth), so killing automatically would confuse users.
|
|
12
|
+
*/
|
|
13
|
+
export declare class SpawnHeartbeat {
|
|
14
|
+
private readonly ms;
|
|
15
|
+
private readonly logTag;
|
|
16
|
+
private timer;
|
|
17
|
+
private firstStdoutSeen;
|
|
18
|
+
constructor(ms: number, logTag: string);
|
|
19
|
+
/**
|
|
20
|
+
* Start the heartbeat timer. If no stdout is observed within `ms`,
|
|
21
|
+
* log a WARN with the spawn context. Safe to call again after `clear()`
|
|
22
|
+
* (e.g. across multiple runs).
|
|
23
|
+
*/
|
|
24
|
+
start(ctx: {
|
|
25
|
+
pid: number;
|
|
26
|
+
binary: string;
|
|
27
|
+
cwd: string;
|
|
28
|
+
}): void;
|
|
29
|
+
/** Mark that stdout has been observed; clears the timer. */
|
|
30
|
+
notifyStdout(): void;
|
|
31
|
+
/** Clear the timer (safe to call multiple times). */
|
|
32
|
+
clear(): void;
|
|
33
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { getLogger } from '../../logger/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Spawn-stage heartbeat: warns when a spawned process emits no stdout within
|
|
4
|
+
* a configured window. Covers the gap between spawn and the first stdout
|
|
5
|
+
* chunk (OAuth prompt, stdio fd misroute, cwd not reachable) where the
|
|
6
|
+
* idle watchdog has not started yet.
|
|
7
|
+
*
|
|
8
|
+
* Extracted from ClaudeRunner and CodexRunner which had identical patterns
|
|
9
|
+
* (field trio + setTimeout + three clearTimeout call sites).
|
|
10
|
+
*
|
|
11
|
+
* Only logs a WARN — never auto-stops. Spawn-stage issues usually need user
|
|
12
|
+
* intervention (re-login OAuth), so killing automatically would confuse users.
|
|
13
|
+
*/
|
|
14
|
+
export class SpawnHeartbeat {
|
|
15
|
+
ms;
|
|
16
|
+
logTag;
|
|
17
|
+
timer = null;
|
|
18
|
+
firstStdoutSeen = false;
|
|
19
|
+
constructor(ms, logTag) {
|
|
20
|
+
this.ms = ms;
|
|
21
|
+
this.logTag = logTag;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Start the heartbeat timer. If no stdout is observed within `ms`,
|
|
25
|
+
* log a WARN with the spawn context. Safe to call again after `clear()`
|
|
26
|
+
* (e.g. across multiple runs).
|
|
27
|
+
*/
|
|
28
|
+
start(ctx) {
|
|
29
|
+
this.firstStdoutSeen = false;
|
|
30
|
+
this.clear();
|
|
31
|
+
this.timer = setTimeout(() => {
|
|
32
|
+
if (!this.firstStdoutSeen) {
|
|
33
|
+
getLogger().warn(`[${this.logTag}] spawn stage stalled: pid=${ctx.pid} binary=${ctx.binary} ` +
|
|
34
|
+
`cwd=${ctx.cwd} — process spawned ${this.ms / 1000}s ago with no stdout output.`);
|
|
35
|
+
}
|
|
36
|
+
}, this.ms);
|
|
37
|
+
}
|
|
38
|
+
/** Mark that stdout has been observed; clears the timer. */
|
|
39
|
+
notifyStdout() {
|
|
40
|
+
this.firstStdoutSeen = true;
|
|
41
|
+
this.clear();
|
|
42
|
+
}
|
|
43
|
+
/** Clear the timer (safe to call multiple times). */
|
|
44
|
+
clear() {
|
|
45
|
+
if (this.timer) {
|
|
46
|
+
clearTimeout(this.timer);
|
|
47
|
+
this.timer = null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=spawn-heartbeat.js.map
|