lark-remote 0.1.13 → 0.2.1

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 (132) hide show
  1. package/CHANGELOG.md +36 -3
  2. package/README.en.md +7 -11
  3. package/README.md +7 -11
  4. package/dist/bridge/final-usage.d.ts +24 -0
  5. package/dist/bridge/final-usage.js +55 -0
  6. package/dist/bridge/index.d.ts +0 -7
  7. package/dist/bridge/index.js +22 -64
  8. package/dist/bridge/queue-manager.js +4 -3
  9. package/dist/card/approval-render.js +39 -94
  10. package/dist/card/bash-renderer.js +12 -21
  11. package/dist/card/card-shared.d.ts +13 -1
  12. package/dist/card/card-shared.js +21 -30
  13. package/dist/card/run-renderer.d.ts +0 -22
  14. package/dist/card/run-renderer.js +206 -153
  15. package/dist/card/run-state.d.ts +29 -1
  16. package/dist/card/run-state.js +172 -84
  17. package/dist/card/text-truncate.d.ts +0 -9
  18. package/dist/card/text-truncate.js +0 -9
  19. package/dist/card/tool-render.d.ts +4 -1
  20. package/dist/card/tool-render.js +76 -32
  21. package/dist/cli.js +2 -2
  22. package/dist/common/fs.d.ts +14 -0
  23. package/dist/common/fs.js +57 -3
  24. package/dist/config/codex-config.js +2 -0
  25. package/dist/config/index.d.ts +0 -5
  26. package/dist/config/index.js +0 -11
  27. package/dist/config/kimi-config.js +1 -0
  28. package/dist/config/opencode-config.js +13 -2
  29. package/dist/config/qr.d.ts +36 -0
  30. package/dist/config/qr.js +124 -0
  31. package/dist/config/wizard.js +69 -27
  32. package/dist/connector/index.d.ts +3 -2
  33. package/dist/connector/index.js +5 -4
  34. package/dist/index.js +15 -42
  35. package/dist/instance-lock.js +7 -0
  36. package/dist/persistence/atomic-write.d.ts +2 -0
  37. package/dist/persistence/atomic-write.js +20 -5
  38. package/dist/persistence/load-json-file.d.ts +1 -2
  39. package/dist/persistence/load-json-file.js +2 -3
  40. package/dist/platform/agent-stopper.d.ts +22 -0
  41. package/dist/platform/agent-stopper.js +19 -0
  42. package/dist/platform/command.d.ts +11 -0
  43. package/dist/platform/command.js +107 -0
  44. package/dist/platform/identity.d.ts +33 -0
  45. package/dist/platform/identity.js +187 -0
  46. package/dist/platform/path.d.ts +24 -0
  47. package/dist/platform/path.js +110 -0
  48. package/dist/platform/select.d.ts +9 -0
  49. package/dist/platform/select.js +12 -0
  50. package/dist/platform/shell.d.ts +51 -0
  51. package/dist/platform/shell.js +75 -0
  52. package/dist/platform/spawn.d.ts +41 -0
  53. package/dist/platform/spawn.js +77 -0
  54. package/dist/platform/terminator-posix.d.ts +38 -0
  55. package/dist/platform/terminator-posix.js +103 -0
  56. package/dist/platform/terminator-win32.d.ts +21 -0
  57. package/dist/platform/terminator-win32.js +110 -0
  58. package/dist/platform/terminator.d.ts +44 -0
  59. package/dist/platform/terminator.js +24 -0
  60. package/dist/platform/types.d.ts +23 -0
  61. package/dist/platform/types.js +5 -0
  62. package/dist/restart.js +4 -2
  63. package/dist/router/card-helpers.d.ts +33 -6
  64. package/dist/router/card-helpers.js +67 -11
  65. package/dist/router/config/common/render.d.ts +1 -1
  66. package/dist/router/config/common/render.js +1 -1
  67. package/dist/router/config-diff.d.ts +4 -0
  68. package/dist/router/config-diff.js +70 -0
  69. package/dist/router/index.d.ts +66 -6
  70. package/dist/router/index.js +451 -316
  71. package/dist/router/utils.d.ts +6 -0
  72. package/dist/router/utils.js +12 -0
  73. package/dist/runner/bash/runner.d.ts +9 -2
  74. package/dist/runner/bash/runner.js +45 -20
  75. package/dist/runner/claude/session.d.ts +4 -1
  76. package/dist/runner/claude/session.js +18 -4
  77. package/dist/runner/codex/app-server/protocol-types.d.ts +35 -1
  78. package/dist/runner/codex/app-server/translator.d.ts +18 -0
  79. package/dist/runner/codex/app-server/translator.js +146 -0
  80. package/dist/runner/common/acp/base-acp-runner.d.ts +116 -0
  81. package/dist/runner/common/acp/base-acp-runner.js +248 -0
  82. package/dist/runner/common/acp/base-translator.d.ts +141 -0
  83. package/dist/runner/common/acp/base-translator.js +271 -0
  84. package/dist/runner/common/acp/protocol-helpers.d.ts +39 -0
  85. package/dist/runner/common/acp/protocol-helpers.js +34 -0
  86. package/dist/runner/common/jsonrpc/transport.d.ts +2 -0
  87. package/dist/runner/common/jsonrpc/transport.js +25 -5
  88. package/dist/runner/common/process-stopper.d.ts +4 -26
  89. package/dist/runner/common/process-stopper.js +4 -76
  90. package/dist/runner/common/runner-utils.d.ts +0 -5
  91. package/dist/runner/common/runner-utils.js +0 -7
  92. package/dist/runner/common/spawning-runner.d.ts +42 -132
  93. package/dist/runner/common/spawning-runner.js +52 -271
  94. package/dist/runner/common/usage.d.ts +2 -0
  95. package/dist/runner/common/usage.js +2 -0
  96. package/dist/runner/index.d.ts +1 -1
  97. package/dist/runner/kimi/acp/runner.d.ts +45 -42
  98. package/dist/runner/kimi/acp/runner.js +140 -293
  99. package/dist/runner/kimi/acp/translator.d.ts +18 -100
  100. package/dist/runner/kimi/acp/translator.js +56 -242
  101. package/dist/runner/opencode/acp/runner.d.ts +22 -46
  102. package/dist/runner/opencode/acp/runner.js +73 -256
  103. package/dist/runner/opencode/acp/translator.d.ts +28 -103
  104. package/dist/runner/opencode/acp/translator.js +51 -236
  105. package/dist/runner/pi/rpc/translator.d.ts +3 -0
  106. package/dist/runner/pi/rpc/translator.js +47 -9
  107. package/dist/runner/probe.d.ts +8 -11
  108. package/dist/runner/probe.js +17 -21
  109. package/dist/runner/registry.d.ts +4 -4
  110. package/dist/runner/registry.js +4 -4
  111. package/dist/runner/types.d.ts +38 -1
  112. package/dist/session/claude/sessions.js +35 -58
  113. package/dist/session/codex/rollout-reader.d.ts +10 -0
  114. package/dist/session/codex/rollout-reader.js +123 -41
  115. package/dist/session/common/constants.d.ts +7 -0
  116. package/dist/session/common/constants.js +9 -0
  117. package/dist/session/common/two-pass.d.ts +22 -0
  118. package/dist/session/common/two-pass.js +70 -0
  119. package/dist/session/dsh/sessions.js +7 -6
  120. package/dist/session/kimi/sessions.d.ts +6 -0
  121. package/dist/session/kimi/sessions.js +50 -73
  122. package/dist/session/opencode/sessions.js +26 -8
  123. package/dist/session/pi/sessions.js +54 -73
  124. package/dist/update/index.d.ts +1 -2
  125. package/dist/update/index.js +1 -2
  126. package/dist/update/install.d.ts +2 -1
  127. package/dist/update/install.js +58 -9
  128. package/dist/update/version-check.d.ts +5 -0
  129. package/dist/update/version-check.js +9 -0
  130. package/package.json +9 -6
  131. package/dist/update/startup-hint.d.ts +0 -5
  132. package/dist/update/startup-hint.js +0 -11
