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.
Files changed (66) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.en.md +1 -1
  3. package/README.md +1 -1
  4. package/dist/bridge/approval-coordinator.d.ts +18 -1
  5. package/dist/bridge/approval-coordinator.js +57 -10
  6. package/dist/bridge/index.js +90 -17
  7. package/dist/card/approval-render.js +18 -0
  8. package/dist/card/run-state.js +10 -5
  9. package/dist/config/index.d.ts +16 -0
  10. package/dist/config/index.js +46 -6
  11. package/dist/config/opencode-config.d.ts +3 -3
  12. package/dist/config/opencode-config.js +38 -22
  13. package/dist/inbound/authorize.d.ts +34 -0
  14. package/dist/inbound/authorize.js +31 -0
  15. package/dist/index.js +116 -63
  16. package/dist/instance-lock.d.ts +9 -0
  17. package/dist/instance-lock.js +113 -42
  18. package/dist/router/config/dsh.d.ts +51 -21
  19. package/dist/router/config/dsh.js +255 -66
  20. package/dist/router/config/opencode.js +16 -6
  21. package/dist/router/config/types.d.ts +6 -3
  22. package/dist/router/index.d.ts +1 -0
  23. package/dist/router/index.js +13 -5
  24. package/dist/runner/codex/app-server/translator.js +21 -7
  25. package/dist/runner/common/acp/base-acp-runner.d.ts +12 -0
  26. package/dist/runner/common/acp/base-acp-runner.js +83 -8
  27. package/dist/runner/common/acp/protocol-types.d.ts +10 -2
  28. package/dist/runner/common/acp/protocol-types.js +3 -1
  29. package/dist/runner/common/connection-based-runner.d.ts +15 -0
  30. package/dist/runner/common/connection-based-runner.js +40 -5
  31. package/dist/runner/common/jsonrpc/connection-manager.d.ts +15 -0
  32. package/dist/runner/common/jsonrpc/connection-manager.js +83 -50
  33. package/dist/runner/dsh/approval.d.ts +53 -0
  34. package/dist/runner/dsh/approval.js +161 -0
  35. package/dist/runner/dsh/auth.d.ts +36 -0
  36. package/dist/runner/dsh/auth.js +118 -0
  37. package/dist/runner/dsh/client.d.ts +161 -42
  38. package/dist/runner/dsh/client.js +551 -244
  39. package/dist/runner/dsh/daemon.d.ts +85 -0
  40. package/dist/runner/dsh/daemon.js +272 -0
  41. package/dist/runner/dsh/merge.d.ts +9 -0
  42. package/dist/runner/dsh/merge.js +51 -0
  43. package/dist/runner/dsh/runner.d.ts +94 -11
  44. package/dist/runner/dsh/runner.js +553 -67
  45. package/dist/runner/dsh/translator.d.ts +43 -3
  46. package/dist/runner/dsh/translator.js +115 -38
  47. package/dist/runner/dsh/types.d.ts +189 -27
  48. package/dist/runner/dsh/types.js +11 -5
  49. package/dist/runner/kimi/acp/runner.js +15 -3
  50. package/dist/runner/opencode/acp/error-monitor.d.ts +2 -2
  51. package/dist/runner/opencode/acp/error-monitor.js +2 -2
  52. package/dist/runner/opencode/acp/runner.d.ts +9 -0
  53. package/dist/runner/opencode/acp/runner.js +40 -1
  54. package/dist/runner/opencode/acp/translator.d.ts +4 -2
  55. package/dist/runner/opencode/acp/translator.js +12 -3
  56. package/dist/runner/pi/rpc/translator.d.ts +2 -0
  57. package/dist/runner/pi/rpc/translator.js +27 -1
  58. package/dist/runner/types.d.ts +34 -0
  59. package/dist/runner/types.js +24 -0
  60. package/dist/session/codex/rollout-reader.js +44 -7
  61. package/dist/session/dsh/sessions.d.ts +45 -6
  62. package/dist/session/dsh/sessions.js +171 -27
  63. package/dist/session/opencode/sessions.d.ts +18 -7
  64. package/dist/session/opencode/sessions.js +294 -15
  65. package/dist/session/pi/sessions.js +6 -2
  66. 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 (open source v1.18.x: `opencode/src/cli/global.ts`).
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
- promptPromise.then((result) => this.handlePromptResolved(result, translator, sessionId), (err) => this.handlePromptRejected(err, translator, sessionId, 'compact'));
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; both end with exactly one concatenation in
13
- * run-state, enforced by each side's seam test.)
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; both end with exactly one concatenation in
13
- * run-state, enforced by each side's seam test.)
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: [{ type: 'thinking', thinking: event.content.text }],
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: this.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
@@ -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;
@@ -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`. We want the LAST turn's usage (matches
145
- // pi/opencode /resume "last turn" display semantics). When
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: r.input_tokens - cached,
314
- outputTokens: r.output_tokens,
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: r.total_tokens,
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.list / session.history)
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.list → filter by cwd → sort by updatedAt desc → local
10
+ * listSessions: session/list → filter by cwd → sort by updatedAt desc → local
11
11
  * pagination (DSH v1 has no cursor pagination).
12
- * readSessionContent: session.history → map SessionEvents to display events +
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, _cwd: string, opts?: {
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 {};