lark-remote 0.5.19 → 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 (136) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/bridge/approval-actions.d.ts +1 -1
  3. package/dist/bridge/approval-actions.js +1 -1
  4. package/dist/bridge/bash-run.d.ts +71 -0
  5. package/dist/bridge/bash-run.js +217 -0
  6. package/dist/bridge/compact.d.ts +155 -0
  7. package/dist/bridge/compact.js +450 -0
  8. package/dist/bridge/final-usage.js +4 -4
  9. package/dist/bridge/inbound-media.js +72 -46
  10. package/dist/bridge/index.d.ts +106 -153
  11. package/dist/bridge/index.js +400 -1000
  12. package/dist/bridge/job-watch.d.ts +68 -0
  13. package/dist/bridge/job-watch.js +146 -0
  14. package/dist/bridge/queue-manager.d.ts +3 -3
  15. package/dist/bridge/queue-manager.js +10 -29
  16. package/dist/card/approval-render.d.ts +2 -2
  17. package/dist/card/approval-render.js +141 -130
  18. package/dist/card/card-budget.js +123 -98
  19. package/dist/card/card-helpers.d.ts +3 -3
  20. package/dist/card/card-helpers.js +6 -6
  21. package/dist/card/final-answer-file.js +1 -1
  22. package/dist/card/run-renderer.d.ts +3 -3
  23. package/dist/card/run-renderer.js +114 -159
  24. package/dist/card/run-state.d.ts +1 -15
  25. package/dist/card/run-state.js +358 -292
  26. package/dist/card/tool-render.d.ts +1 -1
  27. package/dist/card/tool-render.js +21 -17
  28. package/dist/card/usage-format.d.ts +22 -7
  29. package/dist/card/usage-format.js +119 -100
  30. package/dist/cli.js +15 -3
  31. package/dist/clone.js +2 -2
  32. package/dist/common/text-width.js +36 -24
  33. package/dist/common/tool-summary.js +132 -90
  34. package/dist/config/codex-config.js +130 -106
  35. package/dist/config/index.d.ts +16 -2
  36. package/dist/config/index.js +23 -8
  37. package/dist/connector/index.js +6 -6
  38. package/dist/error-classification.js +3 -2
  39. package/dist/inbound/placeholder.d.ts +1 -1
  40. package/dist/inbound/placeholder.js +1 -1
  41. package/dist/index.js +387 -281
  42. package/dist/logger/index.js +6 -6
  43. package/dist/message-handlers.d.ts +0 -5
  44. package/dist/message-handlers.js +254 -237
  45. package/dist/model-catalog.d.ts +2 -2
  46. package/dist/model-catalog.js +3 -8
  47. package/dist/platform/identity.js +1 -1
  48. package/dist/platform/path.d.ts +1 -1
  49. package/dist/platform/path.js +1 -1
  50. package/dist/platform/sleep-blocker.js +1 -1
  51. package/dist/platform/terminator-win32.js +2 -1
  52. package/dist/router/commands/ls.d.ts +3 -0
  53. package/dist/router/commands/ls.js +256 -175
  54. package/dist/router/commands/order.js +184 -159
  55. package/dist/router/commands/resume.js +190 -152
  56. package/dist/router/commands/ws.js +219 -174
  57. package/dist/router/config/dsh.d.ts +4 -8
  58. package/dist/router/config/dsh.js +3 -10
  59. package/dist/router/config/kimi.js +1 -1
  60. package/dist/router/config/types.d.ts +1 -1
  61. package/dist/router/config-diff.js +1 -1
  62. package/dist/router/index.d.ts +56 -11
  63. package/dist/router/index.js +401 -391
  64. package/dist/runner/bash/runner.js +2 -4
  65. package/dist/runner/claude/session.d.ts +1 -1
  66. package/dist/runner/claude/session.js +6 -4
  67. package/dist/runner/codex/app-server/runner.d.ts +9 -2
  68. package/dist/runner/codex/app-server/runner.js +30 -25
  69. package/dist/runner/codex/app-server/translator.d.ts +2 -5
  70. package/dist/runner/codex/app-server/translator.js +291 -238
  71. package/dist/runner/common/acp/acp-approval.d.ts +1 -1
  72. package/dist/runner/common/acp/acp-approval.js +1 -1
  73. package/dist/runner/common/acp/base-acp-runner.d.ts +5 -2
  74. package/dist/runner/common/acp/base-acp-runner.js +26 -65
  75. package/dist/runner/common/acp/base-translator.d.ts +12 -2
  76. package/dist/runner/common/acp/base-translator.js +50 -26
  77. package/dist/runner/common/acp/protocol-types.d.ts +1 -1
  78. package/dist/runner/common/acp/protocol-types.js +2 -2
  79. package/dist/runner/common/connection-based-runner.d.ts +32 -1
  80. package/dist/runner/common/connection-based-runner.js +32 -2
  81. package/dist/runner/common/jsonl-stream.js +154 -187
  82. package/dist/runner/common/jsonrpc/client.d.ts +29 -0
  83. package/dist/runner/common/jsonrpc/client.js +43 -12
  84. package/dist/runner/common/jsonrpc/connection-manager.d.ts +2 -2
  85. package/dist/runner/common/jsonrpc/connection-manager.js +4 -4
  86. package/dist/runner/common/spawning-runner.d.ts +6 -1
  87. package/dist/runner/common/spawning-runner.js +9 -4
  88. package/dist/runner/common/turn-started.d.ts +8 -0
  89. package/dist/runner/common/turn-started.js +11 -0
  90. package/dist/runner/dsh/approval.js +84 -51
  91. package/dist/runner/dsh/catalog-probe.d.ts +17 -0
  92. package/dist/runner/dsh/catalog-probe.js +10 -0
  93. package/dist/runner/dsh/client.d.ts +7 -11
  94. package/dist/runner/dsh/client.js +356 -289
  95. package/dist/runner/dsh/exit-cancel.d.ts +2 -2
  96. package/dist/runner/dsh/exit-cancel.js +2 -2
  97. package/dist/runner/dsh/inflight.d.ts +1 -1
  98. package/dist/runner/dsh/inflight.js +1 -1
  99. package/dist/runner/dsh/mux-stream.js +28 -8
  100. package/dist/runner/dsh/runner.d.ts +44 -4
  101. package/dist/runner/dsh/runner.js +296 -199
  102. package/dist/runner/dsh/translator.d.ts +1 -31
  103. package/dist/runner/dsh/translator.js +180 -178
  104. package/dist/runner/index.d.ts +1 -1
  105. package/dist/runner/kimi/acp/runner.d.ts +1 -2
  106. package/dist/runner/kimi/acp/runner.js +82 -46
  107. package/dist/runner/kimi/acp/translator.d.ts +1 -1
  108. package/dist/runner/kimi/acp/translator.js +3 -4
  109. package/dist/runner/opencode/acp/error-monitor.js +5 -5
  110. package/dist/runner/opencode/acp/runner.d.ts +2 -2
  111. package/dist/runner/opencode/acp/runner.js +4 -4
  112. package/dist/runner/pi/rpc/client.js +4 -13
  113. package/dist/runner/pi/rpc/protocol-types.d.ts +1 -1
  114. package/dist/runner/pi/rpc/protocol-types.js +1 -1
  115. package/dist/runner/pi/rpc/runner.d.ts +3 -0
  116. package/dist/runner/pi/rpc/runner.js +27 -42
  117. package/dist/runner/pi/rpc/translator.js +61 -60
  118. package/dist/runner/types.d.ts +2 -2
  119. package/dist/runner/types.js +1 -1
  120. package/dist/session/claude/sessions.d.ts +45 -0
  121. package/dist/session/claude/sessions.js +160 -122
  122. package/dist/session/codex/rollout-reader.js +181 -182
  123. package/dist/session/common/throw-session-list-error.d.ts +10 -0
  124. package/dist/session/common/throw-session-list-error.js +16 -0
  125. package/dist/session/dsh/sessions.js +291 -235
  126. package/dist/session/kimi/sessions.d.ts +40 -8
  127. package/dist/session/kimi/sessions.js +373 -301
  128. package/dist/session/opencode/sessions.d.ts +142 -1
  129. package/dist/session/opencode/sessions.js +378 -321
  130. package/dist/session/pi/sessions.js +4 -5
  131. package/dist/session/session-store.d.ts +9 -1
  132. package/dist/session/session-store.js +99 -99
  133. package/dist/update/version-check.js +2 -1
  134. package/dist/workspace/index.d.ts +1 -1
  135. package/dist/workspace/index.js +1 -1
  136. 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