@@ -7,10 +7,8 @@
7
7
  * → synthetic init → session/prompt → consume session/update notifications
8
8
  * until prompt settles → result event.
9
9
  *
10
- * Structurally aligned with KimiAcpRunner via the shared ConnectionBasedRunner
11
- * base (same notification queue + waitResolve pattern, same forceFinish/
12
- * stopRequested semantics, same turn idle timeout, same ConnectionLostError
13
- * retry). Differences from kimi:
10
+ * Turn/connection/compact/approval orchestration lives in BaseAcpRunner;
11
+ * opencode-specific parts:
14
12
  * - session/set_mode applies the configured mode (build/plan; default 'build'
15
13
  * keeps manual approvals flowing); opencode sends no set_mode notification,
16
14
  * so the client refreshes the local mode view itself
@@ -18,22 +16,20 @@
18
16
  * service.ts:555-571 summarize completes before the prompt response)
19
17
  * - text/thinking go to the assistant incremental channel (opencode wire
20
18
  * deltas), not turn_diff snapshots
19
+ * - OpencodeLogErrorMonitor tails opencode's own log for LLM stream errors
20
+ * the ACP protocol never forwards
21
21
  */
22
22
  import { ConnectionManager, } from '../../common/jsonrpc/connection-manager.js';
23
23
  import { OpencodeAcpTranslator } from './translator.js';
