lark-remote 0.1.5 → 0.1.7

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 (190) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.en.md +5 -1
  3. package/README.md +10 -2
  4. package/dist/bridge/approval-coordinator.d.ts +131 -0
  5. package/dist/bridge/approval-coordinator.js +409 -0
  6. package/dist/bridge/inbound-media.d.ts +62 -0
  7. package/dist/bridge/inbound-media.js +282 -0
  8. package/dist/bridge/index.d.ts +138 -5
  9. package/dist/bridge/index.js +569 -65
  10. package/dist/bridge/queue-manager.d.ts +11 -1
  11. package/dist/bridge/queue-manager.js +20 -11
  12. package/dist/card/approval-render.d.ts +16 -0
  13. package/dist/card/approval-render.js +363 -0
  14. package/dist/card/bash-card-session.d.ts +2 -24
  15. package/dist/card/bash-card-session.js +6 -73
  16. package/dist/card/bash-renderer.js +2 -3
  17. package/dist/card/card-budget.js +29 -75
  18. package/dist/card/card-session.d.ts +48 -0
  19. package/dist/card/card-session.js +86 -0
  20. package/dist/card/card-shared.d.ts +13 -10
  21. package/dist/card/card-shared.js +27 -24
  22. package/dist/card/run-card-session.d.ts +2 -55
  23. package/dist/card/run-card-session.js +6 -104
  24. package/dist/card/run-renderer.js +140 -202
  25. package/dist/card/run-state.d.ts +38 -3
  26. package/dist/card/run-state.js +221 -11
  27. package/dist/card/text-truncate.d.ts +13 -12
  28. package/dist/card/text-truncate.js +13 -63
  29. package/dist/common/fs.d.ts +8 -0
  30. package/dist/common/fs.js +17 -0
  31. package/dist/common/guards.d.ts +0 -20
  32. package/dist/common/guards.js +0 -43
  33. package/dist/common/sleep.d.ts +2 -0
  34. package/dist/common/sleep.js +5 -0
  35. package/dist/common/truncate.d.ts +39 -0
  36. package/dist/common/truncate.js +92 -0
  37. package/dist/config/codex-config.d.ts +1 -1
  38. package/dist/config/codex-config.js +1 -1
  39. package/dist/config/dir.js +1 -1
  40. package/dist/config/index.d.ts +116 -9
  41. package/dist/config/index.js +122 -29
  42. package/dist/connector/index.d.ts +74 -2
  43. package/dist/connector/index.js +119 -1
  44. package/dist/index.js +139 -99
  45. package/dist/instance-lock.js +4 -12
  46. package/dist/order/alias-resolve.d.ts +13 -0
  47. package/dist/order/alias-resolve.js +22 -0
  48. package/dist/order/alias-store.d.ts +38 -0
  49. package/dist/order/alias-store.js +106 -0
  50. package/dist/persistence/atomic-write.d.ts +8 -2
  51. package/dist/persistence/atomic-write.js +23 -7
  52. package/dist/restart.js +2 -1
  53. package/dist/router/card-helpers.d.ts +63 -0
  54. package/dist/router/card-helpers.js +146 -0
  55. package/dist/router/config/claude.js +25 -1
  56. package/dist/router/config/codex.d.ts +1 -1
  57. package/dist/router/config/codex.js +33 -5
  58. package/dist/router/config/common/model-patch.d.ts +22 -0
  59. package/dist/router/config/common/model-patch.js +26 -0
  60. package/dist/router/config/common/render.js +9 -4
  61. package/dist/router/config/index.d.ts +12 -0
  62. package/dist/router/config/index.js +25 -1
  63. package/dist/router/config/kimi.d.ts +4 -1
  64. package/dist/router/config/kimi.js +27 -0
  65. package/dist/router/config/opencode.js +18 -5
  66. package/dist/router/config/pi.js +4 -5
  67. package/dist/router/config/types.d.ts +12 -3
  68. package/dist/router/index.d.ts +77 -17
  69. package/dist/router/index.js +455 -549
  70. package/dist/router/utils.d.ts +4 -0
  71. package/dist/router/utils.js +9 -2
  72. package/dist/runner/agent-choices-common.d.ts +16 -0
  73. package/dist/runner/agent-choices-common.js +31 -0
  74. package/dist/runner/claude/runner.d.ts +47 -10
  75. package/dist/runner/claude/runner.js +117 -46
  76. package/dist/runner/claude/session.d.ts +166 -0
  77. package/dist/runner/claude/session.js +749 -0
  78. package/dist/runner/codex/app-server/protocol-types.d.ts +519 -0
  79. package/dist/runner/codex/app-server/protocol-types.js +56 -0
  80. package/dist/runner/codex/app-server/runner.d.ts +104 -0
  81. package/dist/runner/codex/app-server/runner.js +377 -0
  82. package/dist/runner/codex/app-server/translator.d.ts +89 -0
  83. package/dist/runner/codex/app-server/translator.js +716 -0
  84. package/dist/runner/codex/index.d.ts +1 -1
  85. package/dist/runner/codex/index.js +1 -1
  86. package/dist/runner/common/acp/protocol-helpers.d.ts +14 -0
  87. package/dist/runner/common/acp/protocol-helpers.js +21 -0
  88. package/dist/runner/common/acp/protocol-types.d.ts +238 -0
  89. package/dist/runner/common/acp/protocol-types.js +75 -0
  90. package/dist/runner/common/connection-based-runner.d.ts +111 -0
  91. package/dist/runner/common/connection-based-runner.js +261 -0
  92. package/dist/runner/common/jsonrpc/client.d.ts +89 -0
  93. package/dist/runner/common/jsonrpc/client.js +243 -0
  94. package/dist/runner/common/jsonrpc/connection-manager.d.ts +125 -0
  95. package/dist/runner/common/jsonrpc/connection-manager.js +205 -0
  96. package/dist/runner/common/jsonrpc/transport.d.ts +50 -0
  97. package/dist/runner/common/jsonrpc/transport.js +230 -0
  98. package/dist/runner/common/jsonrpc/types.d.ts +32 -0
  99. package/dist/runner/common/jsonrpc/types.js +9 -0
  100. package/dist/runner/common/process-stopper.d.ts +2 -1
  101. package/dist/runner/common/process-stopper.js +1 -0
  102. package/dist/runner/common/runner-utils.d.ts +6 -13
  103. package/dist/runner/common/runner-utils.js +6 -47
  104. package/dist/runner/common/spawning-runner.d.ts +24 -70
  105. package/dist/runner/common/spawning-runner.js +98 -152
  106. package/dist/runner/common/usage.d.ts +2 -0
  107. package/dist/runner/common/usage.js +2 -0
  108. package/dist/runner/index.d.ts +5 -5
  109. package/dist/runner/index.js +4 -4
  110. package/dist/runner/kimi/acp/runner.d.ts +106 -0
  111. package/dist/runner/kimi/acp/runner.js +448 -0
  112. package/dist/runner/kimi/acp/translator.d.ts +124 -0
  113. package/dist/runner/kimi/acp/translator.js +336 -0
  114. package/dist/runner/kimi/index.d.ts +1 -1
  115. package/dist/runner/kimi/index.js +1 -1
  116. package/dist/runner/opencode/acp/index.d.ts +1 -0
  117. package/dist/runner/opencode/acp/index.js +2 -0
  118. package/dist/runner/opencode/acp/runner.d.ts +121 -0
  119. package/dist/runner/opencode/acp/runner.js +429 -0
  120. package/dist/runner/opencode/acp/translator.d.ts +119 -0
  121. package/dist/runner/opencode/acp/translator.js +345 -0
  122. package/dist/runner/opencode/index.d.ts +1 -1
  123. package/dist/runner/opencode/index.js +1 -1
  124. package/dist/runner/pi/index.d.ts +1 -1
  125. package/dist/runner/pi/index.js +1 -1
  126. package/dist/runner/pi/rpc/client.d.ts +52 -0
  127. package/dist/runner/pi/rpc/client.js +114 -0
  128. package/dist/runner/pi/rpc/index.d.ts +4 -0
  129. package/dist/runner/pi/rpc/index.js +4 -0
  130. package/dist/runner/pi/rpc/protocol-types.d.ts +127 -0
  131. package/dist/runner/pi/rpc/protocol-types.js +21 -0
  132. package/dist/runner/pi/rpc/runner.d.ts +80 -0
  133. package/dist/runner/pi/rpc/runner.js +199 -0
  134. package/dist/runner/pi/rpc/translator.d.ts +55 -0
  135. package/dist/runner/pi/rpc/translator.js +258 -0
  136. package/dist/runner/registry.d.ts +7 -8
  137. package/dist/runner/registry.js +3 -4
  138. package/dist/runner/resolve-agent-choices.js +18 -59
  139. package/dist/runner/sync-agent-choices.js +16 -67
  140. package/dist/runner/types.d.ts +153 -2
  141. package/dist/session/claude/session-index.js +2 -3
  142. package/dist/session/claude/session-reader.js +3 -6
  143. package/dist/session/claude/sessions.js +6 -13
  144. package/dist/session/codex/rollout-reader.d.ts +2 -2
  145. package/dist/session/codex/rollout-reader.js +57 -17
  146. package/dist/session/codex/sessions.d.ts +1 -1
  147. package/dist/session/codex/sessions.js +2 -2
  148. package/dist/session/common/content-blocks.d.ts +7 -0
  149. package/dist/session/common/content-blocks.js +13 -9
  150. package/dist/session/common/jsonl.d.ts +8 -25
  151. package/dist/session/common/jsonl.js +126 -261
  152. package/dist/session/common/pagination.d.ts +28 -0
  153. package/dist/session/common/pagination.js +33 -0
  154. package/dist/session/kimi/sessions.d.ts +34 -1
  155. package/dist/session/kimi/sessions.js +208 -98
  156. package/dist/session/opencode/sessions.d.ts +17 -3
  157. package/dist/session/opencode/sessions.js +29 -29
  158. package/dist/session/pi/sessions.d.ts +5 -1
  159. package/dist/session/pi/sessions.js +12 -16
  160. package/dist/session/session-store.d.ts +11 -1
  161. package/dist/session/session-store.js +75 -178
  162. package/dist/update/index.d.ts +2 -2
  163. package/dist/update/index.js +2 -2
  164. package/dist/update/install.d.ts +20 -2
  165. package/dist/update/install.js +51 -7
  166. package/package.json +1 -1
  167. package/dist/runner/codex/argv.d.ts +0 -33
  168. package/dist/runner/codex/argv.js +0 -40
  169. package/dist/runner/codex/jsonl.d.ts +0 -37
  170. package/dist/runner/codex/jsonl.js +0 -221
  171. package/dist/runner/codex/runner.d.ts +0 -63
  172. package/dist/runner/codex/runner.js +0 -124
  173. package/dist/runner/common/exec-translator.d.ts +0 -73
  174. package/dist/runner/common/exec-translator.js +0 -105
  175. package/dist/runner/kimi/jsonl.d.ts +0 -28
  176. package/dist/runner/kimi/jsonl.js +0 -143
  177. package/dist/runner/kimi/runner.d.ts +0 -49
  178. package/dist/runner/kimi/runner.js +0 -105
  179. package/dist/runner/opencode/argv.d.ts +0 -32
  180. package/dist/runner/opencode/argv.js +0 -35
  181. package/dist/runner/opencode/jsonl.d.ts +0 -68
  182. package/dist/runner/opencode/jsonl.js +0 -295
  183. package/dist/runner/opencode/runner.d.ts +0 -61
  184. package/dist/runner/opencode/runner.js +0 -93
  185. package/dist/runner/pi/jsonl.d.ts +0 -128
  186. package/dist/runner/pi/jsonl.js +0 -249
  187. package/dist/runner/pi/runner.d.ts +0 -36
  188. package/dist/runner/pi/runner.js +0 -76
  189. package/dist/session/common/catch-up.d.ts +0 -12
  190. package/dist/session/common/catch-up.js +0 -31
