lark-remote 0.5.20 → 0.5.21

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 (55) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/bridge/inbound-media.js +71 -45
  3. package/dist/bridge/index.d.ts +9 -0
  4. package/dist/bridge/index.js +230 -203
  5. package/dist/card/approval-render.js +32 -33
  6. package/dist/card/card-budget.js +123 -98
  7. package/dist/card/run-renderer.js +82 -68
  8. package/dist/card/run-state.d.ts +0 -14
  9. package/dist/card/run-state.js +335 -207
  10. package/dist/card/tool-render.js +20 -16
  11. package/dist/card/usage-format.d.ts +12 -9
  12. package/dist/card/usage-format.js +111 -97
  13. package/dist/common/text-width.js +36 -24
  14. package/dist/common/tool-summary.js +132 -90
  15. package/dist/index.js +226 -190
  16. package/dist/message-handlers.js +253 -236
  17. package/dist/router/commands/ls.d.ts +3 -0
  18. package/dist/router/commands/ls.js +256 -175
  19. package/dist/router/commands/order.js +184 -159
  20. package/dist/router/commands/resume.js +189 -133
  21. package/dist/router/commands/ws.js +219 -174
  22. package/dist/router/index.js +192 -201
  23. package/dist/runner/bash/runner.js +2 -4
  24. package/dist/runner/codex/app-server/translator.d.ts +2 -0
  25. package/dist/runner/codex/app-server/translator.js +287 -221
  26. package/dist/runner/common/acp/base-translator.js +19 -17
  27. package/dist/runner/common/jsonl-stream.js +154 -187
  28. package/dist/runner/common/jsonrpc/client.d.ts +29 -0
  29. package/dist/runner/common/jsonrpc/client.js +43 -12
  30. package/dist/runner/common/spawning-runner.d.ts +5 -0
  31. package/dist/runner/common/spawning-runner.js +8 -3
  32. package/dist/runner/common/turn-started.d.ts +8 -0
  33. package/dist/runner/common/turn-started.js +11 -0
  34. package/dist/runner/dsh/approval.js +84 -51
  35. package/dist/runner/dsh/client.d.ts +7 -9
  36. package/dist/runner/dsh/client.js +356 -287
  37. package/dist/runner/dsh/mux-stream.js +28 -8
  38. package/dist/runner/dsh/runner.d.ts +4 -3
  39. package/dist/runner/dsh/runner.js +88 -64
  40. package/dist/runner/dsh/translator.d.ts +1 -31
  41. package/dist/runner/dsh/translator.js +180 -178
  42. package/dist/runner/kimi/acp/runner.js +81 -44
  43. package/dist/runner/opencode/acp/error-monitor.js +5 -5
  44. package/dist/runner/pi/rpc/client.js +4 -13
  45. package/dist/runner/pi/rpc/translator.js +61 -60
  46. package/dist/session/claude/sessions.js +113 -101
  47. package/dist/session/codex/rollout-reader.js +179 -180
  48. package/dist/session/dsh/sessions.js +281 -231
  49. package/dist/session/kimi/sessions.d.ts +0 -7
  50. package/dist/session/kimi/sessions.js +129 -109
  51. package/dist/session/opencode/sessions.d.ts +0 -11
  52. package/dist/session/opencode/sessions.js +161 -137
  53. package/dist/session/session-store.d.ts +8 -0
  54. package/dist/session/session-store.js +98 -98
  55. package/package.json +1 -1
@@ -1,5 +1,132 @@
1
1
  import { getLogger } from '../../logger/index.js';
2
2
  import { StringDecoder } from 'node:string_decoder';