@@ -151,8 +151,8 @@ export declare class ConnectionManager<TClient extends ConnectionClient = JsonRp
151
151
  * 定时器回调带 **slot 身份校验**:武装之后 slot 可能已被替换——进程自退走
152
152
  * baseOnClose(删 slot 但不经过 disposeSlot)、或同 cwd 已 acquire 了新连接。
153
153
  * 这两种情况下本定时器都已过期,不得回收「当时恰好挂在该 cwd 上」的连接:
154
- * 2026-10-24 线上事故即一条泄漏的过期 TTL 杀掉继任连接的在途 turn,卡片
155
- * 误报 "Codex app-server connection closed"。
154
+ * 一条泄漏的过期 TTL 会杀掉继任连接的在途 turn,卡片误报
155
+ * "Codex app-server connection closed"。
156
156
  */
157
157
  notifyIdle(cwd: string): void;
158
158
  private createClient;
@@ -197,8 +197,8 @@ export class ConnectionManager {
197
197
  * 定时器回调带 **slot 身份校验**:武装之后 slot 可能已被替换——进程自退走
198
198
  * baseOnClose(删 slot 但不经过 disposeSlot)、或同 cwd 已 acquire 了新连接。
199
199
  * 这两种情况下本定时器都已过期,不得回收「当时恰好挂在该 cwd 上」的连接:
200
- * 2026-10-24 线上事故即一条泄漏的过期 TTL 杀掉继任连接的在途 turn,卡片
201
- * 误报 "Codex app-server connection closed"。
200
+ * 一条泄漏的过期 TTL 会杀掉继任连接的在途 turn,卡片误报
201
+ * "Codex app-server connection closed"。
202
202
  */
203
203
  notifyIdle(cwd) {
204
204
  if (this.idleTtlMs <= 0)
@@ -245,8 +245,8 @@ export class ConnectionManager {
245
245
  if (current?.client === client) {
246
246
  // 进程自退(codex app-server 偶发 exit 0,非本程序发起,不经
247
247
  // disposeSlot)也要撤掉挂在旧 slot 上的 idle TTL:定时器不随 slot
248
- // 删除而取消,到期时会按 cwd 误杀继任连接的在途 turn(2026-10-24
249
- // 线上事故,见 notifyIdle 的身份校验与对应回归测试)。
248
+ // 删除而取消,到期时会按 cwd 误杀继任连接的在途 turn(见 notifyIdle
249
+ // 的身份校验与对应回归测试)。
250
250
  this.clearIdleTimer(current);
251
251
  this.slots.delete(cwd);
252
252
  this.onConnectionLost?.(cwd);
@@ -4,7 +4,7 @@ import { type Terminator } from '../../platform/terminator.js';
4
4
  import { SpawnHeartbeat } from './spawn-heartbeat.js';
5
5
  import type { SpawnOptions } from '../types.js';
6
6
  /**
7
- * 进程级退出监听单例分发(修复,2026-08-02)。
7
+ * 进程级退出监听单例分发。
8
8
  *
9
9
  * 背景:各 agent runner 的 registerExitHandlers 曾是同构复制,每个实例
10
10
  * process.on('exit'|'SIGINT'|'SIGTERM') 注册 3 个永不移除的闭包(捕获整个
@@ -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
  /**
@@ -357,7 +362,7 @@ export class SpawningRunner {
357
362
  // matchMode 'agent-invocation'(不是默认的 'executable'):agent CLI 普遍是
358
363
  // `#!/usr/bin/env node` 脚本,ps 看到的是 `node <安装路径>/…/cli.js`,名字只在
359
364
  // 路径段里(pi → @earendil-works/pi-coding-agent/…)。用默认档会恒判 mismatch
360
- // → 这些 agent 的孤儿永远回收不掉(2026-09-20 实测安装形态)。
365
+ // → 这些 agent 的孤儿永远回收不掉。
361
366
  const verdict = verifyPidIdentityVerdictSync(pid, {
362
367
  expectedBinary,
363
368
  matchMode: 'agent-invocation',
@@ -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