lark-remote 0.5.3 → 0.5.5
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 +1 -1
- package/README.md +1 -1
- package/dist/bridge/approval-coordinator.d.ts +18 -1
- package/dist/bridge/approval-coordinator.js +57 -10
- package/dist/bridge/index.js +90 -17
- package/dist/card/approval-render.js +18 -0
- package/dist/card/run-state.js +10 -5
- package/dist/config/index.d.ts +16 -0
- package/dist/config/index.js +46 -6
- package/dist/config/opencode-config.d.ts +3 -3
- package/dist/config/opencode-config.js +38 -22
- package/dist/inbound/authorize.d.ts +34 -0
- package/dist/inbound/authorize.js +31 -0
- package/dist/index.js +116 -63
- package/dist/instance-lock.d.ts +9 -0
- package/dist/instance-lock.js +113 -42
- package/dist/router/config/dsh.d.ts +51 -21
- package/dist/router/config/dsh.js +255 -66
- package/dist/router/config/opencode.js +16 -6
- package/dist/router/config/types.d.ts +6 -3
- package/dist/router/index.d.ts +1 -0
- package/dist/router/index.js +13 -5
- package/dist/runner/codex/app-server/translator.js +21 -7
- package/dist/runner/common/acp/base-acp-runner.d.ts +12 -0
- package/dist/runner/common/acp/base-acp-runner.js +83 -8
- package/dist/runner/common/acp/protocol-types.d.ts +10 -2
- package/dist/runner/common/acp/protocol-types.js +3 -1
- package/dist/runner/common/connection-based-runner.d.ts +15 -0
- package/dist/runner/common/connection-based-runner.js +40 -5
- package/dist/runner/common/jsonrpc/connection-manager.d.ts +15 -0
- package/dist/runner/common/jsonrpc/connection-manager.js +83 -50
- package/dist/runner/dsh/approval.d.ts +53 -0
- package/dist/runner/dsh/approval.js +161 -0
- package/dist/runner/dsh/auth.d.ts +36 -0
- package/dist/runner/dsh/auth.js +118 -0
- package/dist/runner/dsh/client.d.ts +161 -42
- package/dist/runner/dsh/client.js +551 -244
- package/dist/runner/dsh/daemon.d.ts +85 -0
- package/dist/runner/dsh/daemon.js +272 -0
- package/dist/runner/dsh/merge.d.ts +9 -0
- package/dist/runner/dsh/merge.js +51 -0
- package/dist/runner/dsh/runner.d.ts +94 -11
- package/dist/runner/dsh/runner.js +553 -67
- package/dist/runner/dsh/translator.d.ts +43 -3
- package/dist/runner/dsh/translator.js +115 -38
- package/dist/runner/dsh/types.d.ts +189 -27
- package/dist/runner/dsh/types.js +11 -5
- package/dist/runner/kimi/acp/runner.js +15 -3
- package/dist/runner/opencode/acp/error-monitor.d.ts +2 -2
- package/dist/runner/opencode/acp/error-monitor.js +2 -2
- package/dist/runner/opencode/acp/runner.d.ts +9 -0
- package/dist/runner/opencode/acp/runner.js +40 -1
- package/dist/runner/opencode/acp/translator.d.ts +4 -2
- package/dist/runner/opencode/acp/translator.js +12 -3
- package/dist/runner/pi/rpc/translator.d.ts +2 -0
- package/dist/runner/pi/rpc/translator.js +27 -1
- package/dist/runner/types.d.ts +34 -0
- package/dist/runner/types.js +24 -0
- package/dist/session/codex/rollout-reader.js +44 -7
- package/dist/session/dsh/sessions.d.ts +45 -6
- package/dist/session/dsh/sessions.js +171 -27
- package/dist/session/opencode/sessions.d.ts +18 -7
- package/dist/session/opencode/sessions.js +294 -15
- package/dist/session/pi/sessions.js +6 -2
- package/package.json +3 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* OpencodeLogErrorMonitor: watches opencode's own log file for LLM stream
|
|
3
3
|
* errors of the active ACP session and reports them immediately.
|
|
4
4
|
*
|
|
5
|
-
* Why this exists: opencode ACP (v1.18.x) does NOT forward retry/error state
|
|
5
|
+
* Why this exists: opencode ACP (v1.18.x and v2.0.x) does NOT forward retry/error state
|
|
6
6
|
* to the client over the ACP protocol. Its session/update notifications are
|
|
7
7
|
* limited to content deltas (agent_message_chunk / agent_thought_chunk /
|
|
8
8
|
* tool_call / tool_call_update), usage and available-commands updates — there
|
|
@@ -36,7 +36,7 @@ const ERROR_MESSAGE_RE = /error\.error="((?:[^"\\]|\\.)*)"/;
|
|
|
36
36
|
* Resolve opencode's log file path from the XDG data dir
|
|
37
37
|
* (`$XDG_DATA_HOME/opencode/log/opencode.log`, default
|
|
38
38
|
* `~/.local/share/opencode/log/opencode.log`). Matches opencode's own
|
|
39
|
-
* data-dir layout (
|
|
39
|
+
* data-dir layout (verified against v1.18.x and v2.0.32 `debug paths`).
|
|
40
40
|
*/
|
|
41
41
|
export function resolveOpencodeLogPath() {
|
|
42
42
|
const dataHome = process.env.XDG_DATA_HOME || join(homedir(), '.local', 'share');
|
|
@@ -84,6 +84,15 @@ export declare class OpencodeAcpRunner extends BaseAcpRunner<OpencodeAcpTranslat
|
|
|
84
84
|
*/
|
|
85
85
|
protected onCompactPromptFired(promptPromise: Promise<SessionPromptResult>, translator: OpencodeAcpTranslator, sessionId: string, _opts: unknown, _baselineTotalCount: number): void;
|
|
86
86
|
protected buildApprovalOutcome(action: string, pending: AcpPendingApproval, _response: unknown): unknown;
|
|
87
|
+
/**
|
|
88
|
+
* opencode 2 is ACP client → shared background daemon. The daemon can be
|
|
89
|
+
* starting or still activating location plugins when the first session is
|
|
90
|
+
* created; both surface as a JSON-RPC internal error. Retry a few times
|
|
91
|
+
* before failing the turn.
|
|
92
|
+
*/
|
|
93
|
+
protected sessionSetupRetryDelaysMs(): number[];
|
|
94
|
+
protected isRetryableSessionSetupError(err: unknown): boolean;
|
|
95
|
+
protected enrichSessionSetupError(err: unknown): Error;
|
|
87
96
|
getStatusInfo(): AgentStatusInfo;
|
|
88
97
|
/**
|
|
89
98
|
* Hot-apply a mode change to the live ACP session.
|
|
@@ -56,6 +56,13 @@ export class OpencodeAcpRunner extends BaseAcpRunner {
|
|
|
56
56
|
terminal: false,
|
|
57
57
|
},
|
|
58
58
|
},
|
|
59
|
+
onConnected: (result) => {
|
|
60
|
+
const version = result?.agentInfo
|
|
61
|
+
?.version;
|
|
62
|
+
if (version) {
|
|
63
|
+
getLogger().info(`[opencode-acp-runner] connected to OpenCode ${version}`);
|
|
64
|
+
}
|
|
65
|
+
},
|
|
59
66
|
};
|
|
60
67
|
super({
|
|
61
68
|
kind: opts.kind,
|
|
@@ -144,11 +151,43 @@ export class OpencodeAcpRunner extends BaseAcpRunner {
|
|
|
144
151
|
* translation (no wire.jsonl polling, unlike kimi).
|
|
145
152
|
*/
|
|
146
153
|
onCompactPromptFired(promptPromise, translator, sessionId, _opts, _baselineTotalCount) {
|
|
147
|
-
|
|
154
|
+
// 与 setupTurn 相同,捕获发起压缩那一轮的代际 + client;
|
|
155
|
+
// 旧轮迟到的 prompt 结算不得写入新轮的共享队列。
|
|
156
|
+
const generation = this.turnGeneration;
|
|
157
|
+
const client = this.currentClient;
|
|
158
|
+
promptPromise.then((result) => {
|
|
159
|
+
if (!this.isCurrentTurn(generation, client))
|
|
160
|
+
return;
|
|
161
|
+
this.handlePromptResolved(result, translator, sessionId);
|
|
162
|
+
}, (err) => {
|
|
163
|
+
if (!this.isCurrentTurn(generation, client))
|
|
164
|
+
return;
|
|
165
|
+
this.handlePromptRejected(err, translator, sessionId, 'compact');
|
|
166
|
+
});
|
|
148
167
|
}
|
|
149
168
|
buildApprovalOutcome(action, pending, _response) {
|
|
150
169
|
return buildAcpPermissionOutcome(action, pending.options, OPENCODE_APPROVAL_KINDS);
|
|
151
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* opencode 2 is ACP client → shared background daemon. The daemon can be
|
|
173
|
+
* starting or still activating location plugins when the first session is
|
|
174
|
+
* created; both surface as a JSON-RPC internal error. Retry a few times
|
|
175
|
+
* before failing the turn.
|
|
176
|
+
*/
|
|
177
|
+
sessionSetupRetryDelaysMs() {
|
|
178
|
+
return [500, 1500, 3000];
|
|
179
|
+
}
|
|
180
|
+
isRetryableSessionSetupError(err) {
|
|
181
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
182
|
+
return /ServiceUnavailableError|Location plugins did not finish activating|Internal service failure|ECONNREFUSED|fetch failed/i.test(message);
|
|
183
|
+
}
|
|
184
|
+
enrichSessionSetupError(err) {
|
|
185
|
+
if (!this.isRetryableSessionSetupError(err)) {
|
|
186
|
+
return super.enrichSessionSetupError(err);
|
|
187
|
+
}
|
|
188
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
189
|
+
return new Error(`opencode 2.0 后台服务不可用(${message})。请在终端执行 \`opencode service restart\` 后重试。`, { cause: err });
|
|
190
|
+
}
|
|
152
191
|
getStatusInfo() {
|
|
153
192
|
return {
|
|
154
193
|
kind: this.kind,
|
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
* `props.delta` verbatim), so text/thinking chunks go straight onto the
|
|
10
10
|
* `assistant` incremental channel — NO accumulation, NO turn_diff snapshot.
|
|
11
11
|
* (Contrast kimi: same field is also a delta but kimi's contract accumulates
|
|
12
|
-
* into turn_diff snapshots
|
|
13
|
-
*
|
|
12
|
+
* into turn_diff snapshots. Both end as verbatim text in run-state: the
|
|
13
|
+
* assistant channel appends deltas sharing one `itemId`, the snapshot channel
|
|
14
|
+
* replaces by itemId. opencode's `agent_thought_chunk.messageId`
|
|
15
|
+
* (`<msg>:reasoning:<ordinal>`) IS that block identity — pass it through.)
|
|
14
16
|
*
|
|
15
17
|
* Event mapping (source: opencode acp/service.ts + event.ts + tool.ts,
|
|
16
18
|
* dev@1c965451b5):
|
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
* `props.delta` verbatim), so text/thinking chunks go straight onto the
|
|
10
10
|
* `assistant` incremental channel — NO accumulation, NO turn_diff snapshot.
|
|
11
11
|
* (Contrast kimi: same field is also a delta but kimi's contract accumulates
|
|
12
|
-
* into turn_diff snapshots
|
|
13
|
-
*
|
|
12
|
+
* into turn_diff snapshots. Both end as verbatim text in run-state: the
|
|
13
|
+
* assistant channel appends deltas sharing one `itemId`, the snapshot channel
|
|
14
|
+
* replaces by itemId. opencode's `agent_thought_chunk.messageId`
|
|
15
|
+
* (`<msg>:reasoning:<ordinal>`) IS that block identity — pass it through.)
|
|
14
16
|
*
|
|
15
17
|
* Event mapping (source: opencode acp/service.ts + event.ts + tool.ts,
|
|
16
18
|
* dev@1c965451b5):
|
|
@@ -54,7 +56,14 @@ export class OpencodeAcpTranslator extends BaseAcpTranslator {
|
|
|
54
56
|
{
|
|
55
57
|
type: 'assistant',
|
|
56
58
|
message: {
|
|
57
|
-
content: [
|
|
59
|
+
content: [
|
|
60
|
+
{
|
|
61
|
+
type: 'thinking',
|
|
62
|
+
thinking: event.content.text,
|
|
63
|
+
// 同一 reasoning part 的续写 delta 共享 messageId;换 part 才开新块。
|
|
64
|
+
...(event.messageId !== undefined ? { itemId: event.messageId } : {}),
|
|
65
|
+
},
|
|
66
|
+
],
|
|
58
67
|
},
|
|
59
68
|
timestamp: new Date().toISOString(),
|
|
60
69
|
},
|
|
@@ -34,6 +34,8 @@ export declare class PiRpcTranslator {
|
|
|
34
34
|
private hasUsage;
|
|
35
35
|
/** toolcall_start bookkeeping:占位不发事件,end 时补发完整 tool_use。 */
|
|
36
36
|
private pendingToolCall;
|
|
37
|
+
/** 思考块序号:每条消息的每个 thinking 块一个稳定 itemId。 */
|
|
38
|
+
private thinkingSeq;
|
|
37
39
|
/** Provider failure (stopReason="error") on the final assistant message. */
|
|
38
40
|
private lastAssistantStopReason;
|
|
39
41
|
private lastAssistantErrorMessage;
|
|
@@ -25,6 +25,8 @@ export class PiRpcTranslator {
|
|
|
25
25
|
hasUsage = false;
|
|
26
26
|
/** toolcall_start bookkeeping:占位不发事件,end 时补发完整 tool_use。 */
|
|
27
27
|
pendingToolCall = false;
|
|
28
|
+
/** 思考块序号:每条消息的每个 thinking 块一个稳定 itemId。 */
|
|
29
|
+
thinkingSeq = 0;
|
|
28
30
|
/** Provider failure (stopReason="error") on the final assistant message. */
|
|
29
31
|
lastAssistantStopReason;
|
|
30
32
|
lastAssistantErrorMessage;
|
|
@@ -192,9 +194,16 @@ export class PiRpcTranslator {
|
|
|
192
194
|
this.lastAssistantErrorMessage = m.stopReason === 'error' ? m.errorMessage : undefined;
|
|
193
195
|
if (m.usage)
|
|
194
196
|
this.addUsage(m.usage);
|
|
197
|
+
// 块生产者职责:连续 thinking 块在这里合并并写入 '\n' 段落分隔,
|
|
198
|
+
// 再给每个块一个 itemId。run-state 只按 itemId 逐字追加,不猜分隔符。
|
|
199
|
+
const content = mergeConsecutiveThinking(this.content);
|
|
200
|
+
for (const block of content) {
|
|
201
|
+
if (block.type === 'thinking')
|
|
202
|
+
block.itemId = `pi:${++this.thinkingSeq}`;
|
|
203
|
+
}
|
|
195
204
|
const event = {
|
|
196
205
|
type: 'assistant',
|
|
197
|
-
message: { content
|
|
206
|
+
message: { content },
|
|
198
207
|
timestamp: new Date().toISOString(),
|
|
199
208
|
};
|
|
200
209
|
this.content = [];
|
|
@@ -291,4 +300,21 @@ export class PiRpcTranslator {
|
|
|
291
300
|
}
|
|
292
301
|
}
|
|
293
302
|
}
|
|
303
|
+
/**
|
|
304
|
+
* 合并同一消息内连续的 thinking 块(块间用 '\n'),保留其余内容顺序。
|
|
305
|
+
* 与 c0e1fb7 的“避免碎片化思考面板”决定一致。
|
|
306
|
+
*/
|
|
307
|
+
function mergeConsecutiveThinking(content) {
|
|
308
|
+
const out = [];
|
|
309
|
+
for (const block of content) {
|
|
310
|
+
const last = out[out.length - 1];
|
|
311
|
+
if (block.type === 'thinking' && last?.type === 'thinking') {
|
|
312
|
+
out[out.length - 1] = { ...last, thinking: `${last.thinking}\n${block.thinking}` };
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
out.push(block.type === 'thinking' ? { ...block } : block);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return out;
|
|
319
|
+
}
|
|
294
320
|
//# sourceMappingURL=translator.js.map
|
package/dist/runner/types.d.ts
CHANGED
|
@@ -148,6 +148,11 @@ export interface ApprovalView {
|
|
|
148
148
|
requestId: number | string;
|
|
149
149
|
kind: 'command' | 'file' | 'permissions' | 'question' | 'tool';
|
|
150
150
|
reason?: string;
|
|
151
|
+
/**
|
|
152
|
+
* Agent-agnostic scope note rendered at the top of the approval area (e.g. DSH's
|
|
153
|
+
* "this grant applies to one call only"). Keep the audit reason untouched.
|
|
154
|
+
*/
|
|
155
|
+
note?: string;
|
|
151
156
|
/**
|
|
152
157
|
* 提问卡顶部概要行(数据驱动,无 agent 分支):Kimi elicitation form 的
|
|
153
158
|
* 题面只在 form 级 message 里合并出现,逐字段 title 可能只是短 header,
|
|
@@ -201,10 +206,24 @@ export interface ApprovalView {
|
|
|
201
206
|
availableDecisions: string[];
|
|
202
207
|
/** Raw payloads for structured decisions (e.g. acceptWithExecpolicyAmendment). */
|
|
203
208
|
decisionPayloads?: Record<string, unknown>;
|
|
209
|
+
/**
|
|
210
|
+
* 最近一次响应发送失败的原因。协调器失败后回滚 pending 并写回
|
|
211
|
+
* 该字段,卡片据此渲染「提交失败,可重试」,不再让用户误以为已提交。
|
|
212
|
+
*/
|
|
213
|
+
deliveryError?: string;
|
|
204
214
|
}
|
|
205
215
|
interface ThinkingContent {
|
|
206
216
|
type: 'thinking';
|
|
207
217
|
thinking: string;
|
|
218
|
+
/**
|
|
219
|
+
* 思考块身份。同一 itemId 的连续 `thinking` 内容是**同一块的增量**,run-state
|
|
220
|
+
* 逐字拼接(不插入任何分隔符);itemId 变化才开启新块。缺省表示"续写当前块"
|
|
221
|
+
* (块生产者必须显式给 id,避免与上一块粘连)。
|
|
222
|
+
*
|
|
223
|
+
* 来源:opencode `agent_thought_chunk.messageId`(`<msg>:reasoning:<ordinal>`)、
|
|
224
|
+
* DSH `reasoning-delta` 的 attemptId+index、pi 每条消息的块序号。
|
|
225
|
+
*/
|
|
226
|
+
itemId?: string;
|
|
208
227
|
}
|
|
209
228
|
interface TextContent {
|
|
210
229
|
type: 'text';
|
|
@@ -412,6 +431,21 @@ export interface Runner {
|
|
|
412
431
|
*/
|
|
413
432
|
export declare const AGENT_KINDS: readonly ["claude", "codex", "opencode", "pi", "kimi", "dsh"];
|
|
414
433
|
export type AgentKind = (typeof AGENT_KINDS)[number];
|
|
434
|
+
/**
|
|
435
|
+
* Compact 能力声明(运行时门控的能力清单,编译期强制完整)。
|
|
436
|
+
*
|
|
437
|
+
* 键集就是 `AgentKind`:新增 agent 时缺一项 = TypeScript 报错,必须显式回答
|
|
438
|
+
* 「这个 agent 能不能压缩」。历史教训:能力只由 runner 上有没有 `runCompact`
|
|
439
|
+
* 这个鸭子方法隐式决定,dsh 接入时漏了实现,run 卡片直接不渲染按钮——没有
|
|
440
|
+
* 报错、没有置灰、没有日志,用户只能看到一个缺失的按钮(2026-10-17)。
|
|
441
|
+
*
|
|
442
|
+
* 声明与实现的唯一性由 `src/runner/runner-structure.test.ts` 的矩阵用例保证:
|
|
443
|
+
* 每个 kind 的声明值必须等于对应 runner 类是否实现 `runCompact`。bridge 侧
|
|
444
|
+
* 仍按「声明 && 实现」双重门控(fail closed),不一致时打 error 日志。
|
|
445
|
+
*/
|
|
446
|
+
export declare const COMPACT_CAPABILITY: Readonly<Record<AgentKind, boolean>>;
|
|
447
|
+
/** 该 agent 是否声明支持 compact(渲染门控与执行门控共用的唯一判据)。 */
|
|
448
|
+
export declare function supportsCompact(kind: AgentKind): boolean;
|
|
415
449
|
/** Unified session descriptor returned by every AgentSessionReader. */
|
|
416
450
|
export interface AgentSession {
|
|
417
451
|
sessionId: string;
|
package/dist/runner/types.js
CHANGED
|
@@ -16,4 +16,28 @@
|
|
|
16
16
|
* 新增 agent 只改这里。
|
|
17
17
|
*/
|
|
18
18
|
export const AGENT_KINDS = ['claude', 'codex', 'opencode', 'pi', 'kimi', 'dsh'];
|
|
19
|
+
/**
|
|
20
|
+
* Compact 能力声明(运行时门控的能力清单,编译期强制完整)。
|
|
21
|
+
*
|
|
22
|
+
* 键集就是 `AgentKind`:新增 agent 时缺一项 = TypeScript 报错,必须显式回答
|
|
23
|
+
* 「这个 agent 能不能压缩」。历史教训:能力只由 runner 上有没有 `runCompact`
|
|
24
|
+
* 这个鸭子方法隐式决定,dsh 接入时漏了实现,run 卡片直接不渲染按钮——没有
|
|
25
|
+
* 报错、没有置灰、没有日志,用户只能看到一个缺失的按钮(2026-10-17)。
|
|
26
|
+
*
|
|
27
|
+
* 声明与实现的唯一性由 `src/runner/runner-structure.test.ts` 的矩阵用例保证:
|
|
28
|
+
* 每个 kind 的声明值必须等于对应 runner 类是否实现 `runCompact`。bridge 侧
|
|
29
|
+
* 仍按「声明 && 实现」双重门控(fail closed),不一致时打 error 日志。
|
|
30
|
+
*/
|
|
31
|
+
export const COMPACT_CAPABILITY = {
|
|
32
|
+
claude: true,
|
|
33
|
+
codex: true,
|
|
34
|
+
opencode: true,
|
|
35
|
+
pi: true,
|
|
36
|
+
kimi: true,
|
|
37
|
+
dsh: true,
|
|
38
|
+
};
|
|
39
|
+
/** 该 agent 是否声明支持 compact(渲染门控与执行门控共用的唯一判据)。 */
|
|
40
|
+
export function supportsCompact(kind) {
|
|
41
|
+
return COMPACT_CAPABILITY[kind];
|
|
42
|
+
}
|
|
19
43
|
//# sourceMappingURL=types.js.map
|
|
@@ -42,6 +42,9 @@ function subtractRawUsage(a, b) {
|
|
|
42
42
|
total_tokens: a.total_tokens - b.total_tokens,
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
|
+
function emptyTurnUsage() {
|
|
46
|
+
return { input: 0, cached: 0, output: 0, total: 0 };
|
|
47
|
+
}
|
|
45
48
|
/**
|
|
46
49
|
* `compacted` 事件自带的摘要调用 usage(`latest_token_usage_record.usage`)。
|
|
47
50
|
* 用于把「压缩前窗口」与「摘要调用自身的消耗」区分开——两者都写在 compacted 前。
|
|
@@ -141,12 +144,19 @@ function scanCodexRollout(filePath, includeEvents) {
|
|
|
141
144
|
// exactly the 0.153 compatibility incident shape.
|
|
142
145
|
let userResponseItemCount = 0;
|
|
143
146
|
// token_count tracking: codex emits cumulative `total_token_usage` and an
|
|
144
|
-
// incremental `last_token_usage
|
|
145
|
-
//
|
|
147
|
+
// incremental `last_token_usage` **per sampling call** — one turn with tool
|
|
148
|
+
// round-trips logs several `token_count` lines. We want the LAST turn's usage
|
|
149
|
+
// = the sum of that turn's samplings (matches pi/opencode /resume "last turn"
|
|
150
|
+
// display semantics). Turn boundaries come from `task_started`. When
|
|
146
151
|
// last_token_usage is absent we derive it as total - previous_total.
|
|
147
152
|
let lastRawUsage;
|
|
148
153
|
/** 上一条被接受的 raw usage;compacted 分支回退一条时用(见下方注释)。 */
|
|
149
154
|
let previousRawUsage;
|
|
155
|
+
/**
|
|
156
|
+
* 末个 turn 的累计用量;`null` = 文件里没有 `task_started`(旧数据),
|
|
157
|
+
* 此时 flow 字段退回「末条 token_count」语义(见 buildUsageFromScan)。
|
|
158
|
+
*/
|
|
159
|
+
let turnUsage = null;
|
|
150
160
|
// Context window limit from the same token_count event as lastRawUsage
|
|
151
161
|
// (codex reports info.model_context_window per turn; absent on old data).
|
|
152
162
|
let lastContextLimit;
|
|
@@ -194,6 +204,12 @@ function scanCodexRollout(filePath, includeEvents) {
|
|
|
194
204
|
}
|
|
195
205
|
lastUserMessageSource = human.source;
|
|
196
206
|
}
|
|
207
|
+
else if (payload.type === 'task_started') {
|
|
208
|
+
// turn 边界:一个 turn 一条 task_started(与 task_complete 成对,
|
|
209
|
+
// turn_id 见 session_meta 之后的 `turn_context`)。从这里开始重新
|
|
210
|
+
// 累计本 turn 的用量。
|
|
211
|
+
turnUsage = emptyTurnUsage();
|
|
212
|
+
}
|
|
197
213
|
else if (payload.type === 'token_count') {
|
|
198
214
|
const info = payload.info;
|
|
199
215
|
const last = info?.last_token_usage;
|
|
@@ -217,6 +233,13 @@ function scanCodexRollout(filePath, includeEvents) {
|
|
|
217
233
|
lastRawUsage = raw;
|
|
218
234
|
const ctxWindow = info?.model_context_window;
|
|
219
235
|
lastContextLimit = typeof ctxWindow === 'number' ? ctxWindow : undefined;
|
|
236
|
+
if (turnUsage) {
|
|
237
|
+
const cached = Math.min(raw.cached_input_tokens, raw.input_tokens);
|
|
238
|
+
turnUsage.input += raw.input_tokens - cached;
|
|
239
|
+
turnUsage.cached += cached;
|
|
240
|
+
turnUsage.output += raw.output_tokens;
|
|
241
|
+
turnUsage.total += raw.total_tokens;
|
|
242
|
+
}
|
|
220
243
|
}
|
|
221
244
|
// 压缩后的水位:压缩收尾 token_count 全 0(raw 不满足上面 zero-filter),
|
|
222
245
|
// 但 total_tokens 表达真实窗口;之后若出现普通 turn(input>0),
|
|
@@ -288,6 +311,7 @@ function scanCodexRollout(filePath, includeEvents) {
|
|
|
288
311
|
lastContextLimit,
|
|
289
312
|
lastTotalUsage,
|
|
290
313
|
previousTotals,
|
|
314
|
+
turnUsage,
|
|
291
315
|
compactCount,
|
|
292
316
|
compactPreContextLength,
|
|
293
317
|
compactPostContextLength,
|
|
@@ -298,25 +322,38 @@ function scanCodexRollout(filePath, includeEvents) {
|
|
|
298
322
|
* input = raw - cached (codex reports cached input separately), cache
|
|
299
323
|
* creation is never reported by codex, reasoning is a subset of output
|
|
300
324
|
* (display-only, never added to total).
|
|
325
|
+
*
|
|
326
|
+
* Flow fields (input/output/cache/total) are the **sum over the last turn's
|
|
327
|
+
* samplings** when the file marks turn boundaries (`task_started`) — one codex
|
|
328
|
+
* turn logs one `token_count` per sampling, so the last line alone understates
|
|
329
|
+
* a tool-looping run (measured: 212 / 679 displayed vs 3,022 / 3,648 consumed).
|
|
330
|
+
* Legacy files without `task_started` keep the last-`token_count` semantics.
|
|
301
331
|
* contextLength = input_tokens (= (input_tokens-cached) + cached + 0),
|
|
302
332
|
* i.e. input + cacheRead + cacheCreation — the unified context-window
|
|
303
333
|
* occupancy contract across all five readers (excludes
|
|
304
334
|
* output/reasoning).
|
|
305
335
|
*/
|
|
306
336
|
function buildUsageFromScan(scan) {
|
|
307
|
-
const { lastRawUsage, lastContextLimit, lastTotalUsage, compactCount, compactPreContextLength, compactPostContextLength, } = scan;
|
|
337
|
+
const { lastRawUsage, lastContextLimit, lastTotalUsage, turnUsage, compactCount, compactPreContextLength, compactPostContextLength, } = scan;
|
|
308
338
|
if (!lastRawUsage)
|
|
309
339
|
return undefined;
|
|
310
340
|
const r = lastRawUsage;
|
|
311
341
|
const cached = Math.min(r.cached_input_tokens, r.input_tokens);
|
|
342
|
+
// 本 run = 末个 turn 全部采样之和;旧数据(无 task_started)退回末条。
|
|
343
|
+
const flow = turnUsage ?? {
|
|
344
|
+
input: r.input_tokens - cached,
|
|
345
|
+
cached,
|
|
346
|
+
output: r.output_tokens,
|
|
347
|
+
total: r.total_tokens,
|
|
348
|
+
};
|
|
312
349
|
const usage = {
|
|
313
|
-
inputTokens:
|
|
314
|
-
outputTokens:
|
|
350
|
+
inputTokens: flow.input,
|
|
351
|
+
outputTokens: flow.output,
|
|
315
352
|
contextLength: r.input_tokens,
|
|
316
353
|
contextLimit: lastContextLimit,
|
|
317
|
-
cacheReadTokens: cached,
|
|
354
|
+
cacheReadTokens: flow.cached,
|
|
318
355
|
cacheCreationTokens: 0,
|
|
319
|
-
totalTokens:
|
|
356
|
+
totalTokens: flow.total,
|
|
320
357
|
};
|
|
321
358
|
// Cumulative from the final total_token_usage (non-cached input +
|
|
322
359
|
// output), summed across all turns in the session.
|
|
@@ -3,31 +3,62 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The AgentSessionReader contract is fully synchronous (file-style readers),
|
|
5
5
|
* but DSH exposes only async HTTP. The reader therefore bridges with
|
|
6
|
-
* `spawnSync('curl', ...)` for its unary calls (session
|
|
6
|
+
* `spawnSync('curl', ...)` for its unary calls (session/list / session/page)
|
|
7
7
|
* — a synchronous HTTP round-trip to the local DSH host. This keeps the
|
|
8
8
|
* router/bridge interface contract intact without touching any upstream logic.
|
|
9
9
|
*
|
|
10
|
-
* listSessions: session
|
|
10
|
+
* listSessions: session/list → filter by cwd → sort by updatedAt desc → local
|
|
11
11
|
* pagination (DSH v1 has no cursor pagination).
|
|
12
|
-
* readSessionContent: session
|
|
13
|
-
* session-wide usage accumulation (assistant/message.usage)
|
|
12
|
+
* readSessionContent: session/projections → session/page → map SessionEvents +
|
|
13
|
+
* session-wide usage accumulation (assistant/message.usage);本 run 的 flow
|
|
14
|
+
* 字段 = 末个 turn(turn/start–turn/end)内所有采样之和,与 codex 同口径。
|
|
14
15
|
*/
|
|
15
16
|
import type { AgentSession, AgentSessionReader, SessionContent } from '../../runner/index.js';
|
|
16
17
|
interface DshSessionReaderOptions {
|
|
17
18
|
host?: string;
|
|
19
|
+
/** Configured DSH home; selects `.credentials.yaml` when credentialsFile is unset. */
|
|
20
|
+
dshHomeProvider?: () => string | undefined;
|
|
21
|
+
/** `dsh web` launch token (or its startup URL). */
|
|
22
|
+
token?: string;
|
|
23
|
+
/** DSH credentials file with the durable browser-session signing secret. */
|
|
24
|
+
credentialsFile?: string;
|
|
25
|
+
/** Resolve the credentials file on every call (hot /config change). */
|
|
26
|
+
credentialsFileProvider?: () => string | undefined;
|
|
27
|
+
/** Injectable synchronous credentials reader (tests). */
|
|
28
|
+
readCredentials?: (file: string) => Buffer;
|
|
29
|
+
/** Injectable clock (tests). */
|
|
30
|
+
now?: () => number;
|
|
31
|
+
/** Lifetime of minted browser cookies. */
|
|
32
|
+
cookieLifetimeMs?: number;
|
|
33
|
+
/** Resolve the current token on every call (hot /config change). */
|
|
34
|
+
tokenProvider?: () => string | undefined;
|
|
18
35
|
/** 每次调用时解析当前 host(/config 修改 host 后 runner 重建、reader 也跟随新 host)。
|
|
19
36
|
* 优先于固定的 `host`。 */
|
|
20
37
|
hostProvider?: () => string | undefined;
|
|
21
38
|
/** Injectable synchronous unary transport. Defaults to spawnSync curl. */
|
|
22
|
-
syncRequest?: (baseUrl: string, method: string, payload: unknown) => unknown;
|
|
39
|
+
syncRequest?: (baseUrl: string, method: string, payload: unknown, cookie?: string) => unknown;
|
|
40
|
+
/** Injectable synchronous launch-token bootstrap (tests). */
|
|
41
|
+
syncConnect?: (baseUrl: string, token: string) => string;
|
|
23
42
|
}
|
|
24
43
|
export declare class DshSessionReader implements AgentSessionReader {
|
|
25
44
|
private readonly hostProvider;
|
|
45
|
+
private readonly dshHomeProvider;
|
|
46
|
+
private readonly tokenProvider;
|
|
47
|
+
private readonly credentialsFileProvider;
|
|
48
|
+
private readonly readCredentials;
|
|
49
|
+
private readonly now;
|
|
50
|
+
private readonly cookieLifetimeMs;
|
|
26
51
|
private readonly syncRequest;
|
|
52
|
+
private readonly syncConnect;
|
|
53
|
+
private readonly cookieCache;
|
|
27
54
|
constructor(opts?: DshSessionReaderOptions);
|
|
28
55
|
/** 解析当前 host(hostProvider 优先,允许 /config 热改 host)。 */
|
|
29
56
|
private resolveBaseUrl;
|
|
57
|
+
private resolveCredentialsFile;
|
|
30
58
|
private unary;
|
|
59
|
+
/** Mint from the durable secret, or bootstrap and cache a token cookie. */
|
|
60
|
+
private resolveCookie;
|
|
61
|
+
private cookieFromCredentials;
|
|
31
62
|
listSessions(cwd: string, opts?: {
|
|
32
63
|
limit?: number;
|
|
33
64
|
offset?: number;
|
|
@@ -36,9 +67,17 @@ export declare class DshSessionReader implements AgentSessionReader {
|
|
|
36
67
|
total: number;
|
|
37
68
|
};
|
|
38
69
|
getNewestSession(cwd: string): AgentSession | null;
|
|
39
|
-
readSessionContent(sessionId: string,
|
|
70
|
+
readSessionContent(sessionId: string, cwd: string, opts?: {
|
|
40
71
|
maxEvents?: number;
|
|
41
72
|
}): SessionContent;
|
|
42
73
|
isSessionActive(sessionId: string, cwd: string): boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Find the session row in `session/list` that matches BOTH id and cwd.
|
|
76
|
+
* Returns undefined when the id belongs to another workspace (or is unknown) —
|
|
77
|
+
* every caller treats that as "not usable here".
|
|
78
|
+
*/
|
|
79
|
+
private resolveSession;
|
|
80
|
+
/** `session/page` needs an inclusive cursor; projections carry the watermark. */
|
|
81
|
+
private resolveThroughSeq;
|
|
43
82
|
}
|
|
44
83
|
export {};
|