24
24
  import { RpcErrorCode, } from '../../common/acp/protocol-types.js';
25
25
  import { OPENCODE_APPROVAL_KINDS, buildAcpPermissionOutcome, } from '../../common/acp/protocol-helpers.js';
26
26
  import { getLogger } from '../../../logger/index.js';
27
- import { ConnectionBasedRunner } from '../../common/connection-based-runner.js';
27
+ import { BaseAcpRunner } from '../../common/acp/base-acp-runner.js';
28
28
  import { OpencodeLogErrorMonitor, resolveOpencodeLogPath } from './error-monitor.js';
29
29
  // =============================================================================
30
30
  // Runner
31
31
  // =============================================================================
32
- export class OpencodeAcpRunner extends ConnectionBasedRunner {
33
- connectionManager;
34
- currentTranslator = null;
35
- activeSessionId = null;
36
- model;
32
+ export class OpencodeAcpRunner extends BaseAcpRunner {
37
33
  /** Configured session mode applied via session/set_mode (§P5). */
38
34
  configuredMode;
39
35
  /** Current session mode id, parsed from session/new|resume configOptions. */
@@ -43,22 +39,7 @@ export class OpencodeAcpRunner extends ConnectionBasedRunner {
43
39
  /** Tails opencode's own log to surface LLM stream errors the ACP protocol
44
40
  * never forwards (quota/limit errors back off for ~1h → turn would hang). */
45
41
  errorMonitor;
46
- /** Pending approval requests: requestId → kind + view + options. */
47
- pendingApprovals = new Map();
48
- /** Tracker for the in-flight prompt request — needed for cancellation. */
49
- promptSettled = false;
50
- /** Whether the prompt request has been fired (distinguishes "stop before
51
- * prompt sent" from "stop while prompt in-flight" — the latter sends
52
- * session/cancel immediately). */
53
- promptSent = false;
54
42
  constructor(opts) {
55
- super({ kind: opts.kind, sessionReader: opts.sessionReader }, opts.turnIdleTimeoutMs);
56
- this.model = opts.model;
57
- this.configuredMode = opts.mode ?? 'build';
58
- this.errorMonitor = new OpencodeLogErrorMonitor({
59
- logPath: opts.errorMonitorLogPath ?? resolveOpencodeLogPath(),
60
- pollIntervalMs: opts.errorMonitorPollIntervalMs,
61
- });
62
43
  const managerOpts = {
63
44
  binary: opts.binary ?? 'opencode',
64
45
  args: opts.acpArgs ?? ['acp'],
@@ -76,7 +57,17 @@ export class OpencodeAcpRunner extends ConnectionBasedRunner {
76
57
  },
77
58
  },
78
59
  };
79
- this.connectionManager = new ConnectionManager(managerOpts);
60
+ super({
61
+ kind: opts.kind,
62
+ sessionReader: opts.sessionReader,
63
+ turnIdleTimeoutMs: opts.turnIdleTimeoutMs,
64
+ }, new ConnectionManager(managerOpts));
65
+ this.model = opts.model;
66
+ this.configuredMode = opts.mode ?? 'build';
67
+ this.errorMonitor = new OpencodeLogErrorMonitor({
68
+ logPath: opts.errorMonitorLogPath ?? resolveOpencodeLogPath(),
69
+ pollIntervalMs: opts.errorMonitorPollIntervalMs,
70
+ });
80
71
  }
81
72
  get logTag() {
82
73
  return 'opencode-acp-runner';
@@ -87,31 +78,12 @@ export class OpencodeAcpRunner extends ConnectionBasedRunner {
87
78
  get turnInterruptedErrorMessage() {
88
79
  return 'opencode ACP turn interrupted';
89
80
  }
90
- currentSessionId() {
91
- return this.activeSessionId;
81
+ get connectionClosedMessage() {
82
+ return 'opencode ACP connection closed';
92
83
  }
93
84
  shouldDeferStop() {
94
85
  return !this.promptSent;
95
86
  }
96
- async cancelCurrentTurn() {
97
- if (!this.currentClient || !this.activeSessionId)
98
- return;
99
- try {
100
- // session/cancel is a NOTIFICATION (no id, no response) per ACP spec.
101
- const cancelParams = { sessionId: this.activeSessionId };
102
- this.currentClient.notify('session/cancel', cancelParams);
103
- }
104
- catch (err) {
105
- getLogger().warn(`[${this.logTag}] session/cancel failed: ${err.message}`);
106
- }
107
- }
108
- clearTurnState() {
109
- this.errorMonitor.stop();
110
- this.currentTranslator = null;
111
- this.promptSettled = false;
112
- this.promptSent = false;
113
- this.activeSessionId = null;
114
- }
115
87
  /**
116
88
  * Start watching opencode's own log for LLM stream errors of the active
117
89
  * session. opencode ACP does not surface retry/error state to the client
@@ -127,175 +99,10 @@ export class OpencodeAcpRunner extends ConnectionBasedRunner {
127
99
  this.failTurn(`opencode LLM stream error: ${message}`);
128
100
  });
129
101
  }
130
- async releaseConnection(cwd) {
131
- await this.connectionManager.release(cwd);
132
- }
133
- notifyIdle(cwd) {
134
- this.connectionManager.notifyIdle(cwd);
135
- }
136
- async disposeConnections() {
137
- await this.connectionManager.disposeAll();
138
- }
139
- /**
140
- * Run a compact operation on the current session.
141
- *
142
- * opencode special-cases the `/compact` text prompt server-side
143
- * (service.ts:555-571): it runs session.summarize synchronously and the
144
- * prompt response settles only after compaction completes — no background
145
- * record polling needed (unlike kimi).
146
- */
147
- async *runCompact(_message, opts) {
148
- yield* this.executeTurn(opts, async () => {
149
- const client = await this.connectionManager.acquire(opts.cwd);
150
- this.connectionManager.notifyActivity(opts.cwd);
151
- this.currentClient = client;
152
- client.setHooks({
153
- onNotification: (method, params) => this.handleNotification(method, params),
154
- onServerRequest: (id, method, params) => this.handleServerRequest(id, method, params),
155
- onClose: () => this.failTurn('opencode ACP connection closed'),
156
- });
157
- const sessionId = opts.sessionId;
158
- if (!sessionId) {
159
- throw new Error('compact requires a sessionId');
160
- }
161
- this.activeSessionId = sessionId;
162
- this.startErrorMonitor(sessionId);
163
- // Cold-connection fallback: session/resume loads the session into
164
- // memory. On a reused connection the session is already loaded —
165
- // resume is idempotent and harmless.
166
- const resumeParams = { sessionId, cwd: opts.cwd };
167
- const resumeResult = await client.request('session/resume', resumeParams);
168
- this.trackConfigOptions(resumeResult);
169
- const translator = new OpencodeAcpTranslator();
170
- translator.setOperationKind('compact');
171
- this.currentTranslator = translator;
172
- // Emit turn_started with operationKind='compaction' before the prompt.
173
- const turnId = `compact-${Date.now()}`;
174
- const turnStarted = translator.produceTurnStarted(sessionId, turnId);
175
- this.currentTurnId = turnId;
176
- this.pushEvents([turnStarted]);
177
- // Send /compact as prompt text (opencode intercepts it server-side).
178
- const promptParams = {
179
- sessionId,
180
- prompt: [{ type: 'text', text: '/compact' }],
181
- };
182
- this.promptSettled = false;
183
- // ACP holds the session/prompt response for the ENTIRE turn, so the
184
- // RPC-level timeout must not apply — turn liveness is guarded by the
185
- // rolling idle watchdog instead.
186
- const promptPromise = client.request('session/prompt', promptParams, Number.POSITIVE_INFINITY);
187
- this.promptSent = true;
188
- promptPromise.then((result) => {
189
- this.promptSettled = true;
190
- if (this.stopRequested)
191
- return; // already cancelled
192
- const resultEvent = translator.handlePromptResponse(sessionId, result);
193
- this.pushEvents([resultEvent]);
194
- }, (err) => {
195
- this.promptSettled = true;
196
- if (this.stopRequested) {
197
- getLogger().warn(`[${this.logTag}] compact prompt rejected after stopRequested: ${err.message}`);
198
- return;
199
- }
200
- const resultEvent = translator.produceErrorResult(sessionId, err.message);
201
- this.pushEvents([resultEvent]);
202
- });
203
- });
204
- }
205
- /**
206
- * Respond to an approval server request. `response` is the bridge
207
- * ApprovalAction (`{ action: 'accept' | 'decline' | 'cancel' }`).
208
- */
209
- async respondApproval(requestId, response) {
210
- const client = this.currentClient;
211
- const pending = this.pendingApprovals.get(requestId);
212
- if (!client || !pending)
213
- return;
214
- const action = response?.action ?? 'decline';
215
- const acpResponse = buildAcpPermissionOutcome(action, pending.options, OPENCODE_APPROVAL_KINDS);
216
- client.respond(requestId, acpResponse);
217
- this.pendingApprovals.delete(requestId);
218
- getLogger().info(`[${this.logTag}] approval responded requestId=${requestId} action=${action}`);
219
- }
220
- getStatusInfo() {
221
- return {
222
- kind: this.kind,
223
- model: this.model ?? '(opencode-acp)',
224
- extras: {
225
- mode: 'acp',
226
- ...(this.currentModeId ? { sessionMode: this.currentModeId } : {}),
227
- },
228
- };
102
+ createTranslator() {
103
+ return new OpencodeAcpTranslator();
229
104
  }
230
- /**
231
- * Hot-apply a mode change to the live ACP session (§P5).
232
- *
233
- * opencode's session/set_mode sends no notification — the client refreshes
234
- * the local mode view itself. The local mode is always updated so
235
- * `getStatusInfo()` (and therefore `/s`) reflects the new value
236
- * immediately; when a session is connected, also re-sends session/set_mode
237
- * so the running session picks up the new mode without waiting for the
238
- * runner to be evicted/recreated. Failure is non-fatal: the next setupTurn
239
- * re-applies the cached mode when it differs from the session's current
240
- * mode.
241
- */
242
- async updateApprovalMode(settings) {
243
- if (settings.mode !== undefined) {
244
- this.configuredMode = settings.mode;
245
- this.currentModeId = settings.mode;
246
- }
247
- if (!this.currentClient || !this.activeSessionId)
248
- return;
249
- const modeParams = {
250
- sessionId: this.activeSessionId,
251
- modeId: this.configuredMode,
252
- };
253
- try {
254
- await this.currentClient.request('session/set_mode', modeParams);
255
- getLogger().info(`[${this.logTag}] session/set_mode hot-applied session=${this.activeSessionId} modeId=${modeParams.modeId}`);
256
- }
257
- catch (err) {
258
- getLogger().warn(`[${this.logTag}] session/set_mode failed (non-fatal): ${err.message}`);
259
- }
260
- }
261
- // =========================================================================
262
- // Internal
263
- // =========================================================================
264
- /**
265
- * Acquire the connection and set up the turn:
266
- * session/new (new session) or session/resume (existing sessionId)
267
- * → session/prompt.
268
- */
269
- async setupTurn(message, opts) {
270
- const client = await this.connectionManager.acquire(opts.cwd);
271
- this.connectionManager.notifyActivity(opts.cwd);
272
- this.currentClient = client;
273
- client.setHooks({
274
- onNotification: (method, params) => this.handleNotification(method, params),
275
- onServerRequest: (id, method, params) => this.handleServerRequest(id, method, params),
276
- onClose: () => {
277
- getLogger().warn(`[${this.logTag}] client connection closed`);
278
- this.failTurn('opencode ACP connection closed');
279
- },
280
- });
281
- let sessionId;
282
- if (opts.sessionId) {
283
- const resumeParams = {
284
- sessionId: opts.sessionId,
285
- cwd: opts.cwd,
286
- };
287
- const resumeResult = await client.request('session/resume', resumeParams);
288
- this.trackConfigOptions(resumeResult);
289
- sessionId = opts.sessionId;
290
- }
291
- else {
292
- const newParams = { cwd: opts.cwd, mcpServers: [] };
293
- const newResult = await client.request('session/new', newParams);
294
- this.trackConfigOptions(newResult);
295
- sessionId = newResult.sessionId;
296
- }
297
- this.activeSessionId = sessionId;
298
- this.startErrorMonitor(sessionId);
105
+ async applyTurnSettings(client, sessionId) {
299
106
  // §P5: apply the configured mode (build/plan). If the session already
300
107
  // runs it (from session/new|resume configOptions), skip the wire call;
301
108
  // otherwise send session/set_mode and refresh the local view (opencode
@@ -331,39 +138,49 @@ export class OpencodeAcpRunner extends ConnectionBasedRunner {
331
138
  getLogger().warn(`[${this.logTag}] set_config_option model failed (non-fatal): ${err.message}`);
332
139
  }
333
140
  }
334
- const translator = new OpencodeAcpTranslator();
335
- this.currentTranslator = translator;
336
- // Emit turn_started before prompt
337
- const turnId = `turn-${Date.now()}`;
338
- const turnStarted = translator.produceTurnStarted(sessionId, turnId);
339
- this.currentTurnId = turnId;
340
- this.pushEvents([turnStarted]);
341
- // Send the prompt. The prompt request races with notifications (the
342
- // server streams session/update before the prompt response); the
343
- // response settles the turn and is translated into the result event.
344
- const promptParams = {
345
- sessionId,
346
- prompt: [{ type: 'text', text: message }],
141
+ }
142
+ /**
143
+ * opencode /compact is synchronous server-side — standard prompt-settle
144
+ * translation (no wire.jsonl polling, unlike kimi).
145
+ */
146
+ onCompactPromptFired(promptPromise, translator, sessionId, _opts, _baselineTotalCount) {
147
+ promptPromise.then((result) => this.handlePromptResolved(result, translator, sessionId), (err) => this.handlePromptRejected(err, translator, sessionId, 'compact'));
148
+ }
149
+ buildApprovalOutcome(action, pending, _response) {
150
+ return buildAcpPermissionOutcome(action, pending.options, OPENCODE_APPROVAL_KINDS);
151
+ }
152
+ getStatusInfo() {
153
+ return {
154
+ kind: this.kind,
155
+ model: this.model ?? '(opencode-acp)',
156
+ extras: {
157
+ mode: 'acp',
158
+ ...(this.currentModeId ? { sessionMode: this.currentModeId } : {}),
159
+ },
347
160
  };
348
- this.promptSettled = false;
349
- const promptPromise = client.request('session/prompt', promptParams, Number.POSITIVE_INFINITY);
350
- this.promptSent = true;
351
- promptPromise.then((result) => {
352
- this.promptSettled = true;
353
- if (this.stopRequested)
354
- return; // already cancelled
355
- const resultEvent = translator.handlePromptResponse(sessionId, result);
356
- this.pushEvents([resultEvent]);
357
- }, (err) => {
358
- this.promptSettled = true;
359
- if (this.stopRequested) {
360
- getLogger().warn(`[${this.logTag}] prompt rejected after stopRequested: ${err.message}`);
361
- return;
362
- }
363
- const resultEvent = translator.produceErrorResult(sessionId, err.message);
364
- this.pushEvents([resultEvent]);
365
- });
366
161
  }
162
+ /**
163
+ * Hot-apply a mode change to the live ACP session (§P5).
164
+ *
165
+ * opencode's session/set_mode sends no notification — the client refreshes
166
+ * the local mode view itself. The local mode is always updated so
167
+ * `getStatusInfo()` (and therefore `/s`) reflects the new value
168
+ * immediately; when a session is connected, also re-sends session/set_mode
169
+ * so the running session picks up the new mode without waiting for the
170
+ * runner to be evicted/recreated. Failure is non-fatal: the next setupTurn
171
+ * re-applies the cached mode when it differs from the session's current
172
+ * mode.
173
+ */
174
+ async updateApprovalMode(settings) {
175
+ if (settings.mode !== undefined) {
176
+ this.configuredMode = settings.mode;
177
+ this.currentModeId = settings.mode;
178
+ }
179
+ await this.sendApprovalModeUpdate(this.configuredMode);
180
+ }
181
+ // =========================================================================
182
+ // Internal
183
+ // =========================================================================
367
184
  /**
368
185
  * Track the current session mode and model from a session/new|resume
369
186
  * result's configOptions (opencode buildModeSelectOption/buildModelSelectOption:
@@ -382,6 +199,15 @@ export class OpencodeAcpRunner extends ConnectionBasedRunner {
382
199
  this.currentModelValue = model.currentValue;
383
200
  }
384
201
  }
202
+ onTurnSessionReady(sessionId) {
203
+ this.startErrorMonitor(sessionId);
204
+ }
205
+ onSessionEstablished(result) {
206
+ this.trackConfigOptions(result);
207
+ }
208
+ onClearTurnState() {
209
+ this.errorMonitor.stop();
210
+ }
385
211
  handleNotification(method, params) {
386
212
  const events = this.currentTranslator?.handleNotification(method, params) ?? [];
387
213
  this.pushEvents(events);
@@ -399,16 +225,7 @@ export class OpencodeAcpRunner extends ConnectionBasedRunner {
399
225
  getLogger().info(`[${this.logTag}] rejected unsupported server request method=${method} id=${id}`);
400
226
  return;
401
227
  }
402
- for (const ev of events) {
403
- if (ev.type === 'approval_requested') {
404
- this.pendingApprovals.set(ev.requestId, {
405
- kind: ev.kind,
406
- view: ev.view,
407
- options: params.options,
408
- });
409
- getLogger().info(`[${this.logTag}] approval requested requestId=${ev.requestId} kind=${ev.kind}`);
410
- }
411
- }
228
+ this.registerApprovalEvents(events, params.options);
412
229
  this.pushEvents(events);
413
230
  }
414
231
  }
@@ -2,117 +2,42 @@
2
2
  * OpencodeAcpTranslator: translates opencode ACP session/update notifications
3
3
  * and session/request_permission server requests into AgentEvents.
4
4
  *
5
- * Channel choice (event contract in common/acp/protocol-types.ts header):
6
- * opencode's wire `content.text` IS an incremental delta
7
- * (opencode acp/event.ts:231-258 handlePartDelta sends `props.delta`
8
- * verbatim), so text/thinking chunks go straight onto the `assistant`
9
- * incremental channel — NO accumulation in the translator, NO turn_diff
10
- * snapshot. (Contrast kimi: same field is also a delta but kimi's contract
11
- * accumulates into turn_diff snapshots; both end with exactly one
12
- * concatenation in run-state, enforced by each side's seam test.)
5
+ * Extends BaseAcpTranslator (shared envelope dispatch, prompt-response
6
+ * stopReason mapping, tool_call normalization, usage occupancy). opencode's
7
+ * divergence from kimi is the content channel: wire `content.text` IS an
8
+ * incremental delta (opencode acp/event.ts:231-258 handlePartDelta sends
9
+ * `props.delta` verbatim), so text/thinking chunks go straight onto the
10
+ * `assistant` incremental channel — NO accumulation, NO turn_diff snapshot.
11
+ * (Contrast kimi: same field is also a delta but kimi's contract accumulates
12
+ * into turn_diff snapshots; both end with exactly one concatenation in
13
+ * run-state, enforced by each side's seam test.)
13
14
  *
14
15
  * Event mapping (source: opencode acp/service.ts + event.ts + tool.ts,
15
16
  * dev@1c965451b5):
16
17
  * agent_message_chunk → assistant/text delta (content.text passthrough)
17
18
  * agent_thought_chunk → assistant/thinking delta (content.text passthrough)
18
- * tool_call → assistant/tool_use (rawInput object passthrough;
19
- * missing → {} per contract, string → JSON.parse)
20
- * tool_call_update → user/tool_result (status:'failed' → is_error:true;
21
- * rawOutput is an OBJECT: {output} / {error})
22
- * usage_update → live context occupancy {used, size} + cost →
23
- * {total_tokens, context_limit, total_cost_usd}
24
- * available_commands_update / plan / session_info_update /
25
- * current_mode_update / config_option_update / user_message_chunk → discard
19
+ * tool_call / tool_call_update / usage_update / plan / control-plane noise →
20
+ * base class (BaseAcpTranslator; opencode overrides
21
+ * tool_result content extraction + session cost)
26
22
  * prompt stopReason:'end_turn' → result success
27
23
  * prompt stopReason:'cancelled' → result interrupted (独立终态)
28
24
  * prompt other stopReason/error → result error
29
25
  * session/request_permission → approval_requested (optionId echo)
30
26
  */
31
- import type { AgentEvent } from '../../types.js';
32
- import type { ApprovalRequestedEvent } from '../../types.js';
33
- /** Self-produced turn_started event (no wire equivalent — produced on prompt start). */
34
- export interface OpencodeAcpTurnStartedEvent {
35
- type: 'turn_started';
36
- threadId: string;
37
- turnId: string;
38
- operationKind: 'turn' | 'compaction';
39
- timestamp?: string;
40
- }
41
- /**
42
- * Live usage snapshot from usage_update. The wire carries context occupancy
43
- * (`used`), the model context window (`size`) and session cost
44
- * (`cost.amount`) — there is NO input/output token split.
45
- */
46
- export interface OpencodeAcpLiveUsage {
47
- total_tokens?: number;
48
- context_limit?: number;
49
- input_tokens?: number;
50
- output_tokens?: number;
51
- }
52
- /** Usage update event for live token display. */
53
- export interface OpencodeAcpUsageEvent {
54
- type: 'usage';
55
- usage: OpencodeAcpLiveUsage;
56
- timestamp?: string;
57
- }
58
- export type OpencodeAcpTranslatorEvent = AgentEvent | OpencodeAcpTurnStartedEvent | OpencodeAcpUsageEvent | ApprovalRequestedEvent;
59
- export declare class OpencodeAcpTranslator {
60
- /** Pending tool calls keyed by toolCallId (for matching tool_call_update). */
61
- private pendingToolCalls;
62
- /** Current operation kind (turn vs compact). */
63
- private operationKind;
64
- /** Live usage snapshot from usage_update events (context occupancy only). */
65
- private liveUsage;
66
- /** Session cost from usage_update cost.amount (USD). */
67
- private liveCostUsd?;
68
- /** Whether any usage_update has been seen this turn (no update → no usage). */
69
- private hasLiveUsage;
70
- /** Current turn id, set by produceTurnStarted. */
71
- private currentTurnId;
72
- /**
73
- * Handle a notification from the ACP server and return translated events.
74
- */
75
- handleNotification(method: string, params: unknown): OpencodeAcpTranslatorEvent[];
76
- /**
77
- * Handle a server request (reverse RPC from the ACP server).
78
- */
79
- handleServerRequest(id: number | string, method: string, params: unknown): OpencodeAcpTranslatorEvent[];
80
- /**
81
- * Produce a turn_started event. Called by the runner when session/prompt
82
- * is sent (no wire notification for this — self-produced).
83
- */
84
- produceTurnStarted(sessionId: string, turnId: string): OpencodeAcpTurnStartedEvent;
85
- /**
86
- * Translate a prompt response into a result event.
87
- * stopReason: end_turn → success; cancelled → interrupted; anything else
88
- * (max_tokens / refusal / …) → error.
89
- */
90
- handlePromptResponse(sessionId: string, result: {
91
- stopReason: string;
92
- }): AgentEvent;
93
- /**
94
- * Produce an error result event (for JSON-RPC errors during prompt).
95
- */
96
- produceErrorResult(sessionId: string, errorMessage: string): AgentEvent;
97
- /**
98
- * Set the operation kind (turn vs compact).
99
- */
100
- setOperationKind(kind: 'turn' | 'compact'): void;
101
- private handleAgentMessageChunk;
102
- private handleAgentThoughtChunk;
103
- private handleToolCall;
104
- private handleToolCallUpdate;
105
- private handleUsageUpdate;
106
- private handleRequestPermission;
107
- /**
108
- * Derive the approval decision list from the server's offered options.
109
- * accept/decline/cancel are universal; acceptForSession requires an
110
- * always-class option (opencode `allow_always`, kimi `approve_always`).
111
- */
112
- /**
113
- * ResultEvent.usage nominally requires input_tokens/output_tokens, but the
114
- * ACP wire has no such split — the live snapshot is context occupancy only.
115
- * Cast is deliberate: consumers must tolerate undefined input/output.
116
- */
117
- private liveUsageSnapshot;
27
+ import type { RequestPermissionParams, AgentMessageChunkEvent, AgentThoughtChunkEvent, ToolCallUpdateEvent, UsageUpdateEvent } from '../../common/acp/protocol-types.js';
28
+ import { BaseAcpTranslator, type AcpTranslatorEvent } from '../../common/acp/base-translator.js';
29
+ export type { AcpTurnStartedEvent as OpencodeAcpTurnStartedEvent, AcpLiveUsage as OpencodeAcpLiveUsage, AcpUsageEvent as OpencodeAcpUsageEvent, AcpTranslatorEvent as OpencodeAcpTranslatorEvent, } from '../../common/acp/base-translator.js';
30
+ export declare class OpencodeAcpTranslator extends BaseAcpTranslator {
31
+ protected readonly logTag = "opencode-acp-translator";
32
+ protected handleAgentMessageChunk(event: AgentMessageChunkEvent, _sessionId: string): AcpTranslatorEvent[];
33
+ protected handleAgentThoughtChunk(event: AgentThoughtChunkEvent, _sessionId: string): AcpTranslatorEvent[];
34
+ /**
35
+ * opencode rawOutput is an OBJECT (tool.ts): completed →
36
+ * {output, metadata?}; failed → {error, metadata}; in_progress →
37
+ * content[] with shell output snapshot.
38
+ */
39
+ protected extractToolResultContent(event: ToolCallUpdateEvent): unknown;
40
+ /** opencode usage_update also carries session cost: {cost:{amount}} (USD). */
41
+ protected onUsageSample(event: UsageUpdateEvent): void;
42
+ protected handleRequestPermission(requestId: number | string, params: RequestPermissionParams): AcpTranslatorEvent[];
118
43
  }