lark-remote 0.1.4 → 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 +38 -0
- package/README.en.md +0 -1
- package/README.md +0 -1
- 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 +423 -20
- 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 +78 -7
- package/dist/card/run-state.d.ts +25 -3
- package/dist/card/run-state.js +210 -2
- package/dist/config/dir.d.ts +1 -0
- package/dist/config/dir.js +4 -0
- package/dist/config/index.d.ts +43 -0
- package/dist/config/index.js +25 -1
- package/dist/connector/index.d.ts +5 -0
- package/dist/connector/index.js +32 -0
- package/dist/index.js +95 -23
- 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/index.d.ts +0 -10
- package/dist/router/config/index.js +0 -17
- package/dist/router/config/types.d.ts +1 -1
- package/dist/router/index.d.ts +61 -1
- package/dist/router/index.js +323 -48
- 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/argv.d.ts +1 -1
- package/dist/runner/codex/argv.js +1 -1
- 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/runner-utils.d.ts +12 -0
- package/dist/runner/common/runner-utils.js +21 -0
- package/dist/runner/common/spawning-runner.js +9 -1
- 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/claude/session-index.d.ts +93 -0
- package/dist/session/claude/session-index.js +374 -0
- package/dist/session/claude/sessions.d.ts +0 -18
- package/dist/session/claude/sessions.js +102 -319
- package/dist/session/codex/rollout-reader.js +46 -3
- package/dist/update/index.d.ts +10 -0
- package/dist/update/index.js +11 -0
- package/dist/update/install.d.ts +29 -0
- package/dist/update/install.js +93 -0
- package/dist/update/startup-hint.d.ts +5 -0
- package/dist/update/startup-hint.js +11 -0
- package/dist/update/version-check.d.ts +31 -0
- package/dist/update/version-check.js +149 -0
- package/package.json +1 -14
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,42 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.1.6] - 2026-08-14
|
|
9
|
+
|
|
10
|
+
### 新增
|
|
11
|
+
|
|
12
|
+
- Codex App Server 集成:通过 stdio JSON-RPC 持久连接 Codex CLI,支持审批流(文件读写/命令执行)、Compact 上下文压缩、空闲超时自动断开
|
|
13
|
+
- Approval 审批系统:`approval-coordinator` 协调超时/取消/回复,`approval-render` 渲染审批按钮卡片
|
|
14
|
+
- Codex runner 工厂:自动选择直连模式或 App Server 模式
|
|
15
|
+
- `rollout-reader`:Codex App Server rollout 开关读取
|
|
16
|
+
- Run card 审批区域渲染 + context limit 百分比显示
|
|
17
|
+
- 架构文档:`codex-app-server-integration.md`
|
|
18
|
+
|
|
19
|
+
### 修复
|
|
20
|
+
|
|
21
|
+
- `/update` 命令移除自动重启逻辑
|
|
22
|
+
- Codex 配置枚举回归官方标准值
|
|
23
|
+
|
|
24
|
+
## [0.1.5] - 2026-08-14
|
|
25
|
+
|
|
26
|
+
### 新增
|
|
27
|
+
|
|
28
|
+
- `/update` / `/update check` 命令:检查 npm registry 最新版本并一键升级,升级后自动重启 bridge;开发模式(`--dev`)下自动拒绝自更新
|
|
29
|
+
- `--update` CLI 参数:非交互式升级(适用于 cron / 脚本自动化),升级后退出
|
|
30
|
+
- `checkUpdateOnStartup` 配置项:启动时静默检查版本更新并推送提示(默认关闭)
|
|
31
|
+
- 纯内存 session index(`session-index.ts`):替代旧的 `readCwdFromJsonl`,完整 `cwdSet` 支持 EnterWorktree 连续搬迁 A→B→C 场景
|
|
32
|
+
- `verify-test-classification.ts` 脚本:测试分类校验工具
|
|
33
|
+
|
|
34
|
+
### 修复
|
|
35
|
+
|
|
36
|
+
- `/ws`、`/order` 卡片分页从 5 条/页调整为 15 条/页,修复飞书 ErrCode 11310 元素超限
|
|
37
|
+
- CLI `--dev` 帮助文本内部术语"看门狗"替换为用户可理解的"空闲超时自动停止"
|
|
38
|
+
|
|
39
|
+
### 变更
|
|
40
|
+
|
|
41
|
+
- 移除 husky / lint-staged pre-commit 和 pre-push 钩子及相关 devDependencies
|
|
42
|
+
- `sessions.ts` 重构:迁移到 `SessionIndex` + `parseSessionJsonl`,移除 `readCwdFromJsonl`
|
|
43
|
+
|
|
8
44
|
## [0.1.4] - 2026-08-13
|
|
9
45
|
|
|
10
46
|
### 新增
|
|
@@ -115,3 +151,5 @@ Initial release.
|
|
|
115
151
|
[0.1.2]: https://github.com/bungabungawoda/lark-remote/releases/tag/v0.1.2
|
|
116
152
|
[0.1.3]: https://github.com/bungabungawoda/lark-remote/releases/tag/v0.1.3
|
|
117
153
|
[0.1.4]: https://github.com/bungabungawoda/lark-remote/releases/tag/v0.1.4
|
|
154
|
+
[0.1.5]: https://github.com/bungabungawoda/lark-remote/releases/tag/v0.1.5
|
|
155
|
+
[0.1.6]: https://github.com/bungabungawoda/lark-remote/releases/tag/v0.1.6
|
package/README.en.md
CHANGED
|
@@ -150,7 +150,6 @@ Full docs under [`docs/`](docs/):
|
|
|
150
150
|
- Template for adding a new agent: [`docs/en/guides/add-new-agent.md`](docs/en/guides/add-new-agent.md)
|
|
151
151
|
- Codex config card guide: [`docs/en/guides/codex-config.md`](docs/en/guides/codex-config.md)
|
|
152
152
|
- Feishu CardKit 2.0 component reference: [Official Feishu docs](https://open.feishu.cn/document/feishu-cards/card-json-v2-components/component-json-v2-overview)
|
|
153
|
-
- AI collaboration rules and red lines: [`CLAUDE.md`](CLAUDE.md)
|
|
154
153
|
|
|
155
154
|
> This package is a CLI tool only — no programmatic API is exposed.
|
|
156
155
|
|
package/README.md
CHANGED
|
@@ -149,7 +149,6 @@ FEISHU_LIVE_TEST=1 bun run test tests/feishu-reaction-emoji-live.test.ts
|
|
|
149
149
|
- 新增 agent 接入模板:[`docs/zh/guides/add-new-agent.md`](docs/zh/guides/add-new-agent.md)
|
|
150
150
|
- Codex 配置卡片指南:[`docs/zh/guides/codex-config.md`](docs/zh/guides/codex-config.md)
|
|
151
151
|
- 飞书 CardKit 2.0 组件参考:[飞书开放平台官方文档](https://open.feishu.cn/document/feishu-cards/card-json-v2-components/component-json-v2-overview)
|
|
152
|
-
- AI 协作规则与红线:[`CLAUDE.md`](CLAUDE.md)
|
|
153
152
|
|
|
154
153
|
> 本包为纯 CLI 工具,不提供编程 API。
|
|
155
154
|
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ApprovalCoordinator: tracks and manages approval requests for a single run.
|
|
3
|
+
*
|
|
4
|
+
* Handles the lifecycle of command/file/permissions approval requests:
|
|
5
|
+
* - onRequested: add to tracking, arm timeout timer
|
|
6
|
+
* - onResolved: update state, clear timer
|
|
7
|
+
* - submit: validate → atomic state transition → async respond
|
|
8
|
+
* - togglePerm: flip permission item selection
|
|
9
|
+
* - onTurnEnded / onConnectionLost: mark all as expired
|
|
10
|
+
*/
|
|
11
|
+
import type { ApprovalRequestedEvent, ApprovalView, AgentEvent } from '../runner/types.js';
|
|
12
|
+
export type ApprovalState = 'pending' | 'submitting' | 'resolved' | 'failed' | 'expired';
|
|
13
|
+
export interface TrackedApproval {
|
|
14
|
+
requestId: number | string;
|
|
15
|
+
kind: 'command' | 'file' | 'permissions';
|
|
16
|
+
state: ApprovalState;
|
|
17
|
+
view: ApprovalView;
|
|
18
|
+
createdAt: number;
|
|
19
|
+
timeoutTimer: ReturnType<typeof setTimeout> | null;
|
|
20
|
+
}
|
|
21
|
+
export type ApprovalResponder = (requestId: number | string, response: unknown) => Promise<void>;
|
|
22
|
+
export type ApprovalAction = {
|
|
23
|
+
action: 'accept';
|
|
24
|
+
} | {
|
|
25
|
+
action: 'accept_for_session';
|
|
26
|
+
} | {
|
|
27
|
+
action: 'accept_with_execpolicy_amendment';
|
|
28
|
+
} | {
|
|
29
|
+
action: 'decline';
|
|
30
|
+
} | {
|
|
31
|
+
action: 'cancel';
|
|
32
|
+
};
|
|
33
|
+
export interface ApprovalToggleAction {
|
|
34
|
+
permId: string;
|
|
35
|
+
selected: boolean;
|
|
36
|
+
}
|
|
37
|
+
export declare class ApprovalCoordinator {
|
|
38
|
+
private approvals;
|
|
39
|
+
/** 已提交过的 nonce(按 requestId):同一按钮连点/飞书重投递只生效一次。 */
|
|
40
|
+
private submittedNonces;
|
|
41
|
+
private readonly runId;
|
|
42
|
+
private readonly userId;
|
|
43
|
+
private readonly chatId;
|
|
44
|
+
private readonly workspace;
|
|
45
|
+
private readonly approvalTimeoutMs;
|
|
46
|
+
private readonly responder;
|
|
47
|
+
private readonly interruptTurn;
|
|
48
|
+
private readonly pushToCard;
|
|
49
|
+
constructor(opts: {
|
|
50
|
+
runId: string;
|
|
51
|
+
userId: string;
|
|
52
|
+
chatId: string;
|
|
53
|
+
workspace: string;
|
|
54
|
+
approvalTimeoutMs: number;
|
|
55
|
+
responder: ApprovalResponder;
|
|
56
|
+
interruptTurn: () => Promise<void>;
|
|
57
|
+
pushToCard: (events: AgentEvent[]) => Promise<void>;
|
|
58
|
+
});
|
|
59
|
+
/**
|
|
60
|
+
* Handle an approval_requested event.
|
|
61
|
+
*/
|
|
62
|
+
onRequested(event: ApprovalRequestedEvent): void;
|
|
63
|
+
/**
|
|
64
|
+
* Handle an approval_resolved event.
|
|
65
|
+
*/
|
|
66
|
+
onResolved(requestId: number | string): void;
|
|
67
|
+
/**
|
|
68
|
+
* Handle turn ended — mark all pending approvals as expired.
|
|
69
|
+
*/
|
|
70
|
+
onTurnEnded(): void;
|
|
71
|
+
/**
|
|
72
|
+
* Handle connection lost — same as onTurnEnded.
|
|
73
|
+
*/
|
|
74
|
+
onConnectionLost(): void;
|
|
75
|
+
/**
|
|
76
|
+
* Submit an approval decision.
|
|
77
|
+
* Returns a promise that resolves when the response is sent.
|
|
78
|
+
*/
|
|
79
|
+
submit(action: ApprovalAction, ctx: {
|
|
80
|
+
requestId: number | string;
|
|
81
|
+
nonce?: string;
|
|
82
|
+
}): Promise<string>;
|
|
83
|
+
/**
|
|
84
|
+
* Toggle a permission item's selected state.
|
|
85
|
+
*/
|
|
86
|
+
togglePerm(action: ApprovalToggleAction, ctx: {
|
|
87
|
+
requestId: number | string;
|
|
88
|
+
}): Promise<string>;
|
|
89
|
+
/**
|
|
90
|
+
* Update a pending approval's view (out-of-order item/started arrives late).
|
|
91
|
+
* Only applies while the approval is still pending — resolved/expired
|
|
92
|
+
* approvals must not be resurrected or rewritten.
|
|
93
|
+
*/
|
|
94
|
+
updateView(requestId: number | string, view: ApprovalView): void;
|
|
95
|
+
/**
|
|
96
|
+
* Get the number of pending approvals.
|
|
97
|
+
*/
|
|
98
|
+
pendingCount(): number;
|
|
99
|
+
/**
|
|
100
|
+
* Get the head (oldest pending) approval, or undefined.
|
|
101
|
+
*/
|
|
102
|
+
head(): TrackedApproval | undefined;
|
|
103
|
+
private clearTimer;
|
|
104
|
+
private expireApproval;
|
|
105
|
+
private actionToDecision;
|
|
106
|
+
}
|
|
107
|
+
/** Map a card decision string to the ApprovalAction used across the bridge. */
|
|
108
|
+
export declare function decisionToApprovalAction(decision: string): ApprovalAction;
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ApprovalCoordinator: tracks and manages approval requests for a single run.
|
|
3
|
+
*
|
|
4
|
+
* Handles the lifecycle of command/file/permissions approval requests:
|
|
5
|
+
* - onRequested: add to tracking, arm timeout timer
|
|
6
|
+
* - onResolved: update state, clear timer
|
|
7
|
+
* - submit: validate → atomic state transition → async respond
|
|
8
|
+
* - togglePerm: flip permission item selection
|
|
9
|
+
* - onTurnEnded / onConnectionLost: mark all as expired
|
|
10
|
+
*/
|
|
11
|
+
import { getLogger } from '../logger/index.js';
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// Coordinator
|
|
14
|
+
// =============================================================================
|
|
15
|
+
export class ApprovalCoordinator {
|
|
16
|
+
approvals = new Map();
|
|
17
|
+
/** 已提交过的 nonce(按 requestId):同一按钮连点/飞书重投递只生效一次。 */
|
|
18
|
+
submittedNonces = new Map();
|
|
19
|
+
runId;
|
|
20
|
+
userId;
|
|
21
|
+
chatId;
|
|
22
|
+
workspace;
|
|
23
|
+
approvalTimeoutMs;
|
|
24
|
+
responder;
|
|
25
|
+
interruptTurn;
|
|
26
|
+
pushToCard;
|
|
27
|
+
constructor(opts) {
|
|
28
|
+
this.runId = opts.runId;
|
|
29
|
+
this.userId = opts.userId;
|
|
30
|
+
this.chatId = opts.chatId;
|
|
31
|
+
this.workspace = opts.workspace;
|
|
32
|
+
this.approvalTimeoutMs = opts.approvalTimeoutMs;
|
|
33
|
+
this.responder = opts.responder;
|
|
34
|
+
this.interruptTurn = opts.interruptTurn;
|
|
35
|
+
this.pushToCard = opts.pushToCard;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Handle an approval_requested event.
|
|
39
|
+
*/
|
|
40
|
+
onRequested(event) {
|
|
41
|
+
const existing = this.approvals.get(event.requestId);
|
|
42
|
+
if (existing) {
|
|
43
|
+
// Already tracked — update state
|
|
44
|
+
existing.state = 'pending';
|
|
45
|
+
existing.view = event.view;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const timer = setTimeout(() => {
|
|
49
|
+
this.expireApproval(event.requestId);
|
|
50
|
+
}, this.approvalTimeoutMs);
|
|
51
|
+
const tracked = {
|
|
52
|
+
requestId: event.requestId,
|
|
53
|
+
kind: event.kind,
|
|
54
|
+
state: 'pending',
|
|
55
|
+
view: event.view,
|
|
56
|
+
createdAt: Date.now(),
|
|
57
|
+
timeoutTimer: timer,
|
|
58
|
+
};
|
|
59
|
+
this.approvals.set(event.requestId, tracked);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Handle an approval_resolved event.
|
|
63
|
+
*/
|
|
64
|
+
onResolved(requestId) {
|
|
65
|
+
const tracked = this.approvals.get(requestId);
|
|
66
|
+
if (!tracked)
|
|
67
|
+
return;
|
|
68
|
+
tracked.state = 'resolved';
|
|
69
|
+
this.clearTimer(tracked);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Handle turn ended — mark all pending approvals as expired.
|
|
73
|
+
*/
|
|
74
|
+
onTurnEnded() {
|
|
75
|
+
for (const [, tracked] of this.approvals) {
|
|
76
|
+
if (tracked.state === 'pending') {
|
|
77
|
+
tracked.state = 'expired';
|
|
78
|
+
this.clearTimer(tracked);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Handle connection lost — same as onTurnEnded.
|
|
84
|
+
*/
|
|
85
|
+
onConnectionLost() {
|
|
86
|
+
this.onTurnEnded();
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Submit an approval decision.
|
|
90
|
+
* Returns a promise that resolves when the response is sent.
|
|
91
|
+
*/
|
|
92
|
+
async submit(action, ctx) {
|
|
93
|
+
const tracked = this.approvals.get(ctx.requestId);
|
|
94
|
+
if (!tracked) {
|
|
95
|
+
throw new Error(`Approval request ${ctx.requestId} not found`);
|
|
96
|
+
}
|
|
97
|
+
// nonce 去重:同一张卡片渲染出的按钮连续点击(或飞书重投递)携带相同
|
|
98
|
+
// nonce,只允许生效一次;不同渲染的按钮 nonce 不同,状态机(pending →
|
|
99
|
+
// submitting)仍然兜底防二次提交。
|
|
100
|
+
if (ctx.nonce !== undefined) {
|
|
101
|
+
const seen = this.submittedNonces.get(ctx.requestId) ?? new Set();
|
|
102
|
+
if (seen.has(ctx.nonce)) {
|
|
103
|
+
throw new Error(`Approval request ${ctx.requestId} already submitted (duplicate nonce)`);
|
|
104
|
+
}
|
|
105
|
+
seen.add(ctx.nonce);
|
|
106
|
+
this.submittedNonces.set(ctx.requestId, seen);
|
|
107
|
+
}
|
|
108
|
+
if (tracked.state !== 'pending') {
|
|
109
|
+
throw new Error(`Approval request ${ctx.requestId} is no longer pending (state=${tracked.state})`);
|
|
110
|
+
}
|
|
111
|
+
// Validate action against available decisions. decline/cancel are always
|
|
112
|
+
// allowed as a safety override: denying/interrupting is a universal human
|
|
113
|
+
// affordance even when the server's list omits them (observed in practice).
|
|
114
|
+
const decision = this.actionToDecision(action);
|
|
115
|
+
const safetyOverride = decision === 'decline' || decision === 'cancel';
|
|
116
|
+
if (!tracked.view.availableDecisions.includes(decision) && !safetyOverride) {
|
|
117
|
+
throw new Error(`Decision "${decision}" not in available decisions: ${tracked.view.availableDecisions.join(', ')}`);
|
|
118
|
+
}
|
|
119
|
+
// Atomic state transition: pending → submitting(同步,无 await)
|
|
120
|
+
tracked.state = 'submitting';
|
|
121
|
+
this.clearTimer(tracked);
|
|
122
|
+
// Send response via the responder
|
|
123
|
+
try {
|
|
124
|
+
await this.responder(ctx.requestId, action);
|
|
125
|
+
tracked.state = 'resolved';
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
tracked.state = 'failed';
|
|
129
|
+
}
|
|
130
|
+
return `Approval ${tracked.requestId} (${tracked.kind}): ${decision}`;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Toggle a permission item's selected state.
|
|
134
|
+
*/
|
|
135
|
+
async togglePerm(action, ctx) {
|
|
136
|
+
const tracked = this.approvals.get(ctx.requestId);
|
|
137
|
+
if (!tracked) {
|
|
138
|
+
throw new Error(`Approval request ${ctx.requestId} not found`);
|
|
139
|
+
}
|
|
140
|
+
if (tracked.kind !== 'permissions') {
|
|
141
|
+
throw new Error(`Approval request ${ctx.requestId} is not a permissions request`);
|
|
142
|
+
}
|
|
143
|
+
if (tracked.state !== 'pending') {
|
|
144
|
+
throw new Error(`Approval request ${ctx.requestId} is no longer pending`);
|
|
145
|
+
}
|
|
146
|
+
const items = tracked.view.permissions?.items;
|
|
147
|
+
if (!items) {
|
|
148
|
+
throw new Error(`Approval request ${ctx.requestId} has no permissions items`);
|
|
149
|
+
}
|
|
150
|
+
const item = items.find((i) => i.id === action.permId);
|
|
151
|
+
if (!item) {
|
|
152
|
+
throw new Error(`Permission item ${action.permId} not found in request ${ctx.requestId}`);
|
|
153
|
+
}
|
|
154
|
+
item.selected = action.selected;
|
|
155
|
+
return `Permission ${action.permId}: ${action.selected ? 'granted' : 'denied'}`;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Update a pending approval's view (out-of-order item/started arrives late).
|
|
159
|
+
* Only applies while the approval is still pending — resolved/expired
|
|
160
|
+
* approvals must not be resurrected or rewritten.
|
|
161
|
+
*/
|
|
162
|
+
updateView(requestId, view) {
|
|
163
|
+
const tracked = this.approvals.get(requestId);
|
|
164
|
+
if (!tracked || tracked.state !== 'pending')
|
|
165
|
+
return;
|
|
166
|
+
tracked.view = view;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Get the number of pending approvals.
|
|
170
|
+
*/
|
|
171
|
+
pendingCount() {
|
|
172
|
+
let count = 0;
|
|
173
|
+
for (const [, tracked] of this.approvals) {
|
|
174
|
+
if (tracked.state === 'pending')
|
|
175
|
+
count++;
|
|
176
|
+
}
|
|
177
|
+
return count;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Get the head (oldest pending) approval, or undefined.
|
|
181
|
+
*/
|
|
182
|
+
head() {
|
|
183
|
+
let oldest;
|
|
184
|
+
let oldestTime = Infinity;
|
|
185
|
+
for (const [, tracked] of this.approvals) {
|
|
186
|
+
if (tracked.state === 'pending' && tracked.createdAt < oldestTime) {
|
|
187
|
+
oldest = tracked;
|
|
188
|
+
oldestTime = tracked.createdAt;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return oldest;
|
|
192
|
+
}
|
|
193
|
+
// =========================================================================
|
|
194
|
+
// Internal
|
|
195
|
+
// =========================================================================
|
|
196
|
+
clearTimer(tracked) {
|
|
197
|
+
if (tracked.timeoutTimer !== null) {
|
|
198
|
+
clearTimeout(tracked.timeoutTimer);
|
|
199
|
+
tracked.timeoutTimer = null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
expireApproval(requestId) {
|
|
203
|
+
const tracked = this.approvals.get(requestId);
|
|
204
|
+
if (!tracked || tracked.state !== 'pending')
|
|
205
|
+
return;
|
|
206
|
+
tracked.state = 'expired';
|
|
207
|
+
this.clearTimer(tracked);
|
|
208
|
+
// 闭环:审批过期时向 server 发送 cancel(真实协议决策空间中的「停止等待」语义),
|
|
209
|
+
// 避免 server 无限等待审批响应。decline 仅是本桥 UI 安全兜底,不在真实协议内。
|
|
210
|
+
// 此前只改本地状态,server 只能等 runner 的 turn 超时兜底(用户「点了允许还超时」)。
|
|
211
|
+
void this.responder(requestId, { action: 'cancel' }).catch((err) => {
|
|
212
|
+
getLogger().warn(`[approval-coordinator] expiry cancel failed requestId=${requestId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
213
|
+
// 响应失败不是「正常过期」:状态升级为 failed,避免误判为已正常通知
|
|
214
|
+
// server(spec R1:responder 抛错时状态置 failed)。
|
|
215
|
+
tracked.state = 'failed';
|
|
216
|
+
// 兜底:cancel 未能送达时主动中断 turn,避免 server 无限等待审批响应。
|
|
217
|
+
void this.interruptTurn().catch((interruptErr) => {
|
|
218
|
+
getLogger().warn(`[approval-coordinator] expiry interrupt failed requestId=${requestId}: ${interruptErr instanceof Error ? interruptErr.message : String(interruptErr)}`);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
// 通知卡片进入过期态(run-renderer 渲染「⏰ 审批已过期」并隐藏按钮)。
|
|
222
|
+
void this.pushToCard([{ type: 'approval_expired', requestId }]).catch((err) => {
|
|
223
|
+
getLogger().warn(`[approval-coordinator] expiry card event failed requestId=${requestId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
actionToDecision(action) {
|
|
227
|
+
switch (action.action) {
|
|
228
|
+
case 'accept':
|
|
229
|
+
return 'accept';
|
|
230
|
+
case 'accept_for_session':
|
|
231
|
+
return 'acceptForSession';
|
|
232
|
+
case 'accept_with_execpolicy_amendment':
|
|
233
|
+
return 'acceptWithExecpolicyAmendment';
|
|
234
|
+
case 'decline':
|
|
235
|
+
return 'decline';
|
|
236
|
+
case 'cancel':
|
|
237
|
+
return 'cancel';
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/** Map a card decision string to the ApprovalAction used across the bridge. */
|
|
242
|
+
export function decisionToApprovalAction(decision) {
|
|
243
|
+
switch (decision) {
|
|
244
|
+
case 'accept':
|
|
245
|
+
return { action: 'accept' };
|
|
246
|
+
case 'acceptForSession':
|
|
247
|
+
return { action: 'accept_for_session' };
|
|
248
|
+
case 'acceptWithExecpolicyAmendment':
|
|
249
|
+
return { action: 'accept_with_execpolicy_amendment' };
|
|
250
|
+
case 'decline':
|
|
251
|
+
return { action: 'decline' };
|
|
252
|
+
case 'cancel':
|
|
253
|
+
return { action: 'cancel' };
|
|
254
|
+
default:
|
|
255
|
+
return { action: 'decline' };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
//# sourceMappingURL=approval-coordinator.js.map
|
package/dist/bridge/index.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ interface BridgeChannel extends RunCardChannel {
|
|
|
25
25
|
reconnect(): Promise<void>;
|
|
26
26
|
sendFile(chatId: string, filePath: string): Promise<string>;
|
|
27
27
|
addReaction(messageId: string, emoji: string): Promise<void>;
|
|
28
|
+
removeReactionByEmoji(messageId: string, emoji: string): Promise<void>;
|
|
28
29
|
}
|
|
29
30
|
/** Caller context for a bridge operation. */
|
|
30
31
|
interface BridgeContext {
|
|
@@ -118,6 +119,13 @@ export declare class Bridge {
|
|
|
118
119
|
private queueManager;
|
|
119
120
|
/** Active runs per workspace (cwd). Multiple workspaces can run in parallel. */
|
|
120
121
|
private activeRuns;
|
|
122
|
+
/** Approval coordinators keyed by runId. */
|
|
123
|
+
private approvalCoordinators;
|
|
124
|
+
/**
|
|
125
|
+
* 最近一次已完成的 codex app-server run(按 cwd)。run 结束后 activeRuns
|
|
126
|
+
* 已清空,Compact 需要它来校验 runId 并提供 sessionId。
|
|
127
|
+
*/
|
|
128
|
+
private lastCompactableCodexRun;
|
|
121
129
|
/**
|
|
122
130
|
* Active `!` bash runs, keyed by runId (NOT cwd). Bash runs bypass the serial
|
|
123
131
|
* queue and may run in parallel with claude runs / other bash runs in the same
|
|
@@ -324,6 +332,60 @@ export declare class Bridge {
|
|
|
324
332
|
* find and return to this session without scrolling up.
|
|
325
333
|
*/
|
|
326
334
|
private sendCompletionNotificationCard;
|
|
335
|
+
/**
|
|
336
|
+
* Handle an approval response from a card button click.
|
|
337
|
+
* Routes to the ApprovalCoordinator for the given run.
|
|
338
|
+
*/
|
|
339
|
+
handleApprovalRespond(opts: {
|
|
340
|
+
runId: string;
|
|
341
|
+
requestId: number | string;
|
|
342
|
+
decision: string;
|
|
343
|
+
scope?: 'turn' | 'session';
|
|
344
|
+
nonce: string;
|
|
345
|
+
}): Promise<void>;
|
|
346
|
+
/**
|
|
347
|
+
* Handle a permission toggle from a card button click.
|
|
348
|
+
* Routes to the ApprovalCoordinator for the given run.
|
|
349
|
+
*/
|
|
350
|
+
handleApprovalToggle(opts: {
|
|
351
|
+
runId: string;
|
|
352
|
+
requestId: number | string;
|
|
353
|
+
permId: string;
|
|
354
|
+
nonce: string;
|
|
355
|
+
selected?: boolean;
|
|
356
|
+
}): Promise<void>;
|
|
357
|
+
/**
|
|
358
|
+
* Handle codex.compact card action — trigger a Codex compaction request.
|
|
359
|
+
* Validates the run exists and is in a terminal state, then calls
|
|
360
|
+
* the app-server runner's runCompact().
|
|
361
|
+
*/
|
|
362
|
+
handleCodexCompact(value: {
|
|
363
|
+
runId?: string;
|
|
364
|
+
}, ctx: BridgeContext): Promise<void>;
|
|
365
|
+
/**
|
|
366
|
+
* Stream a Codex compaction to a card: start a RunCardSession with
|
|
367
|
+
* operationKind='compaction' (no recursive Compact button), consume
|
|
368
|
+
* runner.runCompact() events, read authoritative jsonl usage, and finish
|
|
369
|
+
* the card. Shared by handleCodexCompact (run card button) and
|
|
370
|
+
* handleResumeCompact (resume cards) so both flows stay in sync.
|
|
371
|
+
*
|
|
372
|
+
* Precondition: callers have already validated that `runner` implements
|
|
373
|
+
* runCompact and that the session exists in `cwd`.
|
|
374
|
+
*/
|
|
375
|
+
private streamCodexCompact;
|
|
376
|
+
/**
|
|
377
|
+
* Handle resume.compact card action — compact a session directly from a
|
|
378
|
+
* resume card (auto-resume / `/resume <id>`), without a runId.
|
|
379
|
+
*
|
|
380
|
+
* Unlike handleCodexCompact (which validates against the last finished run's
|
|
381
|
+
* runId), the resume card carries the sessionId + agent it was rendered for.
|
|
382
|
+
* Validation: the session must exist in the current cwd (same rule as
|
|
383
|
+
* cmdResume / resume.use) and the runner must implement runCompact.
|
|
384
|
+
*/
|
|
385
|
+
handleResumeCompact(value: {
|
|
386
|
+
sessionId?: string;
|
|
387
|
+
agent?: string;
|
|
388
|
+
}, ctx: BridgeContext): Promise<void>;
|
|
327
389
|
/**
|
|
328
390
|
* Execute a bash command and stream output to a card.
|
|
329
391
|
* `!` commands bypass the serial queue entirely (see body comment + §9.6):
|