3
+ function notify(state) {
4
+ if (state.resolveNext) {
5
+ state.resolveNext();
6
+ state.resolveNext = null;
7
+ }
8
+ }
9
+ /** Flush trailing partial line and mark the stream finished (end/close/error 共用). */
10
+ function flushAndFinish(state) {
11
+ // decoder.end() 吐出被 chunk 边界切开的最后一个多字节字符的残余字节
12
+ //(无残尾时返回空串):不加这句,末尾被切开的字符会凭空丢失。
13
+ const tail = state.decoder.end();
14
+ if (tail)
15
+ state.partialFragments.push(tail);
16
+ if (state.partialFragments.length > 0) {
17
+ const trimmed = state.partialFragments.join('').trim();
18
+ if (trimmed)
19
+ state.lineQueue.push(trimmed);
20
+ state.partialFragments.length = 0;
21
+ state.partialBytes = 0;
22
+ }
23
+ state.done = true;
24
+ notify(state);
25
+ }
26
+ /** 超长 partial line 当坏行经 onParseError 上报并清空,避免无换行流吃满内存。 */
27
+ function flushOversizePartial(state) {
28
+ if (state.partialFragments.length === 0)
29
+ return;
30
+ const line = state.partialFragments.join('');
31
+ state.partialFragments.length = 0;
32
+ state.partialBytes = 0;
33
+ const trimmed = line.trim();
34
+ if (!trimmed)
35
+ return;
36
+ if (state.options?.onParseError) {
37
+ state.options.onParseError(trimmed);
38
+ }
39
+ else {
40
+ getLogger().warn(`[jsonl-stream] dropped oversize partial line (${line.length} bytes)`);
41
+ }
42
+ }
43
+ function maybePause(state) {
44
+ if (state.pauseThreshold > 0 && !state.paused && state.lineQueue.length > state.pauseThreshold) {
45
+ state.paused = true;
46
+ state.stdout.pause();
47
+ }
48
+ }
49
+ function maybeResume(state) {
50
+ if (state.paused && state.lineQueue.length <= state.resumeThreshold) {
51
+ state.paused = false;
52
+ state.stdout.resume();
53
+ }
54
+ }
55
+ /** 把本 chunk 产出的完整行移入 lineQueue(跳过空白行)。 */
56
+ function enqueueCompleteLines(state) {
57
+ for (const line of state.completeLines) {
58
+ const trimmed = line.trim();
59
+ if (trimmed)
60
+ state.lineQueue.push(trimmed);
61
+ }
62
+ state.completeLines.length = 0;
63
+ }
64
+ /** 无换行 chunk:整块作为 partial fragment,超限则按坏行冲掉。 */
65
+ function handleChunkWithoutNewline(state, text) {
66
+ state.partialFragments.push(text);
67
+ state.partialBytes += text.length;
68
+ if (state.partialBytes > state.partialByteLimit)
69
+ flushOversizePartial(state);
70
+ }
71
+ /** 有换行 chunk:首段补完 pending 行、中段为完整行、末段为新的 partial tail。 */
72
+ function handleChunkWithNewline(state, segments) {
73
+ state.partialFragments.push(segments[0]);
74
+ state.partialBytes += segments[0].length;
75
+ state.completeLines.push(state.partialFragments.join(''));
76
+ state.partialFragments.length = 0;
77
+ state.partialBytes = 0;
78
+ for (let i = 1; i < segments.length - 1; i++) {
79
+ state.completeLines.push(segments[i]);
80
+ }
81
+ state.partialFragments.push(segments[segments.length - 1]);
82
+ state.partialBytes = segments[segments.length - 1].length;
83
+ // 单个 >10MB 且内部无换行的 chunk,其尾部 partial 自身也可能超限。
84
+ if (state.partialBytes > state.partialByteLimit)
85
+ flushOversizePartial(state);
86
+ }
87
+ function onDataChunk(state, chunk) {
88
+ // StringDecoder 按字节边界解码:多字节字符被 chunk 边界切开时裸 toString
89
+ // 会产出不可逆的 U+FFFD(同族修复见 jsonrpc/transport.ts)。
90
+ const text = state.decoder.write(chunk);
91
+ const segments = text.split('\n');
92
+ if (segments.length === 1) {
93
+ handleChunkWithoutNewline(state, segments[0]);
94
+ }
95
+ else {
96
+ handleChunkWithNewline(state, segments);
97
+ }
98
+ enqueueCompleteLines(state);
99
+ notify(state);
100
+ maybePause(state);
101
+ }
102
+ function onStdoutError(state, err) {
103
+ getLogger().warn(`[jsonl-stream] stdout error: ${err.message}`);
104
+ flushAndFinish(state);
105
+ }
106
+ async function* jsonlGenerator(state) {
107
+ while (true) {
108
+ while (state.lineQueue.length > 0) {
109
+ const line = state.lineQueue.shift();
110
+ try {
111
+ yield JSON.parse(line);
112
+ }
113
+ catch {
114
+ if (state.options?.onParseError) {
115
+ state.options.onParseError(line);
116
+ }
117
+ else {
118
+ getLogger().warn(`[jsonl-stream] failed to parse JSONL: ${line.slice(0, 100)}`);
119
+ }
120
+ }
121
+ maybeResume(state);
122
+ }
123
+ if (state.done)
124
+ return;
125
+ await new Promise((r) => {
126
+ state.resolveNext = r;
127
+ });
128
+ }
129
+ }
3
130
  /**
4
131
  * Parse JSONL (newline-delimited JSON) from a readable stream.
5
132
  *
@@ -22,193 +149,33 @@ import { StringDecoder } from 'node:string_decoder';
22
149
  * (统一方案:所有 runner 自己生成 timestamp).
23
150
  */
