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
@@ -23,191 +23,193 @@
23
23
  * → ResultEvent.usage: input_tokens/output_tokens/cache_read_tokens/
24
24
  * cache_creation_tokens (cacheWrite = cache creation) + total_tokens sum.
25
25
  */
26
- export class DshTranslator {
27
- /** 本 turn 累计用量:DSH 每次模型调用(step)一条 usage,工具往返 = 多条。 */
28
- turnUsage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
29
- /** 末次采样的 usage:上下文水位,或只有 live chunk 时的兜底值。 */
30
- lastUsage;
31
- /** 是否收到过 durable assistant/message.usage(唯一累加来源)。 */
32
- hasMessageUsage = false;
33
- /** 是否只收到过 live usage chunk(无 durable message usage 时的兜底)。 */
34
- hasStreamUsage = false;
35
- /** 当前 attempt 的 id(start 帧携带,chunk 帧也带;断开重连重放时不变)。 */
36
- attemptId = '0';
37
- /**
38
- * 本 turn 末条 durable assistant 消息的正文(**完整**、未截断)。
39
- *
40
- * live chunk 是逐 delta 拼接、会被状态层的 MAX_TEXT_CHARS 窗口截断;
41
- * durable `assistant/message` 一次就到齐全文。收 turn 时把它挂在 result 上,
42
- * 卡片才能渲染完整回答。
43
- */
44
- lastMessageText;
45
- /** Translate one live assistant-stream chunk into AgentEvents. */
46
- streamFrameToAgentEvents(frame, _sessionId) {
47
- if (frame.type === 'start') {
48
- if (frame.attemptId)
49
- this.attemptId = frame.attemptId;
50
- return [];
51
- }
52
- if (frame.type !== 'chunk' || !frame.chunk)
53
- return [];
54
- return this.chunkToAgentEvents(frame.chunk, frame.time ?? Date.now(), frame.attemptId ?? this.attemptId);
55
- }
56
- /**
57
- * Record durable per-step usage — the accumulation source. assistant/message
58
- * carries exactly one usage per model call and is seq-deduped by the client,
59
- * so summing it gives the turn total without double counting.
60
- */
61
- recordMessageUsage(usage) {
62
- this.turnUsage.input += usage.inputTokens ?? 0;
63
- this.turnUsage.output += usage.outputTokens ?? 0;
64
- this.turnUsage.cacheRead += usage.cacheReadTokens ?? 0;
65
- this.turnUsage.cacheWrite += usage.cacheWriteTokens ?? 0;
66
- this.lastUsage = usage;
67
- this.hasMessageUsage = true;
26
+ function newTurnState() {
27
+ return {
28
+ turnUsage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
29
+ hasMessageUsage: false,
30
+ hasStreamUsage: false,
31
+ attemptId: '0',
32
+ };
33
+ }
34
+ // --- 模块级状态操作原语 ---
35
+ /**
36
+ * Record durable per-step usage — the accumulation source. assistant/message
37
+ * carries exactly one usage per model call and is seq-deduped by the client,
38
+ * so summing it gives the turn total without double counting.
39
+ */
40
+ function recordMessageUsage(state, usage) {
41
+ state.turnUsage.input += usage.inputTokens ?? 0;
42
+ state.turnUsage.output += usage.outputTokens ?? 0;
43
+ state.turnUsage.cacheRead += usage.cacheReadTokens ?? 0;
44
+ state.turnUsage.cacheWrite += usage.cacheWriteTokens ?? 0;
45
+ state.lastUsage = usage;
46
+ state.hasMessageUsage = true;
47
+ }
48
+ /**
49
+ * Record a live usage chunk. Never summed: the same step also arrives as a
50
+ * durable assistant/message.usage, so summing both would double count. It only
51
+ * refreshes the watermark and covers a turn whose messages never persisted.
52
+ */
53
+ function recordStreamUsage(state, usage) {
54
+ state.lastUsage = usage;
55
+ state.hasStreamUsage = true;
56
+ }
57
+ function chunkToAgentEvents(state, chunk, time, attemptId = '0') {
58
+ if (chunk.type === 'usage' && chunk.usage && typeof chunk.usage === 'object') {
59
+ recordStreamUsage(state, chunk.usage);
60
+ return [];
68
61
  }
69
- /**
70
- * Record a live usage chunk. Never summed: the same step also arrives as a
71
- * durable assistant/message.usage, so summing both would double count. It only
72
- * refreshes the watermark and covers a turn whose messages never persisted.
73
- */
74
- recordStreamUsage(usage) {
75
- this.lastUsage = usage;
76
- this.hasStreamUsage = true;
62
+ const ts = new Date(time).toISOString();
63
+ if (chunk.type === 'text-delta' && typeof chunk.text === 'string' && chunk.text) {
64
+ return [
65
+ {
66
+ type: 'assistant',
67
+ message: { content: [{ type: 'text', text: chunk.text }] },
68
+ timestamp: ts,
69
+ },
70
+ ];
77
71
  }
78
- chunkToAgentEvents(chunk, time, attemptId = '0') {
79
- if (chunk.type === 'usage' && chunk.usage && typeof chunk.usage === 'object') {
80
- this.recordStreamUsage(chunk.usage);
81
- return [];
82
- }
83
- const ts = new Date(time).toISOString();
84
- if (chunk.type === 'text-delta' && typeof chunk.text === 'string' && chunk.text) {
85
- return [
86
- {
87
- type: 'assistant',
88
- message: { content: [{ type: 'text', text: chunk.text }] },
89
- timestamp: ts,
90
- },
91
- ];
92
- }
93
- if (chunk.type === 'reasoning-delta' && typeof chunk.text === 'string' && chunk.text) {
94
- return [
95
- {
96
- type: 'assistant',
97
- message: {
98
- content: [
99
- {
100
- type: 'thinking',
101
- thinking: chunk.text,
102
- // 同一 attempt 的同一 reasoning block:token delta 共享 itemId。
103
- itemId: `r:${attemptId}:${chunk.index}`,
104
- },
105
- ],
106
- },
107
- timestamp: ts,
72
+ if (chunk.type === 'reasoning-delta' && typeof chunk.text === 'string' && chunk.text) {
73
+ return [
74
+ {
75
+ type: 'assistant',
76
+ message: {
77
+ content: [
78
+ {
79
+ type: 'thinking',
80
+ thinking: chunk.text,
81
+ // 同一 attempt 的同一 reasoning block:token delta 共享 itemId。
82
+ itemId: `r:${attemptId}:${chunk.index}`,
83
+ },
84
+ ],
108
85
  },
109
- ];
110
- }
86
+ timestamp: ts,
87
+ },
88
+ ];
89
+ }
90
+ return [];
91
+ }
92
+ /** Translate one live assistant-stream chunk into AgentEvents. */
93
+ function translateStreamFrame(state, frame, _sessionId) {
94
+ if (frame.type === 'start') {
95
+ if (frame.attemptId)
96
+ state.attemptId = frame.attemptId;
111
97
  return [];
112
98
  }
113
- /** Translate one session event into AgentEvents (may be empty). */
114
- eventToAgentEvents(ev, sessionId) {
115
- switch (ev.type) {
116
- case 'assistant/chunk': {
117
- const chunk = ev.data.chunk;
118
- if (!chunk)
119
- return [];
120
- return this.chunkToAgentEvents(chunk, ev.time);
121
- }
122
- case 'assistant/message': {
123
- const raw = ev.data.usage;
124
- if (raw && typeof raw === 'object') {
125
- this.recordMessageUsage(raw);
126
- }
127
- // 每条 durable 消息都带完整正文;记最后一条非空的就是本轮回答。
128
- const text = messageText(ev.data.message);
129
- if (text.trim())
130
- this.lastMessageText = text;
131
- return [];
132
- }
133
- case 'tool/call': {
134
- // DSH 把「模型发起调用」落成独立事件(name/arguments 是模型原始输出)。
135
- // 这是卡片工具行的唯一来源:assistant/message 里只有完成后的正文块,
136
- // 不含调用轨迹。
137
- const call = ev.data;
138
- const id = typeof call.callId === 'string' ? call.callId : '';
139
- const name = typeof call.name === 'string' ? call.name : '';
140
- if (!id || !name)
141
- return [];
142
- return [
143
- {
144
- type: 'assistant',
145
- message: {
146
- content: [{ type: 'tool_use', id, name, input: parseToolArguments(call.arguments) }],
147
- },
148
- timestamp: new Date(ev.time).toISOString(),
149
- },
150
- ];
151
- }
152
- case 'tool/result': {
153
- const data = ev.data;
154
- const msg = data.message;
155
- const toolCallId = typeof msg?.toolCallId === 'string' ? msg.toolCallId : '';
156
- if (!toolCallId)
157
- return [];
158
- return [
99
+ if (frame.type !== 'chunk' || !frame.chunk)
100
+ return [];
101
+ return chunkToAgentEvents(state, frame.chunk, frame.time ?? Date.now(), frame.attemptId ?? state.attemptId);
102
+ }
103
+ function handleAssistantChunk(state, ev, _sessionId) {
104
+ const chunk = ev.data.chunk;
105
+ if (!chunk)
106
+ return [];
107
+ return chunkToAgentEvents(state, chunk, ev.time);
108
+ }
109
+ function handleAssistantMessage(state, ev, _sessionId) {
110
+ const raw = ev.data.usage;
111
+ if (raw && typeof raw === 'object') {
112
+ recordMessageUsage(state, raw);
113
+ }
114
+ // 每条 durable 消息都带完整正文;记最后一条非空的就是本轮回答。
115
+ const text = messageText(ev.data.message);
116
+ if (text.trim())
117
+ state.lastMessageText = text;
118
+ return [];
119
+ }
120
+ function handleToolCall(_state, ev, _sessionId) {
121
+ // DSH 把「模型发起调用」落成独立事件(name/arguments 是模型原始输出)。
122
+ // 这是卡片工具行的唯一来源:assistant/message 里只有完成后的正文块,
123
+ // 不含调用轨迹。
124
+ const call = ev.data;
125
+ const id = typeof call.callId === 'string' ? call.callId : '';
126
+ const name = typeof call.name === 'string' ? call.name : '';
127
+ if (!id || !name)
128
+ return [];
129
+ return [
130
+ {
131
+ type: 'assistant',
132
+ message: {
133
+ content: [{ type: 'tool_use', id, name, input: parseToolArguments(call.arguments) }],
134
+ },
135
+ timestamp: new Date(ev.time).toISOString(),
136
+ },
137
+ ];
138
+ }
139
+ function handleToolResult(_state, ev, _sessionId) {
140
+ const data = ev.data;
141
+ const msg = data.message;
142
+ const toolCallId = typeof msg?.toolCallId === 'string' ? msg.toolCallId : '';
143
+ if (!toolCallId)
144
+ return [];
145
+ return [
146
+ {
147
+ type: 'user',
148
+ message: {
149
+ content: [
159
150
  {
160
- type: 'user',
161
- message: {
162
- content: [
163
- {
164
- type: 'tool_result',
165
- tool_use_id: toolCallId,
166
- content: toolResultText(msg?.content, data.error),
167
- is_error: msg?.isError === true,
168
- },
169
- ],
170
- },
171
- timestamp: new Date(ev.time).toISOString(),
151
+ type: 'tool_result',
152
+ tool_use_id: toolCallId,
153
+ content: toolResultText(msg?.content, data.error),
154
+ is_error: msg?.isError === true,
172
155
  },
173
- ];
174
- }
175
- case 'turn/end': {
176
- const turnReason = ev.data.reason;
177
- const reason = turnReason?.kind;
178
- const subtype = reason === 'completed'
179
- ? 'success'
180
- : reason === 'aborted' || reason === 'interrupted' || reason === 'forked'
181
- ? 'interrupted'
182
- : 'error';
183
- const result = {
184
- type: 'result',
185
- subtype,
186
- session_id: sessionId,
187
- timestamp: new Date(ev.time).toISOString(),
188
- };
189
- if (this.hasMessageUsage) {
190
- // 本 turn 各采样之和(多步工具往返 = 多条 assistant/message)。
191
- result.usage = mapAggregatedUsage(this.turnUsage);
192
- }
193
- else if (this.hasStreamUsage && this.lastUsage) {
194
- // 只有 live usage chunk(消息未落盘,如启动期中断):退回末次采样。
195
- result.usage = mapUsage(this.lastUsage);
196
- }
197
- if (subtype === 'error') {
198
- const detail = turnReason?.error?.message;
199
- result.errorMessage = detail
200
- ? `turn ended with reason: ${reason}: ${detail}`
201
- : `turn ended with reason: ${reason}`;
202
- }
203
- if (this.lastMessageText) {
204
- result.finalAnswerText = this.lastMessageText;
205
- }
206
- return [result];
207
- }
208
- default:
209
- return [];
210
- }
156
+ ],
157
+ },
158
+ timestamp: new Date(ev.time).toISOString(),
159
+ },
160
+ ];
161
+ }
162
+ function handleTurnEnd(state, ev, sessionId) {
163
+ const turnReason = ev.data.reason;
164
+ const reason = turnReason?.kind;
165
+ const subtype = reason === 'completed'
166
+ ? 'success'
167
+ : reason === 'aborted' || reason === 'interrupted' || reason === 'forked'
168
+ ? 'interrupted'
169
+ : 'error';
170
+ const result = {
171
+ type: 'result',
172
+ subtype,
173
+ session_id: sessionId,
174
+ timestamp: new Date(ev.time).toISOString(),
175
+ };
176
+ if (state.hasMessageUsage) {
177
+ // 本 turn 各采样之和(多步工具往返 = 多条 assistant/message)。
178
+ result.usage = mapAggregatedUsage(state.turnUsage);
179
+ }
180
+ else if (state.hasStreamUsage && state.lastUsage) {
181
+ // 只有 live usage chunk(消息未落盘,如启动期中断):退回末次采样。
182
+ result.usage = mapUsage(state.lastUsage);
183
+ }
184
+ if (subtype === 'error') {
185
+ const detail = turnReason?.error?.message;
186
+ result.errorMessage = detail
187
+ ? `turn ended with reason: ${reason}: ${detail}`
188
+ : `turn ended with reason: ${reason}`;
189
+ }
190
+ if (state.lastMessageText) {
191
+ result.finalAnswerText = state.lastMessageText;
192
+ }
193
+ return [result];
194
+ }
195
+ /** 事件类型 → handler。用 Map 而非对象字面量,避免 prototype 键命中。 */
196
+ const SESSION_EVENT_HANDLERS = new Map([
197
+ ['assistant/chunk', handleAssistantChunk],
198
+ ['assistant/message', handleAssistantMessage],
199
+ ['tool/call', handleToolCall],
200
+ ['tool/result', handleToolResult],
201
+ ['turn/end', handleTurnEnd],
202
+ ]);
203
+ export class DshTranslator {
204
+ state = newTurnState();
205
+ /** Translate one live assistant-stream chunk into AgentEvents. */
206
+ streamFrameToAgentEvents(frame, _sessionId) {
207
+ return translateStreamFrame(this.state, frame, _sessionId);
208
+ }
209
+ /** Translate one session event into AgentEvents (may be empty). */
210
+ eventToAgentEvents(ev, sessionId) {
211
+ const handler = SESSION_EVENT_HANDLERS.get(ev.type);
212
+ return handler ? handler(this.state, ev, sessionId) : [];
211
213
  }
212
214
  }
213
215
  /**
@@ -73,6 +73,85 @@ function toAcpMode(mode) {
73
73
  return 'yolo';
74
74
  }
75
75
  }
76
+ // =============================================================================
77
+ // buildQuestionResponse 的模块级命名 handler(闭包搬出,显式传状态)
78
+ // =============================================================================
79
+ function nonEmptyString(v) {
80
+ return typeof v === 'string' && v.length > 0;
81
+ }
82
+ /** 单题答案 → content[qN](多选清洗空串、单选取首个)。 */
83
+ function collectOneAnswer(q, i, byIndex, content) {
84
+ const value = byIndex[i];
85
+ if (value === undefined)
86
+ return;
87
+ if (q.multiSelect) {
88
+ const values = Array.isArray(value) ? value : [value];
89
+ const cleaned = values.filter(nonEmptyString);
90
+ if (cleaned.length > 0)
91
+ content[`q${i}`] = cleaned;
92
+ }
93
+ else {
94
+ const single = Array.isArray(value) ? value[0] : value;
95
+ if (typeof single === 'string' && single.length > 0)
96
+ content[`q${i}`] = single;
97
+ }
98
+ }
99
+ /** elicitation form:{问题文本: 值} → 按问题顺序展开的 q0..qn content。 */
100
+ function elicitationContent(questions, answers) {
101
+ const byIndex = mapAnswersByIndex(questions, answers);
102
+ const content = {};
103
+ for (let i = 0; i < questions.length; i++) {
104
+ collectOneAnswer(questions[i], i, byIndex, content);
105
+ }
106
+ return content;
107
+ }
108
+ function elicitationResponse(action, pending, response) {
109
+ if (action === 'answer') {
110
+ const answers = response.answers;
111
+ const questions = pending.view.questions ?? [];
112
+ return { action: 'accept', content: elicitationContent(questions, answers ?? {}) };
113
+ }
114
+ if (action === 'decline')
115
+ return { action: 'decline' };
116
+ return { action: 'cancel' };
117
+ }
118
+ /** request_permission 兜底桥:按 label 回显 qN_opt_M optionId。 */
119
+ function selectedOptionId(options, label) {
120
+ for (const opt of options) {
121
+ if (/^q\d+_opt_\d+$/.test(opt.optionId) && opt.name === label)
122
+ return opt.optionId;
123
+ }
124
+ return undefined;
125
+ }
126
+ /** request_permission 兜底桥:找 qN_skip optionId。 */
127
+ function skipOptionId(options) {
128
+ for (const opt of options) {
129
+ if (/^q\d+_skip$/.test(opt.optionId))
130
+ return opt.optionId;
131
+ }
132
+ return undefined;
133
+ }
134
+ function cancelledOutcome() {
135
+ return { outcome: { outcome: 'cancelled' } };
136
+ }
137
+ function selectedOutcome(optionId) {
138
+ return { outcome: { outcome: 'selected', optionId } };
139
+ }
140
+ function permissionResponse(action, pending, response) {
141
+ if (action === 'answer') {
142
+ const answers = response.answers;
143
+ const first = pending.view.questions?.[0];
144
+ const value = first ? answers?.[first.question] : undefined;
145
+ const label = Array.isArray(value) ? value[0] : value;
146
+ const optionId = selectedOptionId(pending.options ?? [], label);
147
+ return optionId ? selectedOutcome(optionId) : cancelledOutcome();
148
+ }
149
+ if (action === 'decline') {
150
+ const skipId = skipOptionId(pending.options ?? []);
151
+ return skipId ? selectedOutcome(skipId) : cancelledOutcome();
152
+ }
153
+ return cancelledOutcome();
154
+ }
76
155
  /**
77
156
  * AskUserQuestion 回编(kind === 'question'):
78
157
  * - elicitation form:answer → {action:'accept', content:{q0..qn}}(多选数组/
@@ -84,51 +163,9 @@ function toAcpMode(mode) {
84
163
  */
85
164
  function buildQuestionResponse(action, pending, response) {
86
165
  if (pending.proto === 'elicitation') {
87
- if (action === 'answer') {
88
- const answers = response.answers;
89
- const questions = pending.view.questions ?? [];
90
- const byIndex = mapAnswersByIndex(questions, answers ?? {});
91
- const content = {};
92
- questions.forEach((q, i) => {
93
- const value = byIndex[i];
94
- if (value === undefined)
95
- return;
96
- if (q.multiSelect) {
97
- const values = Array.isArray(value) ? value : [value];
98
- const cleaned = values.filter((v) => typeof v === 'string' && v.length > 0);
99
- if (cleaned.length > 0)
100
- content[`q${i}`] = cleaned;
101
- }
102
- else {
103
- const single = Array.isArray(value) ? value[0] : value;
104
- if (typeof single === 'string' && single.length > 0)
105
- content[`q${i}`] = single;
106
- }
107
- });
108
- return { action: 'accept', content };
109
- }
110
- if (action === 'decline')
111
- return { action: 'decline' };
112
- return { action: 'cancel' };
113
- }
114
- // request_permission 兜底桥(单题单选)
115
- if (action === 'answer') {
116
- const answers = response.answers;
117
- const first = pending.view.questions?.[0];
118
- const value = first ? answers?.[first.question] : undefined;
119
- const label = Array.isArray(value) ? value[0] : value;
120
- const option = (pending.options ?? []).find((opt) => /^q\d+_opt_\d+$/.test(opt.optionId) && opt.name === label);
121
- if (option)
122
- return { outcome: { outcome: 'selected', optionId: option.optionId } };
123
- return { outcome: { outcome: 'cancelled' } };
166
+ return elicitationResponse(action, pending, response);
124
167
  }
125
- if (action === 'decline') {
126
- const skip = (pending.options ?? []).find((opt) => /^q\d+_skip$/.test(opt.optionId));
127
- if (skip)
128
- return { outcome: { outcome: 'selected', optionId: skip.optionId } };
129
- return { outcome: { outcome: 'cancelled' } };
130
- }
131
- return { outcome: { outcome: 'cancelled' } };
168
+ return permissionResponse(action, pending, response);
132
169
  }
133
170
  // =============================================================================
134
171
  // Runner
@@ -24,7 +24,7 @@
24
24
  * 5xx/network errors retry in seconds and must be left to opencode's own
25
25
  * retry loop (the idle watchdog remains the fallback for silent hangs).
26
26
  */
27
- import { closeSync, openSync, readSync, statSync } from 'node:fs';
27
+ import fs from 'node:fs';
28
28
  import { homedir } from 'node:os';
29
29
  import { join } from 'node:path';
30
30
  import { getLogger } from '../../../logger/index.js';
@@ -88,7 +88,7 @@ export class OpencodeLogErrorMonitor {
88
88
  }
89
89
  currentSize() {
90
90
  try {
91
- return statSync(this.logPath).size;
91
+ return fs.statSync(this.logPath).size;
92
92
  }
93
93
  catch (err) {
94
94
  this.noteIoError(err);
@@ -124,14 +124,14 @@ export class OpencodeLogErrorMonitor {
124
124
  if (size > this.offset) {
125
125
  let chunk;
126
126
  try {
127
- const fd = openSync(this.logPath, 'r');
127
+ const fd = fs.openSync(this.logPath, 'r');
128
128
  try {
129
129
  const buf = Buffer.alloc(size - this.offset);
130
- readSync(fd, buf, 0, buf.length, this.offset);
130
+ fs.readSync(fd, buf, 0, buf.length, this.offset);
131
131
  chunk = buf.toString('utf8');
132
132
  }
133
133
  finally {
134
- closeSync(fd);
134
+ fs.closeSync(fd);
135
135
  }
136
136
  }
137
137
  catch (err) {
@@ -10,7 +10,7 @@
10
10
  * Response correlation is by command id; every non-response line is forwarded
11
11
  * as an event to the active hooks (the runner's translator).
12
12
  */
13
- import { ConnectionLostError } from '../../common/jsonrpc/client.js';
13
+ import { ConnectionLostError, disposeRpcClientInstance, rejectAllPending, } from '../../common/jsonrpc/client.js';
14
14
  export class PiRpcClient {
15
15
  transport;
16
16
  requestTimeoutMs;
@@ -78,13 +78,8 @@ export class PiRpcClient {
78
78
  return;
79
79
  this.transport.write({ ...command, id: `req_${this.nextId++}` });
80
80
  }
81
- async dispose() {
82
- if (this._disposed)
83
- return;
84
- this._disposed = true;
85
- this._ready = false;
86
- this.failPending(new ConnectionLostError('pi rpc client disposed'));
87
- await this.transport.close();
81
+ dispose() {
82
+ return disposeRpcClientInstance(this, this.pending, () => this.transport.close(), 'pi rpc client disposed');
88
83
  }
89
84
  // =========================================================================
90
85
  // Internal
@@ -104,11 +99,7 @@ export class PiRpcClient {
104
99
  this.runHooks?.onEvent?.(message);
105
100
  }
106
101
  failPending(err) {
107
- for (const [_id, pending] of this.pending) {
108
- clearTimeout(pending.timer);
109
- pending.reject(err);
110
- }
111
- this.pending.clear();
102
+ rejectAllPending(this.pending, err);
112
103
  }
113
104
  }
114
105
  //# sourceMappingURL=client.js.map