lark-remote 0.5.0 → 0.5.2
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 +28 -0
- package/dist/bridge/index.d.ts +46 -8
- package/dist/bridge/index.js +104 -33
- package/dist/bridge/queue-manager.d.ts +59 -8
- package/dist/bridge/queue-manager.js +120 -40
- package/dist/cli.js +2 -1
- package/dist/index.js +14 -3
- package/dist/router/index.d.ts +56 -7
- package/dist/router/index.js +107 -13
- package/dist/runner/codex/app-server/runner.d.ts +28 -0
- package/dist/runner/codex/app-server/runner.js +54 -1
- package/dist/runner/codex/app-server/translator.js +10 -2
- package/dist/runner/types.d.ts +10 -1
- package/dist/runner/types.js +12 -1
- package/dist/session/codex/rollout-reader.js +24 -3
- package/package.json +1 -1
|
@@ -42,6 +42,8 @@ export declare class CodexAppServerRunner extends ConnectionBasedRunner<JsonRpcC
|
|
|
42
42
|
private connectionManager;
|
|
43
43
|
private currentTranslator;
|
|
44
44
|
private activeThreadId;
|
|
45
|
+
/** 本轮已记录过日志的外来(子 agent)线程 id,避免逐条 delta 刷盘。 */
|
|
46
|
+
private foreignThreadIds;
|
|
45
47
|
private model?;
|
|
46
48
|
private modelProvider?;
|
|
47
49
|
private reasoningEffort?;
|
|
@@ -91,7 +93,33 @@ export declare class CodexAppServerRunner extends ConnectionBasedRunner<JsonRpcC
|
|
|
91
93
|
* thread/resume(按 sessionId 恢复既有线程)→ turn/start。
|
|
92
94
|
*/
|
|
93
95
|
protected setupTurn(message: string, opts: SpawnOptions): Promise<void>;
|
|
96
|
+
/**
|
|
97
|
+
* Route one server notification into the current turn's translator.
|
|
98
|
+
*
|
|
99
|
+
* App-server 把已知线程的通知全部复用同一条 stdio 连接:agent 用
|
|
100
|
+
* multi-agent v2 派生子 agent 后,子线程的 `turn/started`、`item/*`、
|
|
101
|
+
* `turn/completed`、`thread/tokenUsage/updated` 会与本轮通知交错到达
|
|
102
|
+
* (2026-10-09 实测)。一个 turn 只属于一个线程,因此外来线程的通知一律
|
|
103
|
+
* 丢弃——否则子线程的 `turn/started` 会覆盖本轮 threadId,会话写回把子
|
|
104
|
+
* 线程 id 当成顶层会话指针存盘(冷连接 `thread/resume` 直接报
|
|
105
|
+
* "cannot resume an unloaded multi-agent v2 sub-agent"),且子线程的
|
|
106
|
+
* `turn/completed` 会提前结束本轮 run、token 用量也会串台。
|
|
107
|
+
*/
|
|
94
108
|
private handleNotification;
|
|
109
|
+
/**
|
|
110
|
+
* 线程归属闸门:通知是否属于本轮 turn 的线程。
|
|
111
|
+
*
|
|
112
|
+
* - 无 threadId 的通知(如部分 warning)没有线程维度可用,放行;
|
|
113
|
+
* - `serverRequest/resolved` 保持线程无关:审批按 requestId 关联(桥侧
|
|
114
|
+
* ApprovalCoordinator 跟踪),子 agent 的审批同样必须能结算,否则卡片
|
|
115
|
+
* 上的审批条目会一直挂在 pending。
|
|
116
|
+
*/
|
|
117
|
+
private belongsToActiveThread;
|
|
118
|
+
/**
|
|
119
|
+
* 每个被忽略的外来线程只记一条日志:这是会话指针被污染的入口,出问题时
|
|
120
|
+
* 必须能从日志直接看到「本轮忽略了哪些子线程」,同时避免逐条 delta 刷盘。
|
|
121
|
+
*/
|
|
122
|
+
private noteForeignThread;
|
|
95
123
|
private handleServerRequest;
|
|
96
124
|
private buildGrantedPermissions;
|
|
97
125
|
/** 拒绝权限审批:空 fileSystem + 关闭网络(真实 GrantedPermissionProfile)。 */
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { ConnectionManager, } from '../../common/jsonrpc/connection-manager.js';
|
|
14
14
|
import { CodexAppServerTranslator } from './translator.js';
|
|
15
|
+
import { NotificationMethod, } from './protocol-types.js';
|
|
15
16
|
import { RpcErrorCode } from '../../common/acp/protocol-types.js';
|
|
16
17
|
import { mapAnswersByIndex } from '../../question-common.js';
|
|
17
18
|
import { getLogger } from '../../../logger/index.js';
|
|
@@ -102,6 +103,8 @@ export class CodexAppServerRunner extends ConnectionBasedRunner {
|
|
|
102
103
|
connectionManager;
|
|
103
104
|
currentTranslator = null;
|
|
104
105
|
activeThreadId = null;
|
|
106
|
+
/** 本轮已记录过日志的外来(子 agent)线程 id,避免逐条 delta 刷盘。 */
|
|
107
|
+
foreignThreadIds = new Set();
|
|
105
108
|
model;
|
|
106
109
|
modelProvider;
|
|
107
110
|
reasoningEffort;
|
|
@@ -171,6 +174,7 @@ export class CodexAppServerRunner extends ConnectionBasedRunner {
|
|
|
171
174
|
clearTurnState() {
|
|
172
175
|
this.currentTranslator = null;
|
|
173
176
|
this.activeThreadId = null;
|
|
177
|
+
this.foreignThreadIds.clear();
|
|
174
178
|
}
|
|
175
179
|
async releaseConnection(cwd) {
|
|
176
180
|
await this.connectionManager.release(cwd);
|
|
@@ -362,7 +366,8 @@ export class CodexAppServerRunner extends ConnectionBasedRunner {
|
|
|
362
366
|
// 故 thread.id 可同时用作协议 threadId 与 store/session reader 的 session
|
|
363
367
|
// 键(bridge 用 turn_started 通知的 threadId 写回,/resume、Compact 按此
|
|
364
368
|
// 键定位 JSONL)。forked/subagent 线程二者会分叉(openai/codex#29327),
|
|
365
|
-
//
|
|
369
|
+
// 桥只把主线程作为顶层会话——子线程的通知由 handleNotification 的线程
|
|
370
|
+
// 归属闸门挡掉,不会进来(见 belongsToActiveThread)。
|
|
366
371
|
threadId = threadResult.thread.id;
|
|
367
372
|
}
|
|
368
373
|
this.activeThreadId = threadId;
|
|
@@ -372,10 +377,58 @@ export class CodexAppServerRunner extends ConnectionBasedRunner {
|
|
|
372
377
|
const turnResult = await client.request('turn/start', turnParams);
|
|
373
378
|
this.currentTurnId = turnResult.turn.id;
|
|
374
379
|
}
|
|
380
|
+
/**
|
|
381
|
+
* Route one server notification into the current turn's translator.
|
|
382
|
+
*
|
|
383
|
+
* App-server 把已知线程的通知全部复用同一条 stdio 连接:agent 用
|
|
384
|
+
* multi-agent v2 派生子 agent 后,子线程的 `turn/started`、`item/*`、
|
|
385
|
+
* `turn/completed`、`thread/tokenUsage/updated` 会与本轮通知交错到达
|
|
386
|
+
* (2026-10-09 实测)。一个 turn 只属于一个线程,因此外来线程的通知一律
|
|
387
|
+
* 丢弃——否则子线程的 `turn/started` 会覆盖本轮 threadId,会话写回把子
|
|
388
|
+
* 线程 id 当成顶层会话指针存盘(冷连接 `thread/resume` 直接报
|
|
389
|
+
* "cannot resume an unloaded multi-agent v2 sub-agent"),且子线程的
|
|
390
|
+
* `turn/completed` 会提前结束本轮 run、token 用量也会串台。
|
|
391
|
+
*/
|
|
375
392
|
handleNotification(method, params) {
|
|
393
|
+
if (!this.belongsToActiveThread(method, params)) {
|
|
394
|
+
this.noteForeignThread(params);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
376
397
|
const events = this.currentTranslator?.handleNotification(method, params) ?? [];
|
|
377
398
|
this.pushEvents(events);
|
|
378
399
|
}
|
|
400
|
+
/**
|
|
401
|
+
* 线程归属闸门:通知是否属于本轮 turn 的线程。
|
|
402
|
+
*
|
|
403
|
+
* - 无 threadId 的通知(如部分 warning)没有线程维度可用,放行;
|
|
404
|
+
* - `serverRequest/resolved` 保持线程无关:审批按 requestId 关联(桥侧
|
|
405
|
+
* ApprovalCoordinator 跟踪),子 agent 的审批同样必须能结算,否则卡片
|
|
406
|
+
* 上的审批条目会一直挂在 pending。
|
|
407
|
+
*/
|
|
408
|
+
belongsToActiveThread(method, params) {
|
|
409
|
+
const active = this.activeThreadId;
|
|
410
|
+
if (!active)
|
|
411
|
+
return true;
|
|
412
|
+
if (method === NotificationMethod.SERVER_REQUEST_RESOLVED)
|
|
413
|
+
return true;
|
|
414
|
+
const threadId = params?.threadId;
|
|
415
|
+
if (typeof threadId !== 'string' || threadId.length === 0)
|
|
416
|
+
return true;
|
|
417
|
+
return threadId === active;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* 每个被忽略的外来线程只记一条日志:这是会话指针被污染的入口,出问题时
|
|
421
|
+
* 必须能从日志直接看到「本轮忽略了哪些子线程」,同时避免逐条 delta 刷盘。
|
|
422
|
+
*/
|
|
423
|
+
noteForeignThread(params) {
|
|
424
|
+
const threadId = params?.threadId;
|
|
425
|
+
if (typeof threadId !== 'string' || threadId.length === 0)
|
|
426
|
+
return;
|
|
427
|
+
if (this.foreignThreadIds.has(threadId))
|
|
428
|
+
return;
|
|
429
|
+
this.foreignThreadIds.add(threadId);
|
|
430
|
+
getLogger().info(`[${this.logTag}] ignoring notifications from foreign thread threadId=${threadId} activeThreadId=${this.activeThreadId}`);
|
|
431
|
+
}
|
|
379
432
|
handleServerRequest(id, method, params) {
|
|
380
433
|
const events = this.currentTranslator?.handleServerRequest(id, method, params) ?? [];
|
|
381
434
|
if (events.length === 0) {
|
|
@@ -456,10 +456,18 @@ export class CodexAppServerTranslator {
|
|
|
456
456
|
}
|
|
457
457
|
handleTokenUsageUpdated(params) {
|
|
458
458
|
const last = params.tokenUsage.last;
|
|
459
|
+
// 协议原生 inputTokens 含 cached(total = input + output),而 lark-remote 的
|
|
460
|
+
// 卡片契约是 ccusage 口径「input_tokens 表示未命中缓存的输入」(design §9.21)。
|
|
461
|
+
// 在入口归一,否则 done 卡 Input 会等于 Context、cache% 分母重复计数、
|
|
462
|
+
// Total 被 max(total, input+output+cache) 抬高一倍(实测 364K → 727K)。
|
|
463
|
+
// jsonl 链路有同一处减法(session/codex/rollout-reader.ts buildUsageFromScan)。
|
|
464
|
+
const cachedInputTokens = last.cachedInputTokens === undefined
|
|
465
|
+
? undefined
|
|
466
|
+
: Math.min(last.cachedInputTokens, last.inputTokens);
|
|
459
467
|
this.usage = {
|
|
460
|
-
inputTokens: last.inputTokens,
|
|
468
|
+
inputTokens: last.inputTokens - (cachedInputTokens ?? 0),
|
|
461
469
|
outputTokens: last.outputTokens,
|
|
462
|
-
cachedInputTokens
|
|
470
|
+
cachedInputTokens,
|
|
463
471
|
totalTokens: last.totalTokens,
|
|
464
472
|
contextLimit: params.tokenUsage.modelContextWindow,
|
|
465
473
|
};
|
package/dist/runner/types.d.ts
CHANGED
|
@@ -402,7 +402,16 @@ export interface Runner {
|
|
|
402
402
|
*/
|
|
403
403
|
unregisterExitHandlers?(): void;
|
|
404
404
|
}
|
|
405
|
-
|
|
405
|
+
/**
|
|
406
|
+
* Agent 名单单源(运行时数组 + 类型同源,二者不可能漂移)。
|
|
407
|
+
*
|
|
408
|
+
* 历史上这份名单被手抄成多份(router 的 VALID_AGENTS、bridge 的 resume.compact
|
|
409
|
+
* 校验、各 agent 的 config builder…),漂移过:dsh 加入后 bridge 的
|
|
410
|
+
* resume.compact 仍只有 5 个,带 dsh 的 resume 卡片会静默退回 defaultAgent。
|
|
411
|
+
* 新增 agent 只改这里。
|
|
412
|
+
*/
|
|
413
|
+
export declare const AGENT_KINDS: readonly ["claude", "codex", "opencode", "pi", "kimi", "dsh"];
|
|
414
|
+
export type AgentKind = (typeof AGENT_KINDS)[number];
|
|
406
415
|
/** Unified session descriptor returned by every AgentSessionReader. */
|
|
407
416
|
export interface AgentSession {
|
|
408
417
|
sessionId: string;
|
package/dist/runner/types.js
CHANGED
|
@@ -4,5 +4,16 @@
|
|
|
4
4
|
* 本文件包含所有 Agent 相关的类型定义,从 runner/index.ts 提取。
|
|
5
5
|
* 所有下游模块应从本文件 import 类型,而非直接依赖实现。
|
|
6
6
|
*/
|
|
7
|
-
|
|
7
|
+
// =============================================================================
|
|
8
|
+
// Multi-Agent Adapter Types
|
|
9
|
+
// =============================================================================
|
|
10
|
+
/**
|
|
11
|
+
* Agent 名单单源(运行时数组 + 类型同源,二者不可能漂移)。
|
|
12
|
+
*
|
|
13
|
+
* 历史上这份名单被手抄成多份(router 的 VALID_AGENTS、bridge 的 resume.compact
|
|
14
|
+
* 校验、各 agent 的 config builder…),漂移过:dsh 加入后 bridge 的
|
|
15
|
+
* resume.compact 仍只有 5 个,带 dsh 的 resume 卡片会静默退回 defaultAgent。
|
|
16
|
+
* 新增 agent 只改这里。
|
|
17
|
+
*/
|
|
18
|
+
export const AGENT_KINDS = ['claude', 'codex', 'opencode', 'pi', 'kimi', 'dsh'];
|
|
8
19
|
//# sourceMappingURL=types.js.map
|
|
@@ -42,6 +42,15 @@ function subtractRawUsage(a, b) {
|
|
|
42
42
|
total_tokens: a.total_tokens - b.total_tokens,
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* `compacted` 事件自带的摘要调用 usage(`latest_token_usage_record.usage`)。
|
|
47
|
+
* 用于把「压缩前窗口」与「摘要调用自身的消耗」区分开——两者都写在 compacted 前。
|
|
48
|
+
*/
|
|
49
|
+
function readCompactedCallTotal(payload) {
|
|
50
|
+
const record = payload?.latest_token_usage_record;
|
|
51
|
+
const total = record?.usage?.total_tokens;
|
|
52
|
+
return typeof total === 'number' ? total : undefined;
|
|
53
|
+
}
|
|
45
54
|
export function readCodexRollout(filePath) {
|
|
46
55
|
if (!fs.existsSync(filePath)) {
|
|
47
56
|
return null;
|
|
@@ -136,6 +145,8 @@ function scanCodexRollout(filePath, includeEvents) {
|
|
|
136
145
|
// pi/opencode /resume "last turn" display semantics). When
|
|
137
146
|
// last_token_usage is absent we derive it as total - previous_total.
|
|
138
147
|
let lastRawUsage;
|
|
148
|
+
/** 上一条被接受的 raw usage;compacted 分支回退一条时用(见下方注释)。 */
|
|
149
|
+
let previousRawUsage;
|
|
139
150
|
// Context window limit from the same token_count event as lastRawUsage
|
|
140
151
|
// (codex reports info.model_context_window per turn; absent on old data).
|
|
141
152
|
let lastContextLimit;
|
|
@@ -202,6 +213,7 @@ function scanCodexRollout(filePath, includeEvents) {
|
|
|
202
213
|
lastTotalUsage = total;
|
|
203
214
|
}
|
|
204
215
|
if (raw && (raw.input_tokens || raw.cached_input_tokens || raw.output_tokens)) {
|
|
216
|
+
previousRawUsage = lastRawUsage;
|
|
205
217
|
lastRawUsage = raw;
|
|
206
218
|
const ctxWindow = info?.model_context_window;
|
|
207
219
|
lastContextLimit = typeof ctxWindow === 'number' ? ctxWindow : undefined;
|
|
@@ -223,10 +235,19 @@ function scanCodexRollout(filePath, includeEvents) {
|
|
|
223
235
|
}
|
|
224
236
|
}
|
|
225
237
|
else if (parsed.type === 'compacted') {
|
|
226
|
-
// 顶层 compacted 事件:压缩次数 +1
|
|
238
|
+
// 顶层 compacted 事件:压缩次数 +1,压缩前水位取「压缩调用之前」那条非零
|
|
239
|
+
// token_count 的 total_tokens。真实 rollout 落盘顺序是
|
|
240
|
+
// 末轮 → 摘要调用 → compacted → 压缩后水位:摘要调用自身的 token_count 紧贴
|
|
241
|
+
// 在 compacted 之前,直接取 lastRawUsage 会把摘要调用的消耗当成压缩前窗口
|
|
242
|
+
// (实测把 363K 的窗口显示成 207K)。compacted 自带的摘要调用 usage 用于判定
|
|
243
|
+
// 并回退一条;旧数据没有 latest_token_usage_record 时保持原语义。
|
|
227
244
|
compactCount++;
|
|
228
|
-
|
|
229
|
-
|
|
245
|
+
const compactCallTotal = readCompactedCallTotal(parsed.payload);
|
|
246
|
+
const preCompactUsage = compactCallTotal !== undefined && lastRawUsage?.total_tokens === compactCallTotal
|
|
247
|
+
? previousRawUsage
|
|
248
|
+
: lastRawUsage;
|
|
249
|
+
if (preCompactUsage?.total_tokens !== undefined) {
|
|
250
|
+
compactPreContextLength = preCompactUsage.total_tokens;
|
|
230
251
|
}
|
|
231
252
|
}
|
|
232
253
|
else if (includeEvents && parsed.type === 'response_item' && parsed.payload) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lark-remote",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Feishu (Lark) private-chat bridge to local coding agents — Claude Code / Codex / opencode / pi / kimi, with live-streaming CardKit 2.0 cards",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|