lark-remote 0.1.12 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.en.md +2 -6
  3. package/README.md +3 -7
  4. package/dist/bridge/final-usage.d.ts +24 -0
  5. package/dist/bridge/final-usage.js +55 -0
  6. package/dist/bridge/inbound-media.d.ts +0 -3
  7. package/dist/bridge/inbound-media.js +0 -1
  8. package/dist/bridge/index.d.ts +7 -7
  9. package/dist/bridge/index.js +100 -133
  10. package/dist/bridge/queue-manager.js +4 -3
  11. package/dist/card/approval-render.js +39 -94
  12. package/dist/card/bash-renderer.js +12 -21
  13. package/dist/card/card-shared.d.ts +14 -5
  14. package/dist/card/card-shared.js +23 -34
  15. package/dist/card/run-renderer.d.ts +0 -22
  16. package/dist/card/run-renderer.js +206 -153
  17. package/dist/card/run-state.d.ts +29 -1
  18. package/dist/card/run-state.js +172 -84
  19. package/dist/card/text-truncate.d.ts +0 -9
  20. package/dist/card/text-truncate.js +0 -9
  21. package/dist/card/tool-render.d.ts +4 -1
  22. package/dist/card/tool-render.js +76 -32
  23. package/dist/cli.js +2 -2
  24. package/dist/common/fs.d.ts +14 -0
  25. package/dist/common/fs.js +57 -3
  26. package/dist/common/truncate.d.ts +0 -6
  27. package/dist/common/truncate.js +1 -1
  28. package/dist/common/ttl-cache.d.ts +24 -0
  29. package/dist/common/ttl-cache.js +53 -0
  30. package/dist/config/codex-config.js +14 -11
  31. package/dist/config/index.d.ts +0 -8
  32. package/dist/config/index.js +0 -16
  33. package/dist/config/kimi-config.d.ts +1 -1
  34. package/dist/config/kimi-config.js +2 -1
  35. package/dist/config/opencode-config.d.ts +1 -1
  36. package/dist/config/opencode-config.js +16 -10
  37. package/dist/config/qr.d.ts +36 -0
  38. package/dist/config/qr.js +124 -0
  39. package/dist/config/wizard.js +69 -27
  40. package/dist/connector/index.d.ts +3 -2
  41. package/dist/connector/index.js +5 -3
  42. package/dist/index.js +14 -41
  43. package/dist/instance-lock.js +7 -0
  44. package/dist/persistence/atomic-write.d.ts +2 -0
  45. package/dist/persistence/atomic-write.js +20 -5
  46. package/dist/persistence/load-json-file.d.ts +1 -2
  47. package/dist/persistence/load-json-file.js +2 -3
  48. package/dist/platform/agent-stopper.d.ts +22 -0
  49. package/dist/platform/agent-stopper.js +19 -0
  50. package/dist/platform/command.d.ts +11 -0
  51. package/dist/platform/command.js +107 -0
  52. package/dist/platform/identity.d.ts +33 -0
  53. package/dist/platform/identity.js +187 -0
  54. package/dist/platform/path.d.ts +24 -0
  55. package/dist/platform/path.js +110 -0
  56. package/dist/platform/select.d.ts +9 -0
  57. package/dist/platform/select.js +12 -0
  58. package/dist/platform/shell.d.ts +51 -0
  59. package/dist/platform/shell.js +75 -0
  60. package/dist/platform/spawn.d.ts +41 -0
  61. package/dist/platform/spawn.js +77 -0
  62. package/dist/platform/terminator-posix.d.ts +38 -0
  63. package/dist/platform/terminator-posix.js +103 -0
  64. package/dist/platform/terminator-win32.d.ts +21 -0
  65. package/dist/platform/terminator-win32.js +110 -0
  66. package/dist/platform/terminator.d.ts +44 -0
  67. package/dist/platform/terminator.js +24 -0
  68. package/dist/platform/types.d.ts +23 -0
  69. package/dist/platform/types.js +5 -0
  70. package/dist/restart.js +4 -2
  71. package/dist/router/card-helpers.d.ts +33 -6
  72. package/dist/router/card-helpers.js +67 -11
  73. package/dist/router/config/common/render.d.ts +1 -1
  74. package/dist/router/config/common/render.js +1 -1
  75. package/dist/router/config-diff.d.ts +4 -0
  76. package/dist/router/config-diff.js +70 -0
  77. package/dist/router/index.d.ts +73 -6
  78. package/dist/router/index.js +663 -590
  79. package/dist/router/utils.d.ts +6 -0
  80. package/dist/router/utils.js +12 -0
  81. package/dist/runner/bash/runner.d.ts +9 -2
  82. package/dist/runner/bash/runner.js +45 -20
  83. package/dist/runner/claude/session.d.ts +4 -4
  84. package/dist/runner/claude/session.js +18 -9
  85. package/dist/runner/codex/app-server/protocol-types.d.ts +35 -1
  86. package/dist/runner/codex/app-server/runner.d.ts +1 -16
  87. package/dist/runner/codex/app-server/runner.js +2 -2
  88. package/dist/runner/codex/app-server/translator.d.ts +18 -0
  89. package/dist/runner/codex/app-server/translator.js +146 -0
  90. package/dist/runner/common/acp/base-acp-runner.d.ts +116 -0
  91. package/dist/runner/common/acp/base-acp-runner.js +248 -0
  92. package/dist/runner/common/acp/base-translator.d.ts +141 -0
  93. package/dist/runner/common/acp/base-translator.js +271 -0
  94. package/dist/runner/common/acp/protocol-helpers.d.ts +70 -0
  95. package/dist/runner/common/acp/protocol-helpers.js +82 -0
  96. package/dist/runner/common/connection-based-runner.d.ts +0 -2
  97. package/dist/runner/common/connection-based-runner.js +1 -1
  98. package/dist/runner/common/jsonrpc/transport.d.ts +2 -0
  99. package/dist/runner/common/jsonrpc/transport.js +25 -5
  100. package/dist/runner/common/process-stopper.d.ts +4 -26
  101. package/dist/runner/common/process-stopper.js +4 -76
  102. package/dist/runner/common/runner-utils.d.ts +0 -5
  103. package/dist/runner/common/runner-utils.js +0 -7
  104. package/dist/runner/common/spawning-runner.d.ts +42 -132
  105. package/dist/runner/common/spawning-runner.js +52 -271
  106. package/dist/runner/common/usage.d.ts +2 -0
  107. package/dist/runner/common/usage.js +2 -0
  108. package/dist/runner/dsh/index.d.ts +0 -2
  109. package/dist/runner/dsh/index.js +0 -2
  110. package/dist/runner/dsh/runner.d.ts +0 -2
  111. package/dist/runner/dsh/runner.js +1 -1
  112. package/dist/runner/index.d.ts +1 -1
  113. package/dist/runner/kimi/acp/runner.d.ts +63 -53
  114. package/dist/runner/kimi/acp/runner.js +189 -334
  115. package/dist/runner/kimi/acp/translator.d.ts +18 -101
  116. package/dist/runner/kimi/acp/translator.js +61 -255
  117. package/dist/runner/opencode/acp/runner.d.ts +22 -46
  118. package/dist/runner/opencode/acp/runner.js +74 -297
  119. package/dist/runner/opencode/acp/translator.d.ts +28 -104
  120. package/dist/runner/opencode/acp/translator.js +54 -249
  121. package/dist/runner/pi/rpc/index.d.ts +0 -2
  122. package/dist/runner/pi/rpc/index.js +0 -2
  123. package/dist/runner/pi/rpc/translator.d.ts +3 -0
  124. package/dist/runner/pi/rpc/translator.js +47 -9
  125. package/dist/runner/probe.d.ts +8 -11
  126. package/dist/runner/probe.js +17 -21
  127. package/dist/runner/registry.d.ts +4 -4
  128. package/dist/runner/registry.js +4 -4
  129. package/dist/runner/resolve-agent-choices.js +6 -2
  130. package/dist/runner/sync-agent-choices.js +1 -1
  131. package/dist/runner/types.d.ts +38 -1
  132. package/dist/session/claude/session-index.js +3 -1
  133. package/dist/session/claude/sessions.js +37 -64
  134. package/dist/session/codex/rollout-reader.d.ts +10 -14
  135. package/dist/session/codex/rollout-reader.js +126 -48
  136. package/dist/session/common/constants.d.ts +7 -0
  137. package/dist/session/common/constants.js +9 -0
  138. package/dist/session/common/pagination.d.ts +0 -2
  139. package/dist/session/common/pagination.js +1 -1
  140. package/dist/session/common/recency.d.ts +7 -0
  141. package/dist/session/common/recency.js +10 -0
  142. package/dist/session/common/two-pass.d.ts +22 -0
  143. package/dist/session/common/two-pass.js +70 -0
  144. package/dist/session/common/usage-accumulator.d.ts +14 -0
  145. package/dist/session/common/usage-accumulator.js +15 -0
  146. package/dist/session/dsh/sessions.js +15 -11
  147. package/dist/session/kimi/sessions.d.ts +24 -4
  148. package/dist/session/kimi/sessions.js +99 -103
  149. package/dist/session/opencode/sessions.d.ts +2 -14
  150. package/dist/session/opencode/sessions.js +35 -15
  151. package/dist/session/pi/sessions.js +64 -95
  152. package/dist/update/index.d.ts +2 -3
  153. package/dist/update/index.js +1 -2
  154. package/dist/update/install.d.ts +2 -1
  155. package/dist/update/install.js +58 -9
  156. package/dist/update/version-check.d.ts +5 -0
  157. package/dist/update/version-check.js +9 -0
  158. package/package.json +9 -7
  159. package/dist/update/startup-hint.d.ts +0 -5
  160. 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,62 +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