@@ -0,0 +1,261 @@
1
+ import { ConnectionLostError } from './jsonrpc/client.js';
2
+ import { getLogger } from '../../logger/index.js';
3
+ import { syntheticInitEvent } from './runner-utils.js';
4
+ /** How long to wait for turn output notifications before failing. */
5
+ export const TURN_IDLE_TIMEOUT_MS = 10 * 60 * 1000;
6
+ export class ConnectionBasedRunner {
7
+ kind;
8
+ sessionReader;
9
+ lifetime = 'workspace';
10
+ currentClient = null;
11
+ currentTurnId = null;
12
+ lastEventAt = 0;
13
+ _isRunning = false;
14
+ turnIdleTimeoutMs;
15
+ /** Notification queue consumed by the active run generator. */
16
+ notificationQueue = [];
17
+ waitResolve = null;
18
+ forceFinish = false;
19
+ /** stop() 在 turn/start 或 prompt 响应前到达时置位,完成后再补发 cancel/interrupt。 */
20
+ stopRequested = false;
21
+ constructor(opts, defaultTurnIdleTimeoutMs = TURN_IDLE_TIMEOUT_MS) {
22
+ this.kind = opts.kind;
23
+ this.sessionReader = opts.sessionReader;
24
+ this.turnIdleTimeoutMs = opts.turnIdleTimeoutMs ?? defaultTurnIdleTimeoutMs;
25
+ }
26
+ get isRunning() {
27
+ return this._isRunning;
28
+ }
29
+ /**
30
+ * Run a message. Delegates to executeTurn() with the standard setup.
31
+ */
32
+ async *run(message, opts) {
33
+ yield* this.executeTurn(opts, () => this.setupTurn(message, opts));
34
+ }
35
+ /**
36
+ * Shared turn lifecycle used by run() and runCompact(): isRunning guard →
37
+ * resetRunState → setupFn (with connection-lost respawn-and-retry-once) →
38
+ * deferred stop → synthetic init → consumeTurn; catch yields synthetic init
39
+ * + error result; finally resets flags + notifyIdle.
40
+ *
41
+ * `setupFn` performs the turn-specific setup (acquire, session new/resume,
42
+ * prompt, etc.) and may push events into the notification queue.
43
+ */
44
+ async *executeTurn(opts, setupFn) {
45
+ if (this._isRunning) {
46
+ throw new Error(`${this.constructor.name} is already running`);
47
+ }
48
+ this._isRunning = true;
49
+ this.resetRunState();
50
+ try {
51
+ try {
52
+ await setupFn();
53
+ }
54
+ catch (err) {
55
+ // 连接在 setup 期间丢失(进程被误杀/异常退出):重拉进程并重试一次。
56
+ if (err instanceof ConnectionLostError && !this.stopRequested) {
57
+ getLogger().warn(`[${this.logTag}] connection lost during turn setup (${err.message}), respawning and retrying once`);
58
+ await this.respawnAfterConnectionLost(opts);
59
+ await setupFn();
60
+ }
61
+ else {
62
+ throw err;
63
+ }
64
+ }
65
+ if (this.stopRequested) {
66
+ // 启动期 /stop:turn/start 或 prompt 尚未完成,补发 cancel/interrupt,
67
+ // 避免 server 端 turn 无人接管继续执行。
68
+ getLogger().warn(`[${this.logTag}] stop requested before turn resolved`);
69
+ try {
70
+ await this.cancelCurrentTurn();
71
+ }
72
+ catch (err) {
73
+ getLogger().warn(`[${this.logTag}] deferred cancel failed: ${err.message}`);
74
+ }
75
+ }
76
+ // §9.22 守卫前提:桥的 pre-init result guard 和 run-state reducer 都以
77
+ // system.init 作为「本轮真实开始」的标记,而这些协议没有 init 事件。
78
+ // 缺了它,成功的 result 会被当作 pre-init 丢弃,卡片终态停在 running。
79
+ yield syntheticInitEvent(this.currentSessionId() ?? opts.sessionId ?? '');
80
+ yield* this.consumeTurn();
81
+ }
82
+ catch (err) {
83
+ getLogger().error(`[${this.logTag}] run error: ${err.message}`);
84
+ yield syntheticInitEvent(this.currentSessionId() ?? opts.sessionId ?? '');
85
+ yield {
86
+ type: 'result',
87
+ subtype: 'error',
88
+ // 优先上报本次 setup 创建/恢复的 session/thread id。
89
+ session_id: this.currentSessionId() ?? opts.sessionId ?? '',
90
+ errorMessage: err.message,
91
+ };
92
+ }
93
+ finally {
94
+ this._isRunning = false;
95
+ this.currentTurnId = null;
96
+ this.stopRequested = false;
97
+ this.clearTurnState();
98
+ this.notifyIdle(opts.cwd);
99
+ }
100
+ }
101
+ /**
102
+ * Stop the current turn by cancelling it. Shared skeleton: if the turn/start
103
+ * or prompt has not yet resolved (shouldDeferStop), mark stopRequested and
104
+ * unblock; otherwise cancel now via cancelCurrentTurn().
105
+ */
106
+ async stop(_opts) {
107
+ if (!this._isRunning || !this.currentClient || !this.currentSessionId())
108
+ return;
109
+ if (this.shouldDeferStop()) {
110
+ // turn/start 或 prompt 尚未响应:先标记,等 run() 完成后再补发 cancel。
111
+ this.stopRequested = true;
112
+ this.forceFinish = true;
113
+ this.wakeWaiters();
114
+ return;
115
+ }
116
+ // turn/start 或 prompt 已 resolved:立即 cancel;同时置 stopRequested 抑制
117
+ // 尚未结算的 prompt 结果翻译(opencode 语义),对已结算方无害。
118
+ this.stopRequested = true;
119
+ try {
120
+ await this.cancelCurrentTurn();
121
+ }
122
+ catch (err) {
123
+ getLogger().warn(`[${this.logTag}] stop error: ${err.message}`);
124
+ }
125
+ // Unblock the turn loop regardless of whether the server sends a
126
+ // completed notification afterwards.
127
+ this.forceFinish = true;
128
+ this.wakeWaiters();
129
+ }
130
+ /**
131
+ * Dispose the runner: release the connection.
132
+ */
133
+ async dispose() {
134
+ try {
135
+ await this.disposeConnections();
136
+ }
137
+ catch (err) {
138
+ getLogger().warn(`[${this.logTag}] dispose error: ${err.message}`);
139
+ }
140
+ }
141
+ getUsageAuthority() {
142
+ return 'live';
143
+ }
144
+ killOrphan() {
145
+ // No-op: the connection is managed by ConnectionManager.
146
+ }
147
+ registerExitHandlers() {
148
+ // No-op: connection manager handles cleanup on exit.
149
+ }
150
+ unregisterExitHandlers() {
151
+ // No-op.
152
+ }
153
+ // =========================================================================
154
+ // Shared turn machinery
155
+ // =========================================================================
156
+ /**
157
+ * Consume the notification queue until a result event arrives or the turn
158
+ * times out / is interrupted. forceFinish drains the queue first (the
159
+ * deferred cancel's own events may still be in it) before yielding the
160
+ * interrupted result.
161
+ */
162
+ async *consumeTurn() {
163
+ while (true) {
164
+ const timedOut = await this.waitForEvents(this.nextTurnIdleDeadline());
165
+ if (timedOut) {
166
+ await this.cancelCurrentTurn();
167
+ getLogger().warn(`[${this.logTag}] turn wait timed out`);
168
+ yield {
169
+ type: 'result',
170
+ subtype: 'error',
171
+ session_id: this.currentSessionId() ?? '',
172
+ errorMessage: this.turnTimeoutErrorMessage,
173
+ };
174
+ return;
175
+ }
176
+ if (this.forceFinish && this.notificationQueue.length === 0) {
177
+ yield {
178
+ type: 'result',
179
+ subtype: 'interrupted',
180
+ session_id: this.currentSessionId() ?? '',
181
+ errorMessage: this.turnInterruptedErrorMessage,
182
+ };
183
+ return;
184
+ }
185
+ while (this.notificationQueue.length > 0) {
186
+ const ev = this.notificationQueue.shift();
187
+ yield ev;
188
+ if (ev.type === 'result') {
189
+ return;
190
+ }
191
+ }
192
+ }
193
+ }
194
+ /** Compute the current idle deadline. 0 disables; otherwise rolling on last event. */
195
+ nextTurnIdleDeadline() {
196
+ if (this.turnIdleTimeoutMs <= 0)
197
+ return Number.POSITIVE_INFINITY;
198
+ return this.lastEventAt + this.turnIdleTimeoutMs;
199
+ }
200
+ async waitForEvents(deadline) {
201
+ while (this.notificationQueue.length === 0 && !this.forceFinish) {
202
+ const remaining = deadline - Date.now();
203
+ if (remaining <= 0)
204
+ return true;
205
+ await new Promise((resolve) => {
206
+ this.waitResolve = resolve;
207
+ setTimeout(() => {
208
+ if (this.waitResolve === resolve) {
209
+ this.waitResolve = null;
210
+ resolve();
211
+ }
212
+ }, Math.min(remaining, 30_000));
213
+ });
214
+ }
215
+ return false;
216
+ }
217
+ pushEvents(events) {
218
+ if (events.length === 0)
219
+ return;
220
+ this.lastEventAt = Date.now();
221
+ this.notificationQueue.push(...events);
222
+ this.wakeWaiters();
223
+ }
224
+ wakeWaiters() {
225
+ if (this.waitResolve) {
226
+ const resolve = this.waitResolve;
227
+ this.waitResolve = null;
228
+ resolve();
229
+ }
230
+ }
231
+ failTurn(message) {
232
+ this.pushEvents([
233
+ {
234
+ type: 'result',
235
+ subtype: 'error',
236
+ session_id: this.currentSessionId() ?? '',
237
+ errorMessage: message,
238
+ },
239
+ ]);
240
+ }
241
+ resetRunState() {
242
+ this.notificationQueue = [];
243
+ this.waitResolve = null;
244
+ this.lastEventAt = Date.now();
245
+ this.forceFinish = false;
246
+ }
247
+ /** Release the connection + detach stale client hooks after a lost connection. */
248
+ async respawnAfterConnectionLost(opts) {
249
+ await this.releaseConnection(opts.cwd);
250
+ const staleClient = this.currentClient;
251
+ if (staleClient?.setHooks) {
252
+ staleClient.setHooks({
253
+ onNotification: () => { },
254
+ onServerRequest: () => { },
255
+ onClose: () => { },
256
+ });
257
+ }
258
+ this.resetRunState();
259
+ }
260
+ }
261
+ //# sourceMappingURL=connection-based-runner.js.map
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Generic JSON-RPC client for JSON-line-RPC agent servers
3
+ * (codex app-server, kimi acp, and future ACP-style integrations).
4
+ *
5
+ * Manages the handshake, request/response matching, notification dispatch,
6
+ * and timeout handling over a JsonlRpcTransport. The `initialize` params
7
+ * shape is protocol-specific and injected by the caller (constructor arg);
8
+ * the default is the ACP handshake shape (ACP is the reference protocol).
9
+ */
10
+ import type { JsonlRpcTransport } from './transport.js';
11
+ export declare class RpcError extends Error {
12
+ readonly code: number;
13
+ readonly data?: unknown;
14
+ constructor(code: number, message: string, data?: unknown);
15
+ }
16
+ export declare class RpcTimeoutError extends Error {
17
+ constructor(message: string);
18
+ }
19
+ export declare class ConnectionLostError extends Error {
20
+ constructor(message?: string);
21
+ }
22
+ export interface ClientHooks {
23
+ onNotification(method: string, params: unknown): void;
24
+ onServerRequest(id: number | string, method: string, params: unknown): void;
25
+ onClose(): void;
26
+ }
27
+ export declare class JsonRpcClient<InitializeResult = unknown> {
28
+ private transport;
29
+ /**
30
+ * Lifecycle hooks owned by the connection layer (ConnectionManager): slot
31
+ * cleanup + onConnectionLost. These must survive runner's setHooks().
32
+ */
33
+ private baseHooks;
34
+ /** Per-run hooks from the runner, layered over baseHooks via setHooks(). */
35
+ private runHooks;
36
+ private requestTimeoutMs;
37
+ private initializeParams;
38
+ private pendingRequests;
39
+ private nextId;
40
+ private _ready;
41
+ private _disposed;
42
+ constructor(transport: JsonlRpcTransport, hooks: ClientHooks, requestTimeoutMs: number | undefined, initializeParams: object);
43
+ /**
44
+ * Replace the per-run hooks (used by the runner to bridge
45
+ * notifications/server requests into the active turn's translator).
46
+ * Connection-layer hooks (slot cleanup / onConnectionLost) are preserved:
47
+ * review P2-1 — an overwrite here previously silenced onClose slot cleanup,
48
+ * leaving dead slots behind after the server process was killed.
49
+ */
50
+ setHooks(hooks: ClientHooks): void;
51
+ get ready(): boolean;
52
+ /**
53
+ * Whether the underlying transport is still alive (not disposed and not
54
+ * closed). Used by the connection manager to avoid handing out a cached
55
+ * client whose child process was killed externally.
56
+ */
57
+ get healthy(): boolean;
58
+ /**
59
+ * Connect and perform the initialize handshake.
60
+ * Returns the InitializeResult on success.
61
+ */
62
+ connect(): Promise<InitializeResult>;
63
+ /**
64
+ * Send a JSON-RPC request and wait for the response.
65
+ * `timeoutMs` overrides the default request timeout — turn-scoped requests
66
+ * (e.g. session/prompt) hold their response open for the whole turn, so
67
+ * callers pass the turn idle timeout (bounded by the runner's turn idle
68
+ * watchdog, not the control-plane request timeout).
69
+ */
70
+ request<P, R>(method: string, params?: P, timeoutMs?: number): Promise<R>;
71
+ /**
72
+ * Send a JSON-RPC notification (no response expected).
73
+ */
74
+ notify(method: string, params?: unknown): void;
75
+ /**
76
+ * Respond to a server request (success).
77
+ */
78
+ respond(id: number | string, result: unknown): void;
79
+ /**
80
+ * Respond to a server request (error).
81
+ */
82
+ respondError(id: number | string, code: number, message: string): void;
83
+ /**
84
+ * Dispose the client: cancel pending requests and close transport.
85
+ */
86
+ dispose(): Promise<void>;
87
+ private handleMessage;
88
+ private failPending;
89
+ }
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Generic JSON-RPC client for JSON-line-RPC agent servers
3
+ * (codex app-server, kimi acp, and future ACP-style integrations).
4
+ *
5
+ * Manages the handshake, request/response matching, notification dispatch,
6
+ * and timeout handling over a JsonlRpcTransport. The `initialize` params
7
+ * shape is protocol-specific and injected by the caller (constructor arg);
8
+ * the default is the ACP handshake shape (ACP is the reference protocol).
9
+ */
10
+ import { getLogger } from '../../../logger/index.js';
11
+ // =============================================================================
12
+ // Error Types
13
+ // =============================================================================
14
+ export class RpcError extends Error {
15
+ code;
16
+ data;
17
+ constructor(code, message, data) {
18
+ super(message);
19
+ this.name = 'RpcError';
20
+ this.code = code;
21
+ this.data = data;
22
+ }
23
+ }
24
+ export class RpcTimeoutError extends Error {
25
+ constructor(message) {
26
+ super(message);
27
+ this.name = 'RpcTimeoutError';
28
+ }
29
+ }
30
+ export class ConnectionLostError extends Error {
31
+ constructor(message = 'Connection lost') {
32
+ super(message);
33
+ this.name = 'ConnectionLostError';
34
+ }
35
+ }
36
+ // =============================================================================
37
+ // Client
38
+ // =============================================================================
39
+ export class JsonRpcClient {
40
+ transport;
41
+ /**
42
+ * Lifecycle hooks owned by the connection layer (ConnectionManager): slot
43
+ * cleanup + onConnectionLost. These must survive runner's setHooks().
44
+ */
45
+ baseHooks;
46
+ /** Per-run hooks from the runner, layered over baseHooks via setHooks(). */
47
+ runHooks = null;
48
+ requestTimeoutMs;
49
+ initializeParams;
50
+ pendingRequests = new Map();
51
+ nextId = 1;
52
+ _ready = false;
53
+ _disposed = false;
54
+ constructor(transport, hooks, requestTimeoutMs = 60_000, initializeParams) {
55
+ this.transport = transport;
56
+ this.baseHooks = hooks;
57
+ this.requestTimeoutMs = requestTimeoutMs;
58
+ this.initializeParams = initializeParams;
59
+ }
60
+ /**
61
+ * Replace the per-run hooks (used by the runner to bridge
62
+ * notifications/server requests into the active turn's translator).
63
+ * Connection-layer hooks (slot cleanup / onConnectionLost) are preserved:
64
+ * review P2-1 — an overwrite here previously silenced onClose slot cleanup,
65
+ * leaving dead slots behind after the server process was killed.
66
+ */
67
+ setHooks(hooks) {
68
+ this.runHooks = hooks;
69
+ }
70
+ get ready() {
71
+ return this._ready;
72
+ }
73
+ /**
74
+ * Whether the underlying transport is still alive (not disposed and not
75
+ * closed). Used by the connection manager to avoid handing out a cached
76
+ * client whose child process was killed externally.
77
+ */
78
+ get healthy() {
79
+ return !this._disposed && !this.transport.closed;
80
+ }
81
+ /**
82
+ * Connect and perform the initialize handshake.
83
+ * Returns the InitializeResult on success.
84
+ */
85
+ async connect() {
86
+ const events = {
87
+ onMessage: (msg) => this.handleMessage(msg),
88
+ onClose: () => {
89
+ this.failPending(new ConnectionLostError('transport closed'));
90
+ // 先通知连接层(slot 清理 + onConnectionLost),再通知当前 run 层
91
+ // (failTurn 等)。两层互不覆盖。
92
+ this.baseHooks.onClose();
93
+ this.runHooks?.onClose();
94
+ },
95
+ };
96
+ await this.transport.start(events);
97
+ // ENOENT/启动失败时 transport 已在 start() 内触发 onClose 并置 closed;
98
+ // 直接 fail-fast,避免 initialize 请求挂满 requestTimeoutMs 才报错。
99
+ if (this.transport.closed) {
100
+ throw new ConnectionLostError('JSON-RPC server failed to start (binary missing or crashed)');
101
+ }
102
+ // Send initialize request (shape injected by the caller)
103
+ const result = await this.request('initialize', this.initializeParams);
104
+ this._ready = true;
105
+ return result;
106
+ }
107
+ /**
108
+ * Send a JSON-RPC request and wait for the response.
109
+ * `timeoutMs` overrides the default request timeout — turn-scoped requests
110
+ * (e.g. session/prompt) hold their response open for the whole turn, so
111
+ * callers pass the turn idle timeout (bounded by the runner's turn idle
112
+ * watchdog, not the control-plane request timeout).
113
+ */
114
+ async request(method, params, timeoutMs) {
115
+ if (this._disposed) {
116
+ throw new ConnectionLostError('client is disposed');
117
+ }
118
+ if (this.transport.closed) {
119
+ throw new ConnectionLostError('transport closed');
120
+ }
121
+ const id = this.nextId++;
122
+ const request = {
123
+ jsonrpc: '2.0',
124
+ id,
125
+ method,
126
+ params,
127
+ };
128
+ return new Promise((resolve, reject) => {
129
+ // Clamp to setTimeout's max: callers may pass Infinity for turn-scoped
130
+ // requests (session/prompt) — Infinity would fire immediately in Node.
131
+ const effectiveTimeout = Math.min(timeoutMs ?? this.requestTimeoutMs, 2_147_483_647);
132
+ const timer = setTimeout(() => {
133
+ this.pendingRequests.delete(id);
134
+ reject(new RpcTimeoutError(`request timed out: ${method}`));
135
+ }, effectiveTimeout);
136
+ this.pendingRequests.set(id, {
137
+ resolve: resolve,
138
+ reject,
139
+ timer,
140
+ });
141
+ this.transport.write(request);
142
+ });
143
+ }
144
+ /**
145
+ * Send a JSON-RPC notification (no response expected).
146
+ */
147
+ notify(method, params) {
148
+ if (this._disposed)
149
+ return;
150
+ const notification = {
151
+ jsonrpc: '2.0',
152
+ method,
153
+ params,
154
+ };
155
+ this.transport.write(notification);
156
+ }
157
+ /**
158
+ * Respond to a server request (success).
159
+ */
160
+ respond(id, result) {
161
+ const response = {
162
+ jsonrpc: '2.0',
163
+ id,
164
+ result,
165
+ };
166
+ this.transport.write(response);
167
+ }
168
+ /**
169
+ * Respond to a server request (error).
170
+ */
171
+ respondError(id, code, message) {
172
+ const response = {
173
+ jsonrpc: '2.0',
174
+ id,
175
+ error: { code, message },
176
+ };
177
+ this.transport.write(response);
178
+ }
179
+ /**
180
+ * Dispose the client: cancel pending requests and close transport.
181
+ */
182
+ async dispose() {
183
+ if (this._disposed)
184
+ return;
185
+ this._disposed = true;
186
+ this._ready = false;
187
+ this.failPending(new ConnectionLostError('client disposed'));
188
+ await this.transport.close();
189
+ }
190
+ // =========================================================================
191
+ // Internal
192
+ // =========================================================================
193
+ handleMessage(msg) {
194
+ const message = msg;
195
+ if ('id' in message && 'result' in message) {
196
+ // Success response
197
+ const id = message.id;
198
+ const pending = this.pendingRequests.get(id);
199
+ if (pending) {
200
+ clearTimeout(pending.timer);
201
+ this.pendingRequests.delete(id);
202
+ pending.resolve(message.result);
203
+ }
204
+ return;
205
+ }
206
+ if ('id' in message && 'error' in message) {
207
+ // Error response
208
+ const id = message.id;
209
+ const err = message.error;
210
+ const pending = this.pendingRequests.get(id);
211
+ if (pending) {
212
+ clearTimeout(pending.timer);
213
+ this.pendingRequests.delete(id);
214
+ pending.reject(new RpcError(err.code, err.message, err.data));
215
+ }
216
+ return;
217
+ }
218
+ if ('method' in message && !('id' in message)) {
219
+ // Notification
220
+ const method = message.method;
221
+ this.baseHooks.onNotification(method, message.params);
222
+ this.runHooks?.onNotification(method, message.params);
223
+ return;
224
+ }
225
+ if ('method' in message && 'id' in message) {
226
+ // Server request (incoming request from server — reverse RPC)
227
+ const id = message.id;
228
+ const method = message.method;
229
+ this.baseHooks.onServerRequest(id, method, message.params);
230
+ this.runHooks?.onServerRequest(id, method, message.params);
231
+ return;
232
+ }
233
+ getLogger().warn(`[jsonrpc-client] unknown message type: ${JSON.stringify(message).slice(0, 200)}`);
234
+ }
235
+ failPending(err) {
236
+ for (const [_id, pending] of this.pendingRequests) {
237
+ clearTimeout(pending.timer);
238
+ pending.reject(err);
239
+ }
240
+ this.pendingRequests.clear();
241
+ }
242
+ }
243
+ //# sourceMappingURL=client.js.map