lark-remote 0.1.5 → 0.1.6
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 +17 -0
- package/README.en.md +0 -4
- package/README.md +0 -4
- package/dist/bridge/approval-coordinator.d.ts +108 -0
- package/dist/bridge/approval-coordinator.js +258 -0
- package/dist/bridge/index.d.ts +62 -0
- package/dist/bridge/index.js +402 -21
- package/dist/bridge/queue-manager.d.ts +9 -0
- package/dist/bridge/queue-manager.js +19 -11
- package/dist/card/approval-render.d.ts +16 -0
- package/dist/card/approval-render.js +247 -0
- package/dist/card/card-shared.d.ts +14 -0
- package/dist/card/card-shared.js +28 -0
- package/dist/card/run-renderer.js +77 -6
- package/dist/card/run-state.d.ts +25 -3
- package/dist/card/run-state.js +203 -2
- package/dist/config/dir.js +1 -1
- package/dist/config/index.d.ts +42 -0
- package/dist/config/index.js +23 -1
- package/dist/connector/index.d.ts +5 -0
- package/dist/connector/index.js +12 -0
- package/dist/index.js +44 -22
- package/dist/router/config/codex.d.ts +1 -1
- package/dist/router/config/codex.js +49 -0
- package/dist/router/config/common/render.js +4 -0
- package/dist/router/config/types.d.ts +1 -1
- package/dist/router/index.d.ts +27 -2
- package/dist/router/index.js +109 -28
- package/dist/router/utils.d.ts +2 -0
- package/dist/router/utils.js +5 -2
- package/dist/runner/codex/app-server/client.d.ts +82 -0
- package/dist/runner/codex/app-server/client.js +234 -0
- package/dist/runner/codex/app-server/connection-manager.d.ts +56 -0
- package/dist/runner/codex/app-server/connection-manager.js +170 -0
- package/dist/runner/codex/app-server/protocol-types.d.ts +527 -0
- package/dist/runner/codex/app-server/protocol-types.js +67 -0
- package/dist/runner/codex/app-server/runner.d.ts +121 -0
- package/dist/runner/codex/app-server/runner.js +554 -0
- package/dist/runner/codex/app-server/translator.d.ts +109 -0
- package/dist/runner/codex/app-server/translator.js +734 -0
- package/dist/runner/codex/app-server/transport.d.ts +49 -0
- package/dist/runner/codex/app-server/transport.js +231 -0
- package/dist/runner/codex/factory.d.ts +29 -0
- package/dist/runner/codex/factory.js +39 -0
- package/dist/runner/codex/index.d.ts +2 -0
- package/dist/runner/codex/index.js +2 -0
- package/dist/runner/codex/runner.d.ts +3 -0
- package/dist/runner/codex/runner.js +5 -0
- package/dist/runner/common/usage.d.ts +2 -0
- package/dist/runner/common/usage.js +2 -0
- package/dist/runner/index.d.ts +2 -2
- package/dist/runner/index.js +1 -1
- package/dist/runner/types.d.ts +123 -2
- package/dist/session/codex/rollout-reader.js +43 -0
- package/package.json +1 -1
package/dist/router/index.d.ts
CHANGED
|
@@ -60,6 +60,18 @@ export interface CardActionPayload {
|
|
|
60
60
|
/** CardKit 2.0 input 组件自带提交图标触发回调时回传的输入值 */
|
|
61
61
|
inputValue?: string;
|
|
62
62
|
orderId?: string;
|
|
63
|
+
/** Approval request ID for approval card actions. */
|
|
64
|
+
requestId?: number | string;
|
|
65
|
+
/** Decision for approval.respond (e.g. 'accept', 'decline', 'cancel'). */
|
|
66
|
+
decision?: string;
|
|
67
|
+
/** Scope for acceptForSession: 'turn' | 'session'. */
|
|
68
|
+
scope?: 'turn' | 'session';
|
|
69
|
+
/** Unique nonce to prevent duplicate processing. */
|
|
70
|
+
nonce?: string;
|
|
71
|
+
/** Permission item ID for approval.toggle. */
|
|
72
|
+
permId?: string;
|
|
73
|
+
/** Desired selection state for approval.toggle (card renders !current). */
|
|
74
|
+
selected?: boolean;
|
|
63
75
|
}
|
|
64
76
|
/**
|
|
65
77
|
* Response a cardAction handler may return to give the clicking user native
|
|
@@ -173,6 +185,12 @@ export declare class CommandRouter {
|
|
|
173
185
|
* Validates payload and dispatches to the corresponding command.
|
|
174
186
|
*/
|
|
175
187
|
handleCardAction(value: CardActionPayload, ctx: CommandContext): Promise<CardActionResponse | void>;
|
|
188
|
+
/**
|
|
189
|
+
* Handle approval-related card actions.
|
|
190
|
+
* Routes to the appropriate bridge method based on value.cmd.
|
|
191
|
+
* Returns a toast response for immediate user feedback.
|
|
192
|
+
*/
|
|
193
|
+
private handleApprovalAction;
|
|
176
194
|
/**
|
|
177
195
|
* Handle queue.cancel: remove this message from the queue.
|
|
178
196
|
*/
|
|
@@ -341,8 +359,7 @@ export declare class CommandRouter {
|
|
|
341
359
|
* /update: check for newer version and upgrade if available.
|
|
342
360
|
* /update check: only check, don't upgrade.
|
|
343
361
|
*
|
|
344
|
-
* Flow: check version → if newer, install →
|
|
345
|
-
* Reuses /restart's restartSpawner + pendingExit mechanism.
|
|
362
|
+
* Flow: check version → if newer, install → report success (no auto-restart).
|
|
346
363
|
*/
|
|
347
364
|
private cmdUpdate;
|
|
348
365
|
private cmdNew;
|
|
@@ -365,6 +382,14 @@ export declare class CommandRouter {
|
|
|
365
382
|
* When the session was cleared, renders a compact notice card instead.
|
|
366
383
|
*/
|
|
367
384
|
private buildConfigSwitchCard;
|
|
385
|
+
/**
|
|
386
|
+
* Whether a session of the given agent kind can be compacted from a resume
|
|
387
|
+
* card. Compact (thread/compact/start) is only implemented by the codex
|
|
388
|
+
* app-server runner (`runCompact`), so the button is gated on
|
|
389
|
+
* `serviceMode === 'app-server'` — exec mode spawns a fresh CLI per message
|
|
390
|
+
* and has no persistent session to compact.
|
|
391
|
+
*/
|
|
392
|
+
private canCompactSession;
|
|
368
393
|
private static readonly LS_PAGE_SIZE;
|
|
369
394
|
private cmdLs;
|
|
370
395
|
private cmdWs;
|
package/dist/router/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import YAML from 'yaml';
|
|
5
|
-
import { getConfigDir, getConfigValue, setConfigValue, setConfigValues, mapAgentKey, assertSafeKeyPart, } from '../config/index.js';
|
|
5
|
+
import { getConfigDir, getConfigValue, setConfigValue, setConfigValues, getAgentConfig, mapAgentKey, assertSafeKeyPart, } from '../config/index.js';
|
|
6
6
|
import { syncAgentChoices } from '../runner/index.js';
|
|
7
7
|
import { WorkspaceStore } from '../workspace/index.js';
|
|
8
8
|
import { OrderStore } from '../order/index.js';
|
|
@@ -45,7 +45,7 @@ const RESUME_CONTENT_PREFETCH = 5;
|
|
|
45
45
|
* `New Session`、空串),都不是真实输入,不得渲染成行标题。
|
|
46
46
|
*/
|
|
47
47
|
const RESUME_SUMMARY_PLACEHOLDERS = new Set(['', '(no user message)', '(无摘要)', 'New Session']);
|
|
48
|
-
import { newSessionButton, agentDisplayName } from '../card/card-shared.js';
|
|
48
|
+
import { newSessionButton, resumeCompactButton, agentDisplayName } from '../card/card-shared.js';
|
|
49
49
|
/**
|
|
50
50
|
* Card-action commands that must run with enqueue({ immediate: true }).
|
|
51
51
|
* These bypass the serial queue and execute immediately.
|
|
@@ -96,7 +96,12 @@ export function isImmediateAction(cmd) {
|
|
|
96
96
|
cmd === 'config.toggle' ||
|
|
97
97
|
cmd === 'config.set' ||
|
|
98
98
|
cmd === 'config.input' ||
|
|
99
|
-
cmd === 'config.save'
|
|
99
|
+
cmd === 'config.save' ||
|
|
100
|
+
// 审批响应/权限切换必须即时触达在途 run(同 stop 类控制动作)。若走串行
|
|
101
|
+
// 队列会排在等待审批的 run 之后形成死锁:run 不结束审批不执行,run 结束
|
|
102
|
+
// coordinator 已删响应空转(线上复现:approval.respond 排队卡、审批永不生效)。
|
|
103
|
+
cmd === 'approval.respond' ||
|
|
104
|
+
cmd === 'approval.toggle');
|
|
100
105
|
}
|
|
101
106
|
export class CommandRouter {
|
|
102
107
|
/** Valid agent kinds — single source of truth for resume.use / resume.page / cmdResume. */
|
|
@@ -237,12 +242,12 @@ export class CommandRouter {
|
|
|
237
242
|
return;
|
|
238
243
|
case 'ls.page':
|
|
239
244
|
return this.handleLsPage(value, ctx);
|
|
245
|
+
case 'ws.page':
|
|
246
|
+
return this.handleWsPage(value, ctx);
|
|
240
247
|
case 'ws.use':
|
|
241
248
|
return await this.handleWsUse(value, ctx);
|
|
242
249
|
case 'ws.sort':
|
|
243
250
|
return await this.handleWsSort(value, ctx);
|
|
244
|
-
case 'ws.page':
|
|
245
|
-
return await this.handleWsPage(value, ctx);
|
|
246
251
|
case 'ws.remove':
|
|
247
252
|
return await this.handleWsRemove(value, ctx);
|
|
248
253
|
case 'resume.use': {
|
|
@@ -319,6 +324,15 @@ export class CommandRouter {
|
|
|
319
324
|
// 调 updateCardInPlace → Feishu API 乱序到达导致 toggle 卡死。
|
|
320
325
|
// 2026-07-18: 返回 enqueueConfigAction 的结果以支持 toast 响应
|
|
321
326
|
return this.enqueueConfigAction(value, ctx);
|
|
327
|
+
case 'approval.respond':
|
|
328
|
+
case 'approval.toggle':
|
|
329
|
+
return this.handleApprovalAction(value, ctx);
|
|
330
|
+
case 'codex.compact':
|
|
331
|
+
await this.bridge.handleCodexCompact(value, ctx);
|
|
332
|
+
return;
|
|
333
|
+
case 'resume.compact':
|
|
334
|
+
await this.bridge.handleResumeCompact(value, ctx);
|
|
335
|
+
return;
|
|
322
336
|
default: {
|
|
323
337
|
// Help card buttons: help.<cmd> → execute /<cmd>
|
|
324
338
|
if (value.cmd.startsWith('help.')) {
|
|
@@ -344,6 +358,57 @@ export class CommandRouter {
|
|
|
344
358
|
}
|
|
345
359
|
}
|
|
346
360
|
}
|
|
361
|
+
/**
|
|
362
|
+
* Handle approval-related card actions.
|
|
363
|
+
* Routes to the appropriate bridge method based on value.cmd.
|
|
364
|
+
* Returns a toast response for immediate user feedback.
|
|
365
|
+
*/
|
|
366
|
+
async handleApprovalAction(value, _ctx) {
|
|
367
|
+
const { runId, requestId, decision, scope, nonce, permId } = value;
|
|
368
|
+
if (value.cmd === 'approval.respond') {
|
|
369
|
+
if (!runId || requestId === undefined || !decision || !nonce) {
|
|
370
|
+
return { toast: { type: 'error', content: '缺少审批响应参数' } };
|
|
371
|
+
}
|
|
372
|
+
try {
|
|
373
|
+
await this.bridge.handleApprovalRespond({
|
|
374
|
+
runId,
|
|
375
|
+
requestId,
|
|
376
|
+
decision,
|
|
377
|
+
scope,
|
|
378
|
+
nonce,
|
|
379
|
+
});
|
|
380
|
+
return { toast: { type: 'success', content: '审批已提交' } };
|
|
381
|
+
}
|
|
382
|
+
catch (err) {
|
|
383
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
384
|
+
// 审批已过期:给用户明确反馈,不静默不误导(2026-08-12 事故:点了允许无任何反馈)。
|
|
385
|
+
const content = /state=expired/.test(msg)
|
|
386
|
+
? '⏰ 审批已过期,无法响应'
|
|
387
|
+
: `审批响应失败:${msg}`;
|
|
388
|
+
return { toast: { type: 'error', content } };
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (value.cmd === 'approval.toggle') {
|
|
392
|
+
if (!runId || requestId === undefined || !permId || !nonce) {
|
|
393
|
+
return { toast: { type: 'error', content: '缺少权限切换参数' } };
|
|
394
|
+
}
|
|
395
|
+
try {
|
|
396
|
+
await this.bridge.handleApprovalToggle({
|
|
397
|
+
runId,
|
|
398
|
+
requestId,
|
|
399
|
+
permId,
|
|
400
|
+
nonce,
|
|
401
|
+
selected: value.selected ?? true,
|
|
402
|
+
});
|
|
403
|
+
return { toast: { type: 'info', content: '已切换' } };
|
|
404
|
+
}
|
|
405
|
+
catch (err) {
|
|
406
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
407
|
+
return { toast: { type: 'error', content: `权限切换失败:${msg}` } };
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return { toast: { type: 'error', content: '未知的审批操作' } };
|
|
411
|
+
}
|
|
347
412
|
/**
|
|
348
413
|
* Handle queue.cancel: remove this message from the queue.
|
|
349
414
|
*/
|
|
@@ -984,9 +1049,19 @@ export class CommandRouter {
|
|
|
984
1049
|
if (useResult.text && !useResult.text.includes('已切换')) {
|
|
985
1050
|
return { toast: { type: 'error', content: useResult.text } };
|
|
986
1051
|
}
|
|
987
|
-
// Success:
|
|
988
|
-
|
|
989
|
-
|
|
1052
|
+
// Success: send auto-resume card, or a persistent text confirmation when
|
|
1053
|
+
// there is no history session. Toast alone is insufficient: for this
|
|
1054
|
+
// immediate action the callback response is swallowed by enqueueImmediate
|
|
1055
|
+
// (fire-and-forget), and even if delivered it is transient — the user gets
|
|
1056
|
+
// no perceivable feedback on the switch. Same rationale as config.save
|
|
1057
|
+
// agent-switch notice: persistent message first, toast only as a
|
|
1058
|
+
// fallback if sendResult fails.
|
|
1059
|
+
let fallbackToast;
|
|
1060
|
+
if (useResult.card || useResult.text) {
|
|
1061
|
+
const sent = await this.bridge.sendResult(useResult, ctx);
|
|
1062
|
+
if (!sent && useResult.text) {
|
|
1063
|
+
fallbackToast = useResult.text;
|
|
1064
|
+
}
|
|
990
1065
|
}
|
|
991
1066
|
// Refresh the /ws list card in place so "recent" sort is immediately visible
|
|
992
1067
|
const currentOffset = Math.max(0, Math.trunc(Number(value.offset) || 0));
|
|
@@ -994,11 +1069,11 @@ export class CommandRouter {
|
|
|
994
1069
|
if (refreshed.card) {
|
|
995
1070
|
await this.bridge.updateCardInPlace(refreshed.card, ctx);
|
|
996
1071
|
}
|
|
997
|
-
// Toast feedback (suppress text when
|
|
1072
|
+
// Toast feedback (suppress text when a persistent message was already sent)
|
|
998
1073
|
return {
|
|
999
1074
|
toast: {
|
|
1000
1075
|
type: 'success',
|
|
1001
|
-
content:
|
|
1076
|
+
content: fallbackToast ?? '',
|
|
1002
1077
|
},
|
|
1003
1078
|
};
|
|
1004
1079
|
}
|
|
@@ -1667,8 +1742,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
1667
1742
|
* /update: check for newer version and upgrade if available.
|
|
1668
1743
|
* /update check: only check, don't upgrade.
|
|
1669
1744
|
*
|
|
1670
|
-
* Flow: check version → if newer, install →
|
|
1671
|
-
* Reuses /restart's restartSpawner + pendingExit mechanism.
|
|
1745
|
+
* Flow: check version → if newer, install → report success (no auto-restart).
|
|
1672
1746
|
*/
|
|
1673
1747
|
async cmdUpdate(args, ctx) {
|
|
1674
1748
|
const checkOnly = args[0] === 'check';
|
|
@@ -1702,21 +1776,8 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
1702
1776
|
if (!installResult.success) {
|
|
1703
1777
|
return { text: `❌ 升级失败: ${installResult.error}` };
|
|
1704
1778
|
}
|
|
1705
|
-
// 4.
|
|
1706
|
-
|
|
1707
|
-
return { text: `✅ 已升级到 ${latest},请手动 /restart 重启 bridge` };
|
|
1708
|
-
}
|
|
1709
|
-
try {
|
|
1710
|
-
const pid = this.restartSpawner();
|
|
1711
|
-
this.pendingExit = true;
|
|
1712
|
-
return { text: `✅ 升级成功 (${current} → ${latest}),正在重启(新进程 pid ${pid})…` };
|
|
1713
|
-
}
|
|
1714
|
-
catch (err) {
|
|
1715
|
-
getLogger().error('[router] update restart spawn failed:', err);
|
|
1716
|
-
return {
|
|
1717
|
-
text: `✅ 已升级到 ${latest},但重启失败:${err.message},请手动 /restart`,
|
|
1718
|
-
};
|
|
1719
|
-
}
|
|
1779
|
+
// 4. Report success (no auto-restart — user decides when to /restart)
|
|
1780
|
+
return { text: `✅ 升级成功 (${current} → ${latest}),发送 /restart 重启后生效` };
|
|
1720
1781
|
}
|
|
1721
1782
|
cmdNew(ctx) {
|
|
1722
1783
|
// 只清 sessionId 保留 cwd — 否则 /new 之后 /resume 会提示"请先 /cd
|
|
@@ -1876,6 +1937,11 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
1876
1937
|
],
|
|
1877
1938
|
});
|
|
1878
1939
|
}
|
|
1940
|
+
// Compact 按钮:仅 codex app-server 模式(runner 有 runCompact)且会话未在跑。
|
|
1941
|
+
// 在途 run 会占住 app-server runner,压缩必然失败,此时不渲染。
|
|
1942
|
+
if (!isActive && this.canCompactSession(this.config.defaultAgent)) {
|
|
1943
|
+
actions.push(resumeCompactButton(session.sessionId, this.config.defaultAgent));
|
|
1944
|
+
}
|
|
1879
1945
|
// New session button always shown (use shared function for consistency)
|
|
1880
1946
|
actions.push(newSessionButton());
|
|
1881
1947
|
// Use column_set+column for 2.0 (action tag not supported in 2.0)
|
|
@@ -2030,6 +2096,16 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2030
2096
|
},
|
|
2031
2097
|
};
|
|
2032
2098
|
}
|
|
2099
|
+
/**
|
|
2100
|
+
* Whether a session of the given agent kind can be compacted from a resume
|
|
2101
|
+
* card. Compact (thread/compact/start) is only implemented by the codex
|
|
2102
|
+
* app-server runner (`runCompact`), so the button is gated on
|
|
2103
|
+
* `serviceMode === 'app-server'` — exec mode spawns a fresh CLI per message
|
|
2104
|
+
* and has no persistent session to compact.
|
|
2105
|
+
*/
|
|
2106
|
+
canCompactSession(agentKind) {
|
|
2107
|
+
return (agentKind === 'codex' && getAgentConfig(this.config, 'codex')?.serviceMode === 'app-server');
|
|
2108
|
+
}
|
|
2033
2109
|
static LS_PAGE_SIZE = 30;
|
|
2034
2110
|
cmdLs(args, ctx, offset = 0) {
|
|
2035
2111
|
const cwd = this.sessionStore.getCwd(ctx.userId);
|
|
@@ -2551,7 +2627,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2551
2627
|
}
|
|
2552
2628
|
const card = {
|
|
2553
2629
|
schema: '2.0',
|
|
2554
|
-
config: { wide_screen_mode: true },
|
|
2630
|
+
config: { wide_screen_mode: true, update_multi: true },
|
|
2555
2631
|
header: { title: { tag: 'plain_text', content: 'Workspaces' } },
|
|
2556
2632
|
body: { elements: bodyElements },
|
|
2557
2633
|
};
|
|
@@ -2666,6 +2742,11 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2666
2742
|
behaviors: [{ type: 'callback', value: { cmd: 'stop', runId: activeRunRunId, cwd } }],
|
|
2667
2743
|
});
|
|
2668
2744
|
}
|
|
2745
|
+
// Compact 按钮:仅 codex app-server 模式(runner 有 runCompact)且会话未在跑。
|
|
2746
|
+
// agentKind 来自 /resume [agent] <id> / resume.use 卡片值,非 defaultAgent。
|
|
2747
|
+
if (!isActive && this.canCompactSession(agentKind)) {
|
|
2748
|
+
buttons.push(resumeCompactButton(sessionIdArg, agentKind));
|
|
2749
|
+
}
|
|
2669
2750
|
if (buttons.length > 0) {
|
|
2670
2751
|
elements.push({
|
|
2671
2752
|
tag: 'column_set',
|
package/dist/router/utils.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export declare function formatUsageStats(usage?: {
|
|
|
22
22
|
contextLength?: number;
|
|
23
23
|
contextLimit?: number;
|
|
24
24
|
compactCount?: number;
|
|
25
|
+
/** 压缩前上下文水位;与 contextLength(压缩后)成对出现在 Compact 卡上。 */
|
|
26
|
+
compactPreContextLength?: number;
|
|
25
27
|
cacheReadTokens?: number;
|
|
26
28
|
cacheCreationTokens?: number;
|
|
27
29
|
/** Agent-declared total; when present the display uses max(total, sum of parts). */
|
package/dist/router/utils.js
CHANGED
|
@@ -46,13 +46,16 @@ export function formatUsageStats(usage, options) {
|
|
|
46
46
|
if (options?.showResult && options?.result) {
|
|
47
47
|
lines.push(`结果 - ${options.result}`);
|
|
48
48
|
}
|
|
49
|
-
// 2. Context
|
|
49
|
+
// 2. Context 长度(compact 卡同时展示压缩前水位)
|
|
50
50
|
if (usage?.contextLength !== undefined) {
|
|
51
51
|
const ctxLimit = usage.contextLimit;
|
|
52
52
|
const ctxPercent = ctxLimit !== undefined && ctxLimit > 0
|
|
53
53
|
? ` (${Math.round((usage.contextLength / ctxLimit) * 100)}%)`
|
|
54
54
|
: '';
|
|
55
|
-
|
|
55
|
+
const preCompact = usage.compactPreContextLength !== undefined
|
|
56
|
+
? `(压缩前 ${formatTokenK(usage.compactPreContextLength)})`
|
|
57
|
+
: '';
|
|
58
|
+
lines.push(`Context - ${formatTokenK(usage.contextLength)}${ctxPercent}${preCompact}`);
|
|
56
59
|
}
|
|
57
60
|
// 3. Compact 次数
|
|
58
61
|
if (usage?.compactCount) {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-RPC client for the Codex App Server protocol.
|
|
3
|
+
*
|
|
4
|
+
* Manages the handshake, request/response matching, notification dispatch,
|
|
5
|
+
* and timeout handling over a JsonlRpcTransport.
|
|
6
|
+
*/
|
|
7
|
+
import type { JsonlRpcTransport } from './transport.js';
|
|
8
|
+
import { type InitializeResult } from './protocol-types.js';
|
|
9
|
+
export declare class RpcError extends Error {
|
|
10
|
+
readonly code: number;
|
|
11
|
+
readonly data?: unknown;
|
|
12
|
+
constructor(code: number, message: string, data?: unknown);
|
|
13
|
+
}
|
|
14
|
+
export declare class RpcTimeoutError extends Error {
|
|
15
|
+
constructor(message: string);
|
|
16
|
+
}
|
|
17
|
+
export declare class ConnectionLostError extends Error {
|
|
18
|
+
constructor(message?: string);
|
|
19
|
+
}
|
|
20
|
+
export interface ClientHooks {
|
|
21
|
+
onNotification(method: string, params: unknown): void;
|
|
22
|
+
onServerRequest(id: number | string, method: string, params: unknown): void;
|
|
23
|
+
onClose(): void;
|
|
24
|
+
}
|
|
25
|
+
export declare class CodexAppServerClient {
|
|
26
|
+
private transport;
|
|
27
|
+
/**
|
|
28
|
+
* Lifecycle hooks owned by the connection layer (ConnectionManager): slot
|
|
29
|
+
* cleanup + onConnectionLost. These must survive runner's setHooks().
|
|
30
|
+
*/
|
|
31
|
+
private baseHooks;
|
|
32
|
+
/** Per-run hooks from the runner, layered over baseHooks via setHooks(). */
|
|
33
|
+
private runHooks;
|
|
34
|
+
private requestTimeoutMs;
|
|
35
|
+
private pendingRequests;
|
|
36
|
+
private nextId;
|
|
37
|
+
private _ready;
|
|
38
|
+
private _disposed;
|
|
39
|
+
constructor(transport: JsonlRpcTransport, hooks: ClientHooks, requestTimeoutMs?: number);
|
|
40
|
+
/**
|
|
41
|
+
* Replace the per-run hooks (used by the runner to bridge
|
|
42
|
+
* notifications/server requests into the active turn's translator).
|
|
43
|
+
* Connection-layer hooks (slot cleanup / onConnectionLost) are preserved:
|
|
44
|
+
* review P2-1 — an overwrite here previously silenced onClose slot cleanup,
|
|
45
|
+
* leaving dead slots behind after the app-server process was killed.
|
|
46
|
+
*/
|
|
47
|
+
setHooks(hooks: ClientHooks): void;
|
|
48
|
+
get ready(): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Whether the underlying transport is still alive (not disposed and not
|
|
51
|
+
* closed). Used by the connection manager to avoid handing out a cached
|
|
52
|
+
* client whose child process was killed externally.
|
|
53
|
+
*/
|
|
54
|
+
get healthy(): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Connect and perform the initialize handshake.
|
|
57
|
+
* Returns the InitializeResult on success.
|
|
58
|
+
*/
|
|
59
|
+
connect(): Promise<InitializeResult>;
|
|
60
|
+
/**
|
|
61
|
+
* Send a JSON-RPC request and wait for the response.
|
|
62
|
+
*/
|
|
63
|
+
request<P, R>(method: string, params?: P): Promise<R>;
|
|
64
|
+
/**
|
|
65
|
+
* Send a JSON-RPC notification (no response expected).
|
|
66
|
+
*/
|
|
67
|
+
notify(method: string, params?: unknown): void;
|
|
68
|
+
/**
|
|
69
|
+
* Respond to a server request (success).
|
|
70
|
+
*/
|
|
71
|
+
respond(id: number | string, result: unknown): void;
|
|
72
|
+
/**
|
|
73
|
+
* Respond to a server request (error).
|
|
74
|
+
*/
|
|
75
|
+
respondError(id: number | string, code: number, message: string): void;
|
|
76
|
+
/**
|
|
77
|
+
* Dispose the client: cancel pending requests and close transport.
|
|
78
|
+
*/
|
|
79
|
+
dispose(): Promise<void>;
|
|
80
|
+
private handleMessage;
|
|
81
|
+
private failPending;
|
|
82
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-RPC client for the Codex App Server protocol.
|
|
3
|
+
*
|
|
4
|
+
* Manages the handshake, request/response matching, notification dispatch,
|
|
5
|
+
* and timeout handling over a JsonlRpcTransport.
|
|
6
|
+
*/
|
|
7
|
+
import { getLogger } from '../../../logger/index.js';
|
|
8
|
+
// =============================================================================
|
|
9
|
+
// Error Types
|
|
10
|
+
// =============================================================================
|
|
11
|
+
export class RpcError extends Error {
|
|
12
|
+
code;
|
|
13
|
+
data;
|
|
14
|
+
constructor(code, message, data) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'RpcError';
|
|
17
|
+
this.code = code;
|
|
18
|
+
this.data = data;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export class RpcTimeoutError extends Error {
|
|
22
|
+
constructor(message) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = 'RpcTimeoutError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export class ConnectionLostError extends Error {
|
|
28
|
+
constructor(message = 'Connection lost') {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = 'ConnectionLostError';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// =============================================================================
|
|
34
|
+
// Client
|
|
35
|
+
// =============================================================================
|
|
36
|
+
export class CodexAppServerClient {
|
|
37
|
+
transport;
|
|
38
|
+
/**
|
|
39
|
+
* Lifecycle hooks owned by the connection layer (ConnectionManager): slot
|
|
40
|
+
* cleanup + onConnectionLost. These must survive runner's setHooks().
|
|
41
|
+
*/
|
|
42
|
+
baseHooks;
|
|
43
|
+
/** Per-run hooks from the runner, layered over baseHooks via setHooks(). */
|
|
44
|
+
runHooks = null;
|
|
45
|
+
requestTimeoutMs;
|
|
46
|
+
pendingRequests = new Map();
|
|
47
|
+
nextId = 1;
|
|
48
|
+
_ready = false;
|
|
49
|
+
_disposed = false;
|
|
50
|
+
constructor(transport, hooks, requestTimeoutMs = 60_000) {
|
|
51
|
+
this.transport = transport;
|
|
52
|
+
this.baseHooks = hooks;
|
|
53
|
+
this.requestTimeoutMs = requestTimeoutMs;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Replace the per-run hooks (used by the runner to bridge
|
|
57
|
+
* notifications/server requests into the active turn's translator).
|
|
58
|
+
* Connection-layer hooks (slot cleanup / onConnectionLost) are preserved:
|
|
59
|
+
* review P2-1 — an overwrite here previously silenced onClose slot cleanup,
|
|
60
|
+
* leaving dead slots behind after the app-server process was killed.
|
|
61
|
+
*/
|
|
62
|
+
setHooks(hooks) {
|
|
63
|
+
this.runHooks = hooks;
|
|
64
|
+
}
|
|
65
|
+
get ready() {
|
|
66
|
+
return this._ready;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Whether the underlying transport is still alive (not disposed and not
|
|
70
|
+
* closed). Used by the connection manager to avoid handing out a cached
|
|
71
|
+
* client whose child process was killed externally.
|
|
72
|
+
*/
|
|
73
|
+
get healthy() {
|
|
74
|
+
return !this._disposed && !this.transport.closed;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Connect and perform the initialize handshake.
|
|
78
|
+
* Returns the InitializeResult on success.
|
|
79
|
+
*/
|
|
80
|
+
async connect() {
|
|
81
|
+
const events = {
|
|
82
|
+
onMessage: (msg) => this.handleMessage(msg),
|
|
83
|
+
onClose: () => {
|
|
84
|
+
this.failPending(new ConnectionLostError('transport closed'));
|
|
85
|
+
// 先通知连接层(slot 清理 + onConnectionLost),再通知当前 run 层
|
|
86
|
+
// (failTurn 等)。两层互不覆盖。
|
|
87
|
+
this.baseHooks.onClose();
|
|
88
|
+
this.runHooks?.onClose();
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
await this.transport.start(events);
|
|
92
|
+
// ENOENT/启动失败时 transport 已在 start() 内触发 onClose 并置 closed;
|
|
93
|
+
// 直接 fail-fast,避免 initialize 请求挂满 requestTimeoutMs 才报错。
|
|
94
|
+
if (this.transport.closed) {
|
|
95
|
+
throw new ConnectionLostError('codex app-server failed to start (binary missing or crashed)');
|
|
96
|
+
}
|
|
97
|
+
// Send initialize request
|
|
98
|
+
const initParams = {
|
|
99
|
+
clientInfo: {
|
|
100
|
+
name: 'lark-remote',
|
|
101
|
+
version: '1.0.0',
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
const result = await this.request('initialize', initParams);
|
|
105
|
+
this._ready = true;
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Send a JSON-RPC request and wait for the response.
|
|
110
|
+
*/
|
|
111
|
+
async request(method, params) {
|
|
112
|
+
if (this._disposed) {
|
|
113
|
+
throw new ConnectionLostError('client is disposed');
|
|
114
|
+
}
|
|
115
|
+
const id = this.nextId++;
|
|
116
|
+
const request = {
|
|
117
|
+
jsonrpc: '2.0',
|
|
118
|
+
id,
|
|
119
|
+
method,
|
|
120
|
+
params,
|
|
121
|
+
};
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
const timer = setTimeout(() => {
|
|
124
|
+
this.pendingRequests.delete(id);
|
|
125
|
+
reject(new RpcTimeoutError(`request timed out: ${method}`));
|
|
126
|
+
}, this.requestTimeoutMs);
|
|
127
|
+
this.pendingRequests.set(id, {
|
|
128
|
+
resolve: resolve,
|
|
129
|
+
reject,
|
|
130
|
+
timer,
|
|
131
|
+
});
|
|
132
|
+
this.transport.write(request);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Send a JSON-RPC notification (no response expected).
|
|
137
|
+
*/
|
|
138
|
+
notify(method, params) {
|
|
139
|
+
if (this._disposed)
|
|
140
|
+
return;
|
|
141
|
+
const notification = {
|
|
142
|
+
jsonrpc: '2.0',
|
|
143
|
+
method,
|
|
144
|
+
params,
|
|
145
|
+
};
|
|
146
|
+
this.transport.write(notification);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Respond to a server request (success).
|
|
150
|
+
*/
|
|
151
|
+
respond(id, result) {
|
|
152
|
+
const response = {
|
|
153
|
+
jsonrpc: '2.0',
|
|
154
|
+
id,
|
|
155
|
+
result,
|
|
156
|
+
};
|
|
157
|
+
this.transport.write(response);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Respond to a server request (error).
|
|
161
|
+
*/
|
|
162
|
+
respondError(id, code, message) {
|
|
163
|
+
const response = {
|
|
164
|
+
jsonrpc: '2.0',
|
|
165
|
+
id,
|
|
166
|
+
error: { code, message },
|
|
167
|
+
};
|
|
168
|
+
this.transport.write(response);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Dispose the client: cancel pending requests and close transport.
|
|
172
|
+
*/
|
|
173
|
+
async dispose() {
|
|
174
|
+
if (this._disposed)
|
|
175
|
+
return;
|
|
176
|
+
this._disposed = true;
|
|
177
|
+
this._ready = false;
|
|
178
|
+
this.failPending(new ConnectionLostError('client disposed'));
|
|
179
|
+
await this.transport.close();
|
|
180
|
+
}
|
|
181
|
+
// =========================================================================
|
|
182
|
+
// Internal
|
|
183
|
+
// =========================================================================
|
|
184
|
+
handleMessage(msg) {
|
|
185
|
+
const message = msg;
|
|
186
|
+
if ('id' in message && 'result' in message) {
|
|
187
|
+
// Success response
|
|
188
|
+
const id = message.id;
|
|
189
|
+
const pending = this.pendingRequests.get(id);
|
|
190
|
+
if (pending) {
|
|
191
|
+
clearTimeout(pending.timer);
|
|
192
|
+
this.pendingRequests.delete(id);
|
|
193
|
+
pending.resolve(message.result);
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if ('id' in message && 'error' in message) {
|
|
198
|
+
// Error response
|
|
199
|
+
const id = message.id;
|
|
200
|
+
const err = message.error;
|
|
201
|
+
const pending = this.pendingRequests.get(id);
|
|
202
|
+
if (pending) {
|
|
203
|
+
clearTimeout(pending.timer);
|
|
204
|
+
this.pendingRequests.delete(id);
|
|
205
|
+
pending.reject(new RpcError(err.code, err.message, err.data));
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if ('method' in message && !('id' in message)) {
|
|
210
|
+
// Notification
|
|
211
|
+
const method = message.method;
|
|
212
|
+
this.baseHooks.onNotification(method, message.params);
|
|
213
|
+
this.runHooks?.onNotification(method, message.params);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if ('method' in message && 'id' in message) {
|
|
217
|
+
// Server request (incoming request from server)
|
|
218
|
+
const id = message.id;
|
|
219
|
+
const method = message.method;
|
|
220
|
+
this.baseHooks.onServerRequest(id, method, message.params);
|
|
221
|
+
this.runHooks?.onServerRequest(id, method, message.params);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
getLogger().warn(`[codex-client] unknown message type: ${JSON.stringify(message).slice(0, 200)}`);
|
|
225
|
+
}
|
|
226
|
+
failPending(err) {
|
|
227
|
+
for (const [_id, pending] of this.pendingRequests) {
|
|
228
|
+
clearTimeout(pending.timer);
|
|
229
|
+
pending.reject(err);
|
|
230
|
+
}
|
|
231
|
+
this.pendingRequests.clear();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
//# sourceMappingURL=client.js.map
|