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.
Files changed (201) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/LICENSE +21 -0
  3. package/README.en.md +162 -0
  4. package/README.md +161 -0
  5. package/dist/binder.d.ts +57 -0
  6. package/dist/binder.js +105 -0
  7. package/dist/bridge/index.d.ts +338 -0
  8. package/dist/bridge/index.js +1281 -0
  9. package/dist/bridge/queue-manager.d.ts +277 -0
  10. package/dist/bridge/queue-manager.js +729 -0
  11. package/dist/card/bash-card-session.d.ts +78 -0
  12. package/dist/card/bash-card-session.js +174 -0
  13. package/dist/card/bash-renderer.d.ts +24 -0
  14. package/dist/card/bash-renderer.js +187 -0
  15. package/dist/card/card-budget.d.ts +31 -0
  16. package/dist/card/card-budget.js +280 -0
  17. package/dist/card/card-session.d.ts +83 -0
  18. package/dist/card/card-session.js +185 -0
  19. package/dist/card/card-shared.d.ts +28 -0
  20. package/dist/card/card-shared.js +99 -0
  21. package/dist/card/collapsible.d.ts +26 -0
  22. package/dist/card/collapsible.js +87 -0
  23. package/dist/card/run-card-session.d.ts +78 -0
  24. package/dist/card/run-card-session.js +165 -0
  25. package/dist/card/run-renderer.d.ts +66 -0
  26. package/dist/card/run-renderer.js +841 -0
  27. package/dist/card/run-state.d.ts +103 -0
  28. package/dist/card/run-state.js +313 -0
  29. package/dist/card/text-truncate.d.ts +56 -0
  30. package/dist/card/text-truncate.js +184 -0
  31. package/dist/card/time.d.ts +1 -0
  32. package/dist/card/time.js +14 -0
  33. package/dist/card/tool-render.d.ts +25 -0
  34. package/dist/card/tool-render.js +176 -0
  35. package/dist/cli.d.ts +28 -0
  36. package/dist/cli.js +89 -0
  37. package/dist/common/guards.d.ts +37 -0
  38. package/dist/common/guards.js +65 -0
  39. package/dist/config/codex-config.d.ts +113 -0
  40. package/dist/config/codex-config.js +428 -0
  41. package/dist/config/dir.d.ts +17 -0
  42. package/dist/config/dir.js +81 -0
  43. package/dist/config/index.d.ts +219 -0
  44. package/dist/config/index.js +388 -0
  45. package/dist/config/kimi-config.d.ts +33 -0
  46. package/dist/config/kimi-config.js +111 -0
  47. package/dist/config/opencode-config.d.ts +20 -0
  48. package/dist/config/opencode-config.js +124 -0
  49. package/dist/config/pi-config.d.ts +49 -0
  50. package/dist/config/pi-config.js +151 -0
  51. package/dist/config/wizard.d.ts +12 -0
  52. package/dist/config/wizard.js +84 -0
  53. package/dist/connector/file-limits.d.ts +7 -0
  54. package/dist/connector/file-limits.js +8 -0
  55. package/dist/connector/index.d.ts +86 -0
  56. package/dist/connector/index.js +340 -0
  57. package/dist/error-classification.d.ts +15 -0
  58. package/dist/error-classification.js +24 -0
  59. package/dist/index.d.ts +1 -0
  60. package/dist/index.js +580 -0
  61. package/dist/instance-lock.d.ts +30 -0
  62. package/dist/instance-lock.js +184 -0
  63. package/dist/logger/index.d.ts +57 -0
  64. package/dist/logger/index.js +178 -0
  65. package/dist/order/index.d.ts +27 -0
  66. package/dist/order/index.js +90 -0
  67. package/dist/persistence/atomic-write.d.ts +10 -0
  68. package/dist/persistence/atomic-write.js +65 -0
  69. package/dist/persistence/load-json-file.d.ts +9 -0
  70. package/dist/persistence/load-json-file.js +23 -0
  71. package/dist/restart.d.ts +24 -0
  72. package/dist/restart.js +73 -0
  73. package/dist/router/card-helpers.d.ts +28 -0
  74. package/dist/router/card-helpers.js +65 -0
  75. package/dist/router/config/claude.d.ts +14 -0
  76. package/dist/router/config/claude.js +72 -0
  77. package/dist/router/config/codex.d.ts +22 -0
  78. package/dist/router/config/codex.js +100 -0
  79. package/dist/router/config/common/render.d.ts +16 -0
  80. package/dist/router/config/common/render.js +158 -0
  81. package/dist/router/config/index.d.ts +18 -0
  82. package/dist/router/config/index.js +38 -0
  83. package/dist/router/config/kimi.d.ts +21 -0
  84. package/dist/router/config/kimi.js +63 -0
  85. package/dist/router/config/opencode.d.ts +12 -0
  86. package/dist/router/config/opencode.js +44 -0
  87. package/dist/router/config/pi.d.ts +12 -0
  88. package/dist/router/config/pi.js +50 -0
  89. package/dist/router/config/types.d.ts +43 -0
  90. package/dist/router/config/types.js +8 -0
  91. package/dist/router/index.d.ts +318 -0
  92. package/dist/router/index.js +2715 -0
  93. package/dist/router/order-exec-dispatch.d.ts +30 -0
  94. package/dist/router/order-exec-dispatch.js +48 -0
  95. package/dist/router/utils.d.ts +44 -0
  96. package/dist/router/utils.js +116 -0
  97. package/dist/runner/bash/index.d.ts +4 -0
  98. package/dist/runner/bash/index.js +5 -0
  99. package/dist/runner/bash/runner.d.ts +55 -0
  100. package/dist/runner/bash/runner.js +213 -0
  101. package/dist/runner/claude/index.d.ts +4 -0
  102. package/dist/runner/claude/index.js +5 -0
  103. package/dist/runner/claude/runner.d.ts +29 -0
  104. package/dist/runner/claude/runner.js +83 -0
  105. package/dist/runner/codex/argv.d.ts +33 -0
  106. package/dist/runner/codex/argv.js +40 -0
  107. package/dist/runner/codex/index.d.ts +1 -0
  108. package/dist/runner/codex/index.js +2 -0
  109. package/dist/runner/codex/jsonl.d.ts +37 -0
  110. package/dist/runner/codex/jsonl.js +221 -0
  111. package/dist/runner/codex/runner.d.ts +65 -0
  112. package/dist/runner/codex/runner.js +124 -0
  113. package/dist/runner/common/exec-translator.d.ts +73 -0
  114. package/dist/runner/common/exec-translator.js +105 -0
  115. package/dist/runner/common/jsonl-stream.d.ts +39 -0
  116. package/dist/runner/common/jsonl-stream.js +203 -0
  117. package/dist/runner/common/process-stopper.d.ts +28 -0
  118. package/dist/runner/common/process-stopper.js +79 -0
  119. package/dist/runner/common/runner-utils.d.ts +21 -0
  120. package/dist/runner/common/runner-utils.js +65 -0
  121. package/dist/runner/common/spawn-heartbeat.d.ts +33 -0
  122. package/dist/runner/common/spawn-heartbeat.js +51 -0
  123. package/dist/runner/common/spawning-runner.d.ts +321 -0
  124. package/dist/runner/common/spawning-runner.js +634 -0
  125. package/dist/runner/common/usage.d.ts +31 -0
  126. package/dist/runner/common/usage.js +29 -0
  127. package/dist/runner/index.d.ts +19 -0
  128. package/dist/runner/index.js +24 -0
  129. package/dist/runner/kimi/index.d.ts +1 -0
  130. package/dist/runner/kimi/index.js +2 -0
  131. package/dist/runner/kimi/jsonl.d.ts +28 -0
  132. package/dist/runner/kimi/jsonl.js +143 -0
  133. package/dist/runner/kimi/runner.d.ts +50 -0
  134. package/dist/runner/kimi/runner.js +105 -0
  135. package/dist/runner/opencode/argv.d.ts +32 -0
  136. package/dist/runner/opencode/argv.js +35 -0
  137. package/dist/runner/opencode/index.d.ts +1 -0
  138. package/dist/runner/opencode/index.js +2 -0
  139. package/dist/runner/opencode/jsonl.d.ts +68 -0
  140. package/dist/runner/opencode/jsonl.js +295 -0
  141. package/dist/runner/opencode/runner.d.ts +63 -0
  142. package/dist/runner/opencode/runner.js +93 -0
  143. package/dist/runner/pi/index.d.ts +1 -0
  144. package/dist/runner/pi/index.js +2 -0
  145. package/dist/runner/pi/jsonl.d.ts +128 -0
  146. package/dist/runner/pi/jsonl.js +249 -0
  147. package/dist/runner/pi/runner.d.ts +37 -0
  148. package/dist/runner/pi/runner.js +76 -0
  149. package/dist/runner/registry.d.ts +54 -0
  150. package/dist/runner/registry.js +75 -0
  151. package/dist/runner/resolve-agent-choices.d.ts +7 -0
  152. package/dist/runner/resolve-agent-choices.js +76 -0
  153. package/dist/runner/sync-agent-choices.d.ts +6 -0
  154. package/dist/runner/sync-agent-choices.js +73 -0
  155. package/dist/runner/types.d.ts +250 -0
  156. package/dist/runner/types.js +8 -0
  157. package/dist/session/claude/index.d.ts +1 -0
  158. package/dist/session/claude/index.js +2 -0
  159. package/dist/session/claude/session-reader.d.ts +34 -0
  160. package/dist/session/claude/session-reader.js +46 -0
  161. package/dist/session/claude/sessions.d.ts +53 -0
  162. package/dist/session/claude/sessions.js +528 -0
  163. package/dist/session/codex/index.d.ts +1 -0
  164. package/dist/session/codex/index.js +2 -0
  165. package/dist/session/codex/rollout-reader.d.ts +88 -0
  166. package/dist/session/codex/rollout-reader.js +473 -0
  167. package/dist/session/codex/sessions.d.ts +21 -0
  168. package/dist/session/codex/sessions.js +40 -0
  169. package/dist/session/common/catch-up.d.ts +12 -0
  170. package/dist/session/common/catch-up.js +31 -0
  171. package/dist/session/common/constants.d.ts +11 -0
  172. package/dist/session/common/constants.js +12 -0
  173. package/dist/session/common/content-blocks.d.ts +37 -0
  174. package/dist/session/common/content-blocks.js +68 -0
  175. package/dist/session/common/jsonl.d.ts +77 -0
  176. package/dist/session/common/jsonl.js +351 -0
  177. package/dist/session/common/usage-accumulator.d.ts +58 -0
  178. package/dist/session/common/usage-accumulator.js +72 -0
  179. package/dist/session/index.d.ts +7 -0
  180. package/dist/session/index.js +11 -0
  181. package/dist/session/kimi/index.d.ts +1 -0
  182. package/dist/session/kimi/index.js +2 -0
  183. package/dist/session/kimi/sessions.d.ts +81 -0
  184. package/dist/session/kimi/sessions.js +527 -0
  185. package/dist/session/opencode/index.d.ts +1 -0
  186. package/dist/session/opencode/index.js +2 -0
  187. package/dist/session/opencode/sessions.d.ts +81 -0
  188. package/dist/session/opencode/sessions.js +388 -0
  189. package/dist/session/pi/index.d.ts +1 -0
  190. package/dist/session/pi/index.js +2 -0
  191. package/dist/session/pi/sessions.d.ts +27 -0
  192. package/dist/session/pi/sessions.js +399 -0
  193. package/dist/session/registry.d.ts +12 -0
  194. package/dist/session/registry.js +20 -0
  195. package/dist/session/session-store.d.ts +86 -0
  196. package/dist/session/session-store.js +450 -0
  197. package/dist/startup-contact.d.ts +18 -0
  198. package/dist/startup-contact.js +51 -0
  199. package/dist/workspace/index.d.ts +19 -0
  200. package/dist/workspace/index.js +53 -0
  201. 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