- import { findOptionIdByKind } from '../../common/acp/protocol-helpers.js';
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
- /**
30
- * Build the ACP protocol response for a permission approval decision.
31
- *
32
- * accept → {outcome:{outcome:'selected', optionId:<allow_once kind>}}
33
- * accept_for_session → {outcome:{outcome:'selected', optionId:<allow_always kind>}}
34
- * decline → {outcome:{outcome:'selected', optionId:<reject kind>}}
35
- * cancel → {outcome:{outcome:'cancelled'}}
36
- *
37
- * optionId is opaque and echoed back as-is (opencode permission.ts:20-24
38
- * offers once/always/reject with kinds allow_once/allow_always/reject_once;
39
- * the server maps optionId 'once'/'always' → approve, anything else →
40
- * reject — permission.ts:219-223). If no matching option is found, fall
41
- * back to cancelled (safe universal default).
42
- */
43
- function buildApprovalResponse(action, pending) {
44
- if (action === 'cancel') {
45
- return { outcome: { outcome: 'cancelled' } };
46
- }
47
- if (action === 'accept') {
48
- const optionId = findOptionIdByKind(pending.options, ['allow_once', 'allow_always']);
49
- if (optionId) {
50
- return { outcome: { outcome: 'selected', optionId } };
51
- }
52
- return { outcome: { outcome: 'cancelled' } };
53
- }
54
- if (action === 'accept_for_session') {
55
- // §P4: 卡片「允许本次会话」→ always 类 optionId(opencode 'always')
56
- const optionId = findOptionIdByKind(pending.options, ['allow_always', 'approve_always']);
57
- if (optionId) {
58
- return { outcome: { outcome: 'selected', optionId } };
59
- }
60
- return { outcome: { outcome: 'cancelled' } };
61
- }
62
- // decline
63
- const optionId = findOptionIdByKind(pending.options, ['reject_once', 'reject']);
64
- if (optionId) {
65
- return { outcome: { outcome: 'selected', optionId } };
66
- }
67
- return { outcome: { outcome: 'cancelled' } };
68
- }
69
29
  // =============================================================================