24
151
  export function createJSONLStream(stdout, options) {
25
- const lineQueue = [];
26
- // accumulate partial-line fragments in an array instead of repeatedly
27
- // string-concatenating a growing `partialLine`. A huge single JSON line
28
- // (large tool_result / text_end) spanning many chunks caused O(n²) copies:
29
- // each chunk rebuilt `partialLine + text` over the whole accumulated prefix.
30
- // With a fragment array, we join only once per chunk (at the line boundary)
31
- // and push complete lines; the residual tail stays as array fragments until
32
- // the next newline or stream end. Net allocation: O(n) instead of O(n²).
33
- const partialFragments = [];
34
- // 跨 chunk 多字节字符的边界安全解码器(见 stdout 'data' 处理)。
35
- const decoder = new StringDecoder('utf8');
36
- // accumulated byte length of partialFragments. Backpressure (above)
37
- // only counts COMPLETE lines in lineQueue; a never-newline stream (malformed
38
- // output, binary garbage) accumulates fragments without ever completing a
39
- // line, so pause never fires and memory grows unbounded. Cap the partial
40
- // line at 10MB: once exceeded, flush it as a bad line via onParseError and
41
- // reset the buffer, bounding peak memory for a single pathological line.
42
- const PARTIAL_BYTE_LIMIT = 10 * 1024 * 1024;
43
- let partialBytes = 0;
44
- const completeLines = [];
45
- let done = false;
46
- let resolveNext = null;
47
- // backpressure thresholds. Default matches production (100).
48
152
  const pauseThreshold = options?.pauseThreshold ?? 100;
49
- // Default low-water mark is half the high-water mark for meaningful hysteresis
50
- // (avoids per-line pause/resume thrashing when callers don't set it explicitly).
51
- let resumeThreshold = options?.resumeThreshold ?? Math.max(0, Math.floor(pauseThreshold / 2));
52
- // Clamp misconfiguration: resumeThreshold > pauseThreshold would resume at queue
53
- // high water then immediately re-pause on the next line → thrash (nit).
54
- if (resumeThreshold > pauseThreshold)
55
- resumeThreshold = pauseThreshold;
56
- let paused = false;
57
- function notify() {
58
- if (resolveNext) {
59
- resolveNext();
60
- resolveNext = null;
61
- }
62
- }
63
- // Flush any trailing partial line and mark the stream finished. Shared by the
64
- // 'end'/'close'/'error' handlers so flush logic lives in one place (nit).
65
- function flushAndFinish() {
66
- // decoder.end() 吐出被 chunk 边界切开的最后一个多字节字符的残余字节
67
- //(无残尾时返回空串):不加这句,末尾被切开的字符会凭空丢失。
68
- const tail = decoder.end();
69
- if (tail)
70
- partialFragments.push(tail);
71
- if (partialFragments.length > 0) {
72
- const trimmed = partialFragments.join('').trim();
73
- if (trimmed)
74
- lineQueue.push(trimmed);
75
- partialFragments.length = 0;
76
- partialBytes = 0;
77
- }
78
- done = true;
79
- notify();
80
- }
81
- // flush an oversize partial line as a bad line. When partialFragments
82
- // exceeds the byte cap, the partial line is treated as malformed (a single
83
- // JSON line should never reach 10MB) and reported via onParseError rather
84
- // than accumulated indefinitely. Resets the buffer so a pathological stream
85
- // cannot leak memory line after line.
86
- function flushOversizePartial() {
87
- if (partialFragments.length === 0)
88
- return;
89
- const line = partialFragments.join('');
90
- partialFragments.length = 0;
91
- partialBytes = 0;
92
- const trimmed = line.trim();
93
- if (!trimmed)
94
- return;
95
- if (options?.onParseError) {
96
- options.onParseError(trimmed);
97
- }
98
- else {
99
- getLogger().warn(`[jsonl-stream] dropped oversize partial line (${line.length} bytes)`);
100
- }
101
- }
102
- // Apply backpressure after pushing new lines: pause stdout when the queue
103
- // exceeds the high-water mark so the producer stops feeding until the
104
- // consumer drains it.bounds memory in long runs.
105
- function maybePause() {
106
- if (pauseThreshold > 0 && !paused && lineQueue.length > pauseThreshold) {
107
- paused = true;
108
- stdout.pause();
109
- }
110
- }
111
- // Release backpressure after consuming a line: resume stdout once the queue
112
- // drains to the low-water mark so the producer can feed again.
113
- function maybeResume() {
114
- if (paused && lineQueue.length <= resumeThreshold) {
115
- paused = false;
116
- stdout.resume();
117
- }
118
- }
119
- stdout.on('data', (chunk) => {
120
- // StringDecoder 按字节边界解码:多字节字符(中文/emoji)被 chunk 边界
121
- // 切开时,裸 toString 会产出不可逆的 U+FFFD(同族修复见
122
- // jsonrpc/transport.ts,claude 的中文输出走本流)。
123
- const text = decoder.write(chunk);
124
- // split on newlines. The first segment continues the pending partial
125
- // line (joined with accumulated fragments — one join per chunk, not per
126
- // byte); subsequent segments are complete lines. The last segment is the
127
- // new partial tail (kept as a single fragment).
128
- const segments = text.split('\n');
129
- if (segments.length === 1) {
130
- // No newline in this chunk: the whole chunk is a partial fragment.
131
- partialFragments.push(segments[0]);
132
- partialBytes += segments[0].length;
133
- // cap the partial line. A never-newline stream would otherwise
134
- // accumulate fragments without bound (backpressure only counts complete
135
- // lines). Flush as a bad line once the byte cap is exceeded.
136
- if (partialBytes > PARTIAL_BYTE_LIMIT) {
137
- flushOversizePartial();
138
- }
139
- }
140
- else {
141
- // First segment completes the pending line.
142
- partialFragments.push(segments[0]);
143
- partialBytes += segments[0].length;
144
- completeLines.push(partialFragments.join(''));
145
- partialFragments.length = 0;
146
- partialBytes = 0;
147
- // Middle segments are each complete lines.
148
- for (let i = 1; i < segments.length - 1; i++) {
149
- completeLines.push(segments[i]);
150
- }
151
- // Last segment is the new partial tail.
152
- partialFragments.push(segments[segments.length - 1]);
153
- partialBytes = segments[segments.length - 1].length;
154
- // the trailing partial tail can itself exceed the cap if a single
155
- // chunk is larger than 10MB with no internal newline.
156
- if (partialBytes > PARTIAL_BYTE_LIMIT) {
157
- flushOversizePartial();
158
- }
159
- }
160
- // Push complete lines into the queue (skipping blank/whitespace-only).
161
- for (const line of completeLines) {
162
- const trimmed = line.trim();
163
- if (trimmed)
164
- lineQueue.push(trimmed);
165
- }
166
- completeLines.length = 0;
167
- notify();
168
- maybePause();
169
- });
170
- stdout.on('end', () => {
171
- flushAndFinish();
172
- });
173
- stdout.on('close', () => {
174
- flushAndFinish();
175
- });
176
- // handle stdout 'error' so the generator terminates instead of
177
- // hanging forever on the `await new Promise` below. Without this, a stdout
178
- // error (e.g. broken pipe, runner crash) leaves the consumer awaiting a
179
- // notify that never comes → bridge serial queue blocks permanently. We do
180
- // not re-throw the error through the generator (the runner's `try/catch`
181
- // around `for await` and the proc 'close' path already surface failures via
182
- // the unified result event); we only guarantee the generator ends. The error
183
- // is logged for diagnosis.
184
- stdout.on('error', (err) => {
185
- getLogger().warn(`[jsonl-stream] stdout error: ${err.message}`);
186
- flushAndFinish();
187
- });
188
- async function* generator() {
189
- while (true) {
190
- while (lineQueue.length > 0) {
191
- const line = lineQueue.shift();
192
- try {
193
- yield JSON.parse(line);
194
- }
195
- catch {
196
- if (options?.onParseError) {
197
- options.onParseError(line);
198
- }
199
- else {
200
- getLogger().warn(`[jsonl-stream] failed to parse JSONL: ${line.slice(0, 100)}`);
201
- }
202
- }
203
- maybeResume();
204
- }
205
- if (done)
206
- return;
207
- await new Promise((r) => {
208
- resolveNext = r;
209
- });
210
- }
211
- }
212
- return generator();
153
+ const state = {
154
+ lineQueue: [],
155
+ partialFragments: [],
156
+ decoder: new StringDecoder('utf8'),
157
+ completeLines: [],
158
+ done: false,
159
+ resolveNext: null,
160
+ partialBytes: 0,
161
+ pauseThreshold,
162
+ // 默认低水位为高水位一半,避免逐行 pause/resume 抖动(调用方未显式设置时)。
163
+ resumeThreshold: options?.resumeThreshold ?? Math.max(0, Math.floor(pauseThreshold / 2)),
164
+ paused: false,
165
+ // 单条 never-newline 流的 fragment 上限(10MB),防内存无界增长。
166
+ partialByteLimit: 10 * 1024 * 1024,
167
+ stdout,
168
+ options,
169
+ };
170
+ // 误配(resumeThreshold > pauseThreshold)会在高水位立刻 re-pause,钳到高水位。
171
+ if (state.resumeThreshold > state.pauseThreshold)
172
+ state.resumeThreshold = state.pauseThreshold;
173
+ stdout.on('data', (chunk) => onDataChunk(state, chunk));
174
+ stdout.on('end', () => flushAndFinish(state));
175
+ stdout.on('close', () => flushAndFinish(state));
176
+ // stdout error 只保证 generator 结束并记日志,不向 generator 重抛(runner
177
+ // 的 try/catch 与 proc close 路径已统一上报失败);否则 consumer 会永久挂住。
178
+ stdout.on('error', (err) => onStdoutError(state, err));
179
+ return jsonlGenerator(state);
213
180
  }
