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.
- package/CHANGELOG.md +39 -0
- package/README.en.md +5 -1
- package/README.md +10 -2
- package/dist/bridge/approval-coordinator.d.ts +131 -0
- package/dist/bridge/approval-coordinator.js +409 -0
- package/dist/bridge/inbound-media.d.ts +62 -0
- package/dist/bridge/inbound-media.js +282 -0
- package/dist/bridge/index.d.ts +138 -5
- package/dist/bridge/index.js +569 -65
- package/dist/bridge/queue-manager.d.ts +11 -1
- package/dist/bridge/queue-manager.js +20 -11
- package/dist/card/approval-render.d.ts +16 -0
- package/dist/card/approval-render.js +363 -0
- package/dist/card/bash-card-session.d.ts +2 -24
- package/dist/card/bash-card-session.js +6 -73
- package/dist/card/bash-renderer.js +2 -3
- package/dist/card/card-budget.js +29 -75
- package/dist/card/card-session.d.ts +48 -0
- package/dist/card/card-session.js +86 -0
- package/dist/card/card-shared.d.ts +13 -10
- package/dist/card/card-shared.js +27 -24
- package/dist/card/run-card-session.d.ts +2 -55
- package/dist/card/run-card-session.js +6 -104
- package/dist/card/run-renderer.js +140 -202
- package/dist/card/run-state.d.ts +38 -3
- package/dist/card/run-state.js +221 -11
- package/dist/card/text-truncate.d.ts +13 -12
- package/dist/card/text-truncate.js +13 -63
- package/dist/common/fs.d.ts +8 -0
- package/dist/common/fs.js +17 -0
- package/dist/common/guards.d.ts +0 -20
- package/dist/common/guards.js +0 -43
- package/dist/common/sleep.d.ts +2 -0
- package/dist/common/sleep.js +5 -0
- package/dist/common/truncate.d.ts +39 -0
- package/dist/common/truncate.js +92 -0
- package/dist/config/codex-config.d.ts +1 -1
- package/dist/config/codex-config.js +1 -1
- package/dist/config/dir.js +1 -1
- package/dist/config/index.d.ts +116 -9
- package/dist/config/index.js +122 -29
- package/dist/connector/index.d.ts +74 -2
- package/dist/connector/index.js +119 -1
- package/dist/index.js +139 -99
- package/dist/instance-lock.js +4 -12
- package/dist/order/alias-resolve.d.ts +13 -0
- package/dist/order/alias-resolve.js +22 -0
- package/dist/order/alias-store.d.ts +38 -0
- package/dist/order/alias-store.js +106 -0
- package/dist/persistence/atomic-write.d.ts +8 -2
- package/dist/persistence/atomic-write.js +23 -7
- package/dist/restart.js +2 -1
- package/dist/router/card-helpers.d.ts +63 -0
- package/dist/router/card-helpers.js +146 -0
- package/dist/router/config/claude.js +25 -1
- package/dist/router/config/codex.d.ts +1 -1
- package/dist/router/config/codex.js +33 -5
- package/dist/router/config/common/model-patch.d.ts +22 -0
- package/dist/router/config/common/model-patch.js +26 -0
- package/dist/router/config/common/render.js +9 -4
- package/dist/router/config/index.d.ts +12 -0
- package/dist/router/config/index.js +25 -1
- package/dist/router/config/kimi.d.ts +4 -1
- package/dist/router/config/kimi.js +27 -0
- package/dist/router/config/opencode.js +18 -5
- package/dist/router/config/pi.js +4 -5
- package/dist/router/config/types.d.ts +12 -3
- package/dist/router/index.d.ts +77 -17
- package/dist/router/index.js +455 -549
- package/dist/router/utils.d.ts +4 -0
- package/dist/router/utils.js +9 -2
- package/dist/runner/agent-choices-common.d.ts +16 -0
- package/dist/runner/agent-choices-common.js +31 -0
- package/dist/runner/claude/runner.d.ts +47 -10
- package/dist/runner/claude/runner.js +117 -46
- package/dist/runner/claude/session.d.ts +166 -0
- package/dist/runner/claude/session.js +749 -0
- package/dist/runner/codex/app-server/protocol-types.d.ts +519 -0
- package/dist/runner/codex/app-server/protocol-types.js +56 -0
- package/dist/runner/codex/app-server/runner.d.ts +104 -0
- package/dist/runner/codex/app-server/runner.js +377 -0
- package/dist/runner/codex/app-server/translator.d.ts +89 -0
- package/dist/runner/codex/app-server/translator.js +716 -0
- package/dist/runner/codex/index.d.ts +1 -1
- package/dist/runner/codex/index.js +1 -1
- package/dist/runner/common/acp/protocol-helpers.d.ts +14 -0
- package/dist/runner/common/acp/protocol-helpers.js +21 -0
- package/dist/runner/common/acp/protocol-types.d.ts +238 -0
- package/dist/runner/common/acp/protocol-types.js +75 -0
- package/dist/runner/common/connection-based-runner.d.ts +111 -0
- package/dist/runner/common/connection-based-runner.js +261 -0
- package/dist/runner/common/jsonrpc/client.d.ts +89 -0
- package/dist/runner/common/jsonrpc/client.js +243 -0
- package/dist/runner/common/jsonrpc/connection-manager.d.ts +125 -0
- package/dist/runner/common/jsonrpc/connection-manager.js +205 -0
- package/dist/runner/common/jsonrpc/transport.d.ts +50 -0
- package/dist/runner/common/jsonrpc/transport.js +230 -0
- package/dist/runner/common/jsonrpc/types.d.ts +32 -0
- package/dist/runner/common/jsonrpc/types.js +9 -0
- package/dist/runner/common/process-stopper.d.ts +2 -1
- package/dist/runner/common/process-stopper.js +1 -0
- package/dist/runner/common/runner-utils.d.ts +6 -13
- package/dist/runner/common/runner-utils.js +6 -47
- package/dist/runner/common/spawning-runner.d.ts +24 -70
- package/dist/runner/common/spawning-runner.js +98 -152
- package/dist/runner/common/usage.d.ts +2 -0
- package/dist/runner/common/usage.js +2 -0
- package/dist/runner/index.d.ts +5 -5
- package/dist/runner/index.js +4 -4
- package/dist/runner/kimi/acp/runner.d.ts +106 -0
- package/dist/runner/kimi/acp/runner.js +448 -0
- package/dist/runner/kimi/acp/translator.d.ts +124 -0
- package/dist/runner/kimi/acp/translator.js +336 -0
- package/dist/runner/kimi/index.d.ts +1 -1
- package/dist/runner/kimi/index.js +1 -1
- package/dist/runner/opencode/acp/index.d.ts +1 -0
- package/dist/runner/opencode/acp/index.js +2 -0
- package/dist/runner/opencode/acp/runner.d.ts +121 -0
- package/dist/runner/opencode/acp/runner.js +429 -0
- package/dist/runner/opencode/acp/translator.d.ts +119 -0
- package/dist/runner/opencode/acp/translator.js +345 -0
- package/dist/runner/opencode/index.d.ts +1 -1
- package/dist/runner/opencode/index.js +1 -1
- package/dist/runner/pi/index.d.ts +1 -1
- package/dist/runner/pi/index.js +1 -1
- package/dist/runner/pi/rpc/client.d.ts +52 -0
- package/dist/runner/pi/rpc/client.js +114 -0
- package/dist/runner/pi/rpc/index.d.ts +4 -0
- package/dist/runner/pi/rpc/index.js +4 -0
- package/dist/runner/pi/rpc/protocol-types.d.ts +127 -0
- package/dist/runner/pi/rpc/protocol-types.js +21 -0
- package/dist/runner/pi/rpc/runner.d.ts +80 -0
- package/dist/runner/pi/rpc/runner.js +199 -0
- package/dist/runner/pi/rpc/translator.d.ts +55 -0
- package/dist/runner/pi/rpc/translator.js +258 -0
- package/dist/runner/registry.d.ts +7 -8
- package/dist/runner/registry.js +3 -4
- package/dist/runner/resolve-agent-choices.js +18 -59
- package/dist/runner/sync-agent-choices.js +16 -67
- package/dist/runner/types.d.ts +153 -2
- package/dist/session/claude/session-index.js +2 -3
- package/dist/session/claude/session-reader.js +3 -6
- package/dist/session/claude/sessions.js +6 -13
- package/dist/session/codex/rollout-reader.d.ts +2 -2
- package/dist/session/codex/rollout-reader.js +57 -17
- package/dist/session/codex/sessions.d.ts +1 -1
- package/dist/session/codex/sessions.js +2 -2
- package/dist/session/common/content-blocks.d.ts +7 -0
- package/dist/session/common/content-blocks.js +13 -9
- package/dist/session/common/jsonl.d.ts +8 -25
- package/dist/session/common/jsonl.js +126 -261
- package/dist/session/common/pagination.d.ts +28 -0
- package/dist/session/common/pagination.js +33 -0
- package/dist/session/kimi/sessions.d.ts +34 -1
- package/dist/session/kimi/sessions.js +208 -98
- package/dist/session/opencode/sessions.d.ts +17 -3
- package/dist/session/opencode/sessions.js +29 -29
- package/dist/session/pi/sessions.d.ts +5 -1
- package/dist/session/pi/sessions.js +12 -16
- package/dist/session/session-store.d.ts +11 -1
- package/dist/session/session-store.js +75 -178
- package/dist/update/index.d.ts +2 -2
- package/dist/update/index.js +2 -2
- package/dist/update/install.d.ts +20 -2
- package/dist/update/install.js +51 -7
- package/package.json +1 -1
- package/dist/runner/codex/argv.d.ts +0 -33
- package/dist/runner/codex/argv.js +0 -40
- package/dist/runner/codex/jsonl.d.ts +0 -37
- package/dist/runner/codex/jsonl.js +0 -221
- package/dist/runner/codex/runner.d.ts +0 -63
- package/dist/runner/codex/runner.js +0 -124
- package/dist/runner/common/exec-translator.d.ts +0 -73
- package/dist/runner/common/exec-translator.js +0 -105
- package/dist/runner/kimi/jsonl.d.ts +0 -28
- package/dist/runner/kimi/jsonl.js +0 -143
- package/dist/runner/kimi/runner.d.ts +0 -49
- package/dist/runner/kimi/runner.js +0 -105
- package/dist/runner/opencode/argv.d.ts +0 -32
- package/dist/runner/opencode/argv.js +0 -35
- package/dist/runner/opencode/jsonl.d.ts +0 -68
- package/dist/runner/opencode/jsonl.js +0 -295
- package/dist/runner/opencode/runner.d.ts +0 -61
- package/dist/runner/opencode/runner.js +0 -93
- package/dist/runner/pi/jsonl.d.ts +0 -128
- package/dist/runner/pi/jsonl.js +0 -249
- package/dist/runner/pi/runner.d.ts +0 -36
- package/dist/runner/pi/runner.js +0 -76
- package/dist/session/common/catch-up.d.ts +0 -12
- 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
|