70
30
  // Runner
71
31
  // =============================================================================
72
- export class OpencodeAcpRunner extends ConnectionBasedRunner {
73
- connectionManager;
74
- currentTranslator = null;
75
- activeSessionId = null;
76
- model;
32
+ export class OpencodeAcpRunner extends BaseAcpRunner {
77
33
  /** Configured session mode applied via session/set_mode (§P5). */
78
34
  configuredMode;
79
35
  /** Current session mode id, parsed from session/new|resume configOptions. */
@@ -83,22 +39,7 @@ export class OpencodeAcpRunner extends ConnectionBasedRunner {
83
39
  /** Tails opencode's own log to surface LLM stream errors the ACP protocol
84
40
  * never forwards (quota/limit errors back off for ~1h → turn would hang). */
85
41
  errorMonitor;
86
- /** Pending approval requests: requestId → kind + view + options. */
87
- pendingApprovals = new Map();
88
- /** Tracker for the in-flight prompt request — needed for cancellation. */
89
- promptSettled = false;
90
- /** Whether the prompt request has been fired (distinguishes "stop before
91
- * prompt sent" from "stop while prompt in-flight" — the latter sends
92
- * session/cancel immediately). */
93
- promptSent = false;
94
42
  constructor(opts) {
95
- super({ kind: opts.kind, sessionReader: opts.sessionReader }, opts.turnIdleTimeoutMs);
96
- this.model = opts.model;
97
- this.configuredMode = opts.mode ?? 'build';
98
- this.errorMonitor = new OpencodeLogErrorMonitor({
99
- logPath: opts.errorMonitorLogPath ?? resolveOpencodeLogPath(),
100
- pollIntervalMs: opts.errorMonitorPollIntervalMs,
101
- });
102
43
  const managerOpts = {
103
44
  binary: opts.binary ?? 'opencode',
104
45
  args: opts.acpArgs ?? ['acp'],
@@ -116,7 +57,17 @@ export class OpencodeAcpRunner extends ConnectionBasedRunner {
116
57
  },
117
58
  },
118
59
  };
119
- 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
+ });
120
71
  }