214
181
  //# sourceMappingURL=jsonl-stream.js.map
@@ -19,6 +19,35 @@ export declare class RpcTimeoutError extends Error {
19
19
  export declare class ConnectionLostError extends Error {
20
20
  constructor(message?: string);
21
21
  }
22
+ /** Minimal shape of a pending request entry shared by JSON-line RPC clients. */
23
+ export interface RpcPendingEntry {
24
+ timer: ReturnType<typeof setTimeout>;
25
+ reject: (err: Error) => void;
26
+ }
27
+ /** Reject + clear all in-flight requests (dispose / transport-close path). */
28
+ export declare function rejectAllPending<K, V extends RpcPendingEntry>(pending: Map<K, V>, err: Error): void;
29
+ /** Per-client inputs for the shared dispose sequence. */
30
+ export interface RpcDisposeContext<K, V extends RpcPendingEntry> {
31
+ alreadyDisposed: boolean;
32
+ markDisposed(): void;
33
+ pending: Map<K, V>;
34
+ close(): Promise<void>;
35
+ disposedMessage: string;
36
+ }
37
+ /**
38
+ * Shared dispose sequence: no-op when already disposed, else mark
39
+ * disposed + not-ready, reject pending, and close the transport. The pi RPC
40
+ * client reuses this; only the disposed error message differs per client.
41
+ */
42
+ export declare function disposeRpcClient<K, V extends RpcPendingEntry>(ctx: RpcDisposeContext<K, V>): Promise<void>;
43
+ /**
44
+ * Dispose a client instance from its shared lifecycle shape. JsonRpcClient and
45
+ * PiRpcClient both delegate here so the dispose sequence has a single
46
+ * implementation; only the pending map and the disposed message differ.
47
+ * `client` is typed as object because the two classes keep `_disposed` /
48
+ * `_ready` private — the shape is asserted locally.
49
+ */
50
+ export declare function disposeRpcClientInstance<K, V extends RpcPendingEntry>(client: object, pending: Map<K, V>, close: () => Promise<void>, disposedMessage?: string): Promise<void>;
22
51
  export interface ClientHooks {
23
52
  onNotification(method: string, params: unknown): void;
24
53
  onServerRequest(id: number | string, method: string, params: unknown): void;
@@ -33,6 +33,46 @@ export class ConnectionLostError extends Error {
33
33
  this.name = 'ConnectionLostError';
34
34
  }
35
35
  }
36
+ /** Reject + clear all in-flight requests (dispose / transport-close path). */
37
+ export function rejectAllPending(pending, err) {
38
+ for (const entry of pending.values()) {
39
+ clearTimeout(entry.timer);
40
+ entry.reject(err);
41
+ }
42
+ pending.clear();
43
+ }
44
+ /**
45
+ * Shared dispose sequence: no-op when already disposed, else mark
46
+ * disposed + not-ready, reject pending, and close the transport. The pi RPC
47
+ * client reuses this; only the disposed error message differs per client.
48
+ */
49
+ export async function disposeRpcClient(ctx) {
50
+ if (ctx.alreadyDisposed)
51
+ return;
52
+ ctx.markDisposed();
53
+ rejectAllPending(ctx.pending, new ConnectionLostError(ctx.disposedMessage));
54
+ await ctx.close();
55
+ }
56
+ /**
57
+ * Dispose a client instance from its shared lifecycle shape. JsonRpcClient and
58
+ * PiRpcClient both delegate here so the dispose sequence has a single
59
+ * implementation; only the pending map and the disposed message differ.
60
+ * `client` is typed as object because the two classes keep `_disposed` /
61
+ * `_ready` private — the shape is asserted locally.
62
+ */
63
+ export function disposeRpcClientInstance(client, pending, close, disposedMessage = 'client disposed') {
64
+ const state = client;
65
+ return disposeRpcClient({
66
+ alreadyDisposed: state._disposed,
67
+ markDisposed: () => {
68
+ state._disposed = true;
69
+ state._ready = false;
70
+ },
71
+ pending,
72
+ close,
73
+ disposedMessage,
74
+ });
75
+ }
36
76
  // =============================================================================
37
77
  // Client
38
78
  // =============================================================================
@@ -184,13 +224,8 @@ export class JsonRpcClient {
184
224
  /**
185
225
  * Dispose the client: cancel pending requests and close transport.
186
226
  */
187
- async dispose() {
188
- if (this._disposed)
189
- return;
190
- this._disposed = true;
191
- this._ready = false;
192
- this.failPending(new ConnectionLostError('client disposed'));
193
- await this.transport.close();
227
+ dispose() {
228
+ return disposeRpcClientInstance(this, this.pendingRequests, () => this.transport.close());
194
229
  }
195
230
  // =========================================================================
196
231
  // Internal
@@ -238,11 +273,7 @@ export class JsonRpcClient {
238
273
  getLogger().warn(`[jsonrpc-client] unknown message type: ${JSON.stringify(message).slice(0, 200)}`);
239
274
  }
240
275
  failPending(err) {
241
- for (const [_id, pending] of this.pendingRequests) {
242
- clearTimeout(pending.timer);
243
- pending.reject(err);
244
- }
245
- this.pendingRequests.clear();
276
+ rejectAllPending(this.pendingRequests, err);
246
277
  }
247
278
  }
248
279
  //# sourceMappingURL=client.js.map
@@ -27,6 +27,11 @@ interface ExitCleanupHandler {
27
27
  }
28
28
  /** Register a process-exit cleanup handler (installs listeners at most once). */
29
29
  export declare function registerExitCleanup(handler: ExitCleanupHandler): void;
30
+ /**
31
+ * 子进程存活性判定(SpawningRunner 与 BashProcessRunner 的 `isRunning` 共用)。
32
+ * 两边曾是逐字相同的一段表达式,抽到共同依赖模块避免漂移。
33
+ */
34
+ export declare function isChildProcessRunning(proc: ChildProcess | null): boolean;
30
35
  /**
31
36
  * pid 文件名的 workspace 后缀:`-<消毒 cwd>-<cwd hash>`。
32
37
  *
@@ -54,6 +54,13 @@ export function registerExitCleanup(handler) {
54
54
  installExitListenersOnce();
55
55
  registeredRunners.add(handler);
56
56
  }
57
+ /**
58
+ * 子进程存活性判定(SpawningRunner 与 BashProcessRunner 的 `isRunning` 共用)。
59
+ * 两边曾是逐字相同的一段表达式,抽到共同依赖模块避免漂移。
60
+ */
61
+ export function isChildProcessRunning(proc) {
62
+ return proc !== null && proc.exitCode === null && proc.signalCode === null;
63
+ }
57
64
  /**
58
65
  * FNV-1a 32 位哈希(hex,平台无关确定性):pid 文件后缀的消歧段。
59
66
  */
@@ -154,9 +161,7 @@ export class SpawningRunner {
154
161
  this.spawnHeartbeat = new SpawnHeartbeat(opts.spawnHeartbeatMs ?? 30_000, this.logTag);
155
162
  }
156
163
  get isRunning() {
157
- return (this.currentProcess !== null &&
158
- this.currentProcess.exitCode === null &&
159
- this.currentProcess.signalCode === null);
164
+ return isChildProcessRunning(this.currentProcess);
160
165
  }
161
166
  // --- Hooks (subclasses override as needed) ---
162
167
  /**
@@ -1,2 +1,10 @@
1
1
  import type { TurnStartedEvent } from '../types.js';
2
2
  export declare function makeTurnStartedEvent(sessionId: string, turnId: string, operationKind: 'turn' | 'compact'): TurnStartedEvent;
3
+ /**
4
+ * 自产 turn_started 事件的完整序列:先让调用方记录 per-turn 状态,再构造事件。
5
+ *
6
+ * ACP(`BaseAcpTranslator`)与 pi(`PiRpcTranslator`)的 `produceTurnStarted`
7
+ * 曾是逐字同形的两段;唯一差异是记录的字段(currentTurnId / sessionId),
8
+ * 由 `recordState` 注入,共同序列收编在此,避免两处方法体漂移。
9
+ */
10
+ export declare function produceTurnStartedEvent(sessionId: string, turnId: string, operationKind: 'turn' | 'compact', recordState: (sessionId: string, turnId: string) => void): TurnStartedEvent;
@@ -7,4 +7,15 @@ export function makeTurnStartedEvent(sessionId, turnId, operationKind) {
7
7
  timestamp: new Date().toISOString(),
8
8
  };
9
9
  }
10
+ /**
11
+ * 自产 turn_started 事件的完整序列:先让调用方记录 per-turn 状态,再构造事件。
12
+ *
13
+ * ACP(`BaseAcpTranslator`)与 pi(`PiRpcTranslator`)的 `produceTurnStarted`
14
+ * 曾是逐字同形的两段;唯一差异是记录的字段(currentTurnId / sessionId),
15
+ * 由 `recordState` 注入,共同序列收编在此,避免两处方法体漂移。
16
+ */
17
+ export function produceTurnStartedEvent(sessionId, turnId, operationKind, recordState) {
18
+ recordState(sessionId, turnId);
19
+ return makeTurnStartedEvent(sessionId, turnId, operationKind);
20
+ }
10
21
  //# sourceMappingURL=turn-started.js.map