121
72
  get logTag() {
122
73
  return 'opencode-acp-runner';
@@ -127,31 +78,12 @@ export class OpencodeAcpRunner extends ConnectionBasedRunner {
127
78
  get turnInterruptedErrorMessage() {
128
79
  return 'opencode ACP turn interrupted';
129
80
  }
130
- currentSessionId() {
131
- return this.activeSessionId;
81
+ get connectionClosedMessage() {
82
+ return 'opencode ACP connection closed';
132
83
  }
133
84
  shouldDeferStop() {
134
85
  return !this.promptSent;
135
86
  }
136
- async cancelCurrentTurn() {
137
- if (!this.currentClient || !this.activeSessionId)
138
- return;
139
- try {
140
- // session/cancel is a NOTIFICATION (no id, no response) per ACP spec.
141
- const cancelParams = { sessionId: this.activeSessionId };
142
- this.currentClient.notify('session/cancel', cancelParams);
143
- }
144
- catch (err) {
145
- getLogger().warn(`[${this.logTag}] session/cancel failed: ${err.message}`);
146
- }
147
- }
148
- clearTurnState() {
149
- this.errorMonitor.stop();
150
- this.currentTranslator = null;
151
- this.promptSettled = false;
152
- this.promptSent = false;
153
- this.activeSessionId = null;
154
- }
155
87
  /**
156
88
  * Start watching opencode's own log for LLM stream errors of the active
157
89
  * session. opencode ACP does not surface retry/error state to the client
@@ -167,175 +99,10 @@ export class OpencodeAcpRunner extends ConnectionBasedRunner {
167
99
  this.failTurn(`opencode LLM stream error: ${message}`);
168
100
  });
169
101
  }
170
- async releaseConnection(cwd) {
171
- await this.connectionManager.release(cwd);
172
- }
173
- notifyIdle(cwd) {
174
- this.connectionManager.notifyIdle(cwd);
175
- }
176
- async disposeConnections() {
177
- await this.connectionManager.disposeAll();
178
- }
179
- /**
180
- * Run a compact operation on the current session.
181
- *
182
- * opencode special-cases the `/compact` text prompt server-side
183
- * (service.ts:555-571): it runs session.summarize synchronously and the
184
- * prompt response settles only after compaction completes — no background
185
- * record polling needed (unlike kimi).
186
- */
187
- async *runCompact(_message, opts) {
188
- yield* this.executeTurn(opts, async () => {
189
- const client = await this.connectionManager.acquire(opts.cwd);
190
- this.connectionManager.notifyActivity(opts.cwd);
191
- this.currentClient = client;
192
- client.setHooks({
193
- onNotification: (method, params) => this.handleNotification(method, params),
194
- onServerRequest: (id, method, params) => this.handleServerRequest(id, method, params),
195
- onClose: () => this.failTurn('opencode ACP connection closed'),
196
- });
197
- const sessionId = opts.sessionId;
198
- if (!sessionId) {
199
- throw new Error('compact requires a sessionId');
200
- }
201
- this.activeSessionId = sessionId;
202
- this.startErrorMonitor(sessionId);
203
- // Cold-connection fallback: session/resume loads the session into
204
- // memory. On a reused connection the session is already loaded —
205
- // resume is idempotent and harmless.
206
- const resumeParams = { sessionId, cwd: opts.cwd };
207
- const resumeResult = await client.request('session/resume', resumeParams);
208
- this.trackConfigOptions(resumeResult);
209
- const translator = new OpencodeAcpTranslator();
210
- translator.setOperationKind('compact');
211
- this.currentTranslator = translator;
212
- // Emit turn_started with operationKind='compaction' before the prompt.
213
- const turnId = `compact-${Date.now()}`;
214
- const turnStarted = translator.produceTurnStarted(sessionId, turnId);
215
- this.currentTurnId = turnId;
216
- this.pushEvents([turnStarted]);
217
- // Send /compact as prompt text (opencode intercepts it server-side).
218
- const promptParams = {
219
- sessionId,
220
- prompt: [{ type: 'text', text: '/compact' }],
221
- };
222
- this.promptSettled = false;
223
- // ACP holds the session/prompt response for the ENTIRE turn, so the
224
- // RPC-level timeout must not apply — turn liveness is guarded by the
225
- // rolling idle watchdog instead.
226
- const promptPromise = client.request('session/prompt', promptParams, Number.POSITIVE_INFINITY);
227
- this.promptSent = true;
228
- promptPromise.then((result) => {
229
- this.promptSettled = true;
230
- if (this.stopRequested)
231
- return; // already cancelled
232
- const resultEvent = translator.handlePromptResponse(sessionId, result);
233
- this.pushEvents([resultEvent]);
234
- }, (err) => {
235
- this.promptSettled = true;
236
- if (this.stopRequested) {
237
- getLogger().warn(`[${this.logTag}] compact prompt rejected after stopRequested: ${err.message}`);
238
- return;
239
- }
240
- const resultEvent = translator.produceErrorResult(sessionId, err.message);
241
- this.pushEvents([resultEvent]);
242
- });
243
- });
244
- }
245
- /**
246
- * Respond to an approval server request. `response` is the bridge
247
- * ApprovalAction (`{ action: 'accept' | 'decline' | 'cancel' }`).
248
- */
249
- async respondApproval(requestId, response) {
250
- const client = this.currentClient;
251
- const pending = this.pendingApprovals.get(requestId);
252
- if (!client || !pending)
253
- return;
254
- const action = response?.action ?? 'decline';
255
- const acpResponse = buildApprovalResponse(action, pending);
256
- client.respond(requestId, acpResponse);
257
- this.pendingApprovals.delete(requestId);
258
- getLogger().info(`[${this.logTag}] approval responded requestId=${requestId} action=${action}`);
259
- }
260
- getStatusInfo() {
261
- return {
262
- kind: this.kind,
263
- model: this.model ?? '(opencode-acp)',
264
- extras: {
265
- mode: 'acp',
266
- ...(this.currentModeId ? { sessionMode: this.currentModeId } : {}),
267
- },
268
- };
269
- }
270
- /**
271
- * Hot-apply a mode change to the live ACP session (§P5).
272
- *
273
- * opencode's session/set_mode sends no notification — the client refreshes
274
- * the local mode view itself. The local mode is always updated so
275
- * `getStatusInfo()` (and therefore `/s`) reflects the new value
276
- * immediately; when a session is connected, also re-sends session/set_mode
277
- * so the running session picks up the new mode without waiting for the
278
- * runner to be evicted/recreated. Failure is non-fatal: the next setupTurn
279
- * re-applies the cached mode when it differs from the session's current
280
- * mode.
281
- */
282
- async updateApprovalMode(settings) {
283
- if (settings.mode !== undefined) {
284
- this.configuredMode = settings.mode;
285
- this.currentModeId = settings.mode;
286
- }
287
- if (!this.currentClient || !this.activeSessionId)
288
- return;
289
- const modeParams = {
290
- sessionId: this.activeSessionId,
291
- modeId: this.configuredMode,
292
- };
293
- try {
294
- await this.currentClient.request('session/set_mode', modeParams);
295
- getLogger().info(`[${this.logTag}] session/set_mode hot-applied session=${this.activeSessionId} modeId=${modeParams.modeId}`);
296
- }
297
- catch (err) {
298
- getLogger().warn(`[${this.logTag}] session/set_mode failed (non-fatal): ${err.message}`);
299
- }
102
+ createTranslator() {
103
+ return new OpencodeAcpTranslator();
300
104
  }
301
- // =========================================================================
302
- // Internal
303
- // =========================================================================
304
- /**
305
- * Acquire the connection and set up the turn:
306
- * session/new (new session) or session/resume (existing sessionId)
307
- * → session/prompt.
308
- */
309
- async setupTurn(message, opts) {
310
- const client = await this.connectionManager.acquire(opts.cwd);
311
- this.connectionManager.notifyActivity(opts.cwd);
312
- this.currentClient = client;
313
- client.setHooks({
314
- onNotification: (method, params) => this.handleNotification(method, params),
315
- onServerRequest: (id, method, params) => this.handleServerRequest(id, method, params),
316
- onClose: () => {
317
- getLogger().warn(`[${this.logTag}] client connection closed`);
318
- this.failTurn('opencode ACP connection closed');
319
- },
320
- });
321
- let sessionId;
322
- if (opts.sessionId) {
323
- const resumeParams = {
324
- sessionId: opts.sessionId,
325
- cwd: opts.cwd,
326
- };
327
- const resumeResult = await client.request('session/resume', resumeParams);
328
- this.trackConfigOptions(resumeResult);
329
- sessionId = opts.sessionId;
330
- }
331
- else {
332
- const newParams = { cwd: opts.cwd, mcpServers: [] };
333
- const newResult = await client.request('session/new', newParams);
334
- this.trackConfigOptions(newResult);
335
- sessionId = newResult.sessionId;
336
- }
337
- this.activeSessionId = sessionId;
338
- this.startErrorMonitor(sessionId);
105
+ async applyTurnSettings(client, sessionId) {
339
106
  // §P5: apply the configured mode (build/plan). If the session already
340
107
  // runs it (from session/new|resume configOptions), skip the wire call;
341
108
  // otherwise send session/set_mode and refresh the local view (opencode
@@ -371,39 +138,49 @@ export class OpencodeAcpRunner extends ConnectionBasedRunner {
371
138
  getLogger().warn(`[${this.logTag}] set_config_option model failed (non-fatal): ${err.message}`);
372
139
  }
373
140
  }
374
- const translator = new OpencodeAcpTranslator();
375
- this.currentTranslator = translator;
376
- // Emit turn_started before prompt
377
- const turnId = `turn-${Date.now()}`;
378
- const turnStarted = translator.produceTurnStarted(sessionId, turnId);
379
- this.currentTurnId = turnId;
380
- this.pushEvents([turnStarted]);
381
- // Send the prompt. The prompt request races with notifications (the
382
- // server streams session/update before the prompt response); the
383
- // response settles the turn and is translated into the result event.
384
- const promptParams = {
385
- sessionId,
386
- 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
+ },
387
160
  };
388
- this.promptSettled = false;
389
- const promptPromise = client.request('session/prompt', promptParams, Number.POSITIVE_INFINITY);
390
- this.promptSent = true;
391
- promptPromise.then((result) => {
392
- this.promptSettled = true;
393
- if (this.stopRequested)
394
- return; // already cancelled
395
- const resultEvent = translator.handlePromptResponse(sessionId, result);
396
- this.pushEvents([resultEvent]);
397
- }, (err) => {
398
- this.promptSettled = true;
399
- if (this.stopRequested) {
400
- getLogger().warn(`[${this.logTag}] prompt rejected after stopRequested: ${err.message}`);
401
- return;
402
- }
403
- const resultEvent = translator.produceErrorResult(sessionId, err.message);
404
- this.pushEvents([resultEvent]);
405
- });
406
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
+ // =========================================================================
407
184
  /**
408
185
  * Track the current session mode and model from a session/new|resume
409
186
  * result's configOptions (opencode buildModeSelectOption/buildModelSelectOption:
@@ -422,6 +199,15 @@ export class OpencodeAcpRunner extends ConnectionBasedRunner {
422
199
  this.currentModelValue = model.currentValue;
423
200
  }
424
201
  }
202
+ onTurnSessionReady(sessionId) {
203
+ this.startErrorMonitor(sessionId);
204
+ }
205
+ onSessionEstablished(result) {
206
+ this.trackConfigOptions(result);
207
+ }
208
+ onClearTurnState() {
209
+ this.errorMonitor.stop();
210
+ }
425
211
  handleNotification(method, params) {
426
212
  const events = this.currentTranslator?.handleNotification(method, params) ?? [];
427
213
  this.pushEvents(events);
@@ -439,16 +225,7 @@ export class OpencodeAcpRunner extends ConnectionBasedRunner {
439
225
  getLogger().info(`[${this.logTag}] rejected unsupported server request method=${method} id=${id}`);
440
226
  return;
441
227
  }
442
- for (const ev of events) {
443
- if (ev.type === 'approval_requested') {
444
- this.pendingApprovals.set(ev.requestId, {
445
- kind: ev.kind,
446
- view: ev.view,
447
- options: params.options,
448
- });
449
- getLogger().info(`[${this.logTag}] approval requested requestId=${ev.requestId} kind=${ev.kind}`);
450
- }
451
- }
228
+ this.registerApprovalEvents(events, params.options);
452
229
  this.pushEvents(events);
453
230
  }
454
231
  }
@@ -2,118 +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
- private deriveAvailableDecisions;
113
- /**
114
- * ResultEvent.usage nominally requires input_tokens/output_tokens, but the
115
- * ACP wire has no such split — the live snapshot is context occupancy only.
116
- * Cast is deliberate: consumers must tolerate undefined input/output.
117
- */
118
- 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[];
119
43
  }