lark-remote 0.5.19 → 0.5.21

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 (136) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/bridge/approval-actions.d.ts +1 -1
  3. package/dist/bridge/approval-actions.js +1 -1
  4. package/dist/bridge/bash-run.d.ts +71 -0
  5. package/dist/bridge/bash-run.js +217 -0
  6. package/dist/bridge/compact.d.ts +155 -0
  7. package/dist/bridge/compact.js +450 -0
  8. package/dist/bridge/final-usage.js +4 -4
  9. package/dist/bridge/inbound-media.js +72 -46
  10. package/dist/bridge/index.d.ts +106 -153
  11. package/dist/bridge/index.js +400 -1000
  12. package/dist/bridge/job-watch.d.ts +68 -0
  13. package/dist/bridge/job-watch.js +146 -0
  14. package/dist/bridge/queue-manager.d.ts +3 -3
  15. package/dist/bridge/queue-manager.js +10 -29
  16. package/dist/card/approval-render.d.ts +2 -2
  17. package/dist/card/approval-render.js +141 -130
  18. package/dist/card/card-budget.js +123 -98
  19. package/dist/card/card-helpers.d.ts +3 -3
  20. package/dist/card/card-helpers.js +6 -6
  21. package/dist/card/final-answer-file.js +1 -1
  22. package/dist/card/run-renderer.d.ts +3 -3
  23. package/dist/card/run-renderer.js +114 -159
  24. package/dist/card/run-state.d.ts +1 -15
  25. package/dist/card/run-state.js +358 -292
  26. package/dist/card/tool-render.d.ts +1 -1
  27. package/dist/card/tool-render.js +21 -17
  28. package/dist/card/usage-format.d.ts +22 -7
  29. package/dist/card/usage-format.js +119 -100
  30. package/dist/cli.js +15 -3
  31. package/dist/clone.js +2 -2
  32. package/dist/common/text-width.js +36 -24
  33. package/dist/common/tool-summary.js +132 -90
  34. package/dist/config/codex-config.js +130 -106
  35. package/dist/config/index.d.ts +16 -2
  36. package/dist/config/index.js +23 -8
  37. package/dist/connector/index.js +6 -6
  38. package/dist/error-classification.js +3 -2
  39. package/dist/inbound/placeholder.d.ts +1 -1
  40. package/dist/inbound/placeholder.js +1 -1
  41. package/dist/index.js +387 -281
  42. package/dist/logger/index.js +6 -6
  43. package/dist/message-handlers.d.ts +0 -5
  44. package/dist/message-handlers.js +254 -237
  45. package/dist/model-catalog.d.ts +2 -2
  46. package/dist/model-catalog.js +3 -8
  47. package/dist/platform/identity.js +1 -1
  48. package/dist/platform/path.d.ts +1 -1
  49. package/dist/platform/path.js +1 -1
  50. package/dist/platform/sleep-blocker.js +1 -1
  51. package/dist/platform/terminator-win32.js +2 -1
  52. package/dist/router/commands/ls.d.ts +3 -0
  53. package/dist/router/commands/ls.js +256 -175
  54. package/dist/router/commands/order.js +184 -159
  55. package/dist/router/commands/resume.js +190 -152
  56. package/dist/router/commands/ws.js +219 -174
  57. package/dist/router/config/dsh.d.ts +4 -8
  58. package/dist/router/config/dsh.js +3 -10
  59. package/dist/router/config/kimi.js +1 -1
  60. package/dist/router/config/types.d.ts +1 -1
  61. package/dist/router/config-diff.js +1 -1
  62. package/dist/router/index.d.ts +56 -11
  63. package/dist/router/index.js +401 -391
  64. package/dist/runner/bash/runner.js +2 -4
  65. package/dist/runner/claude/session.d.ts +1 -1
  66. package/dist/runner/claude/session.js +6 -4
  67. package/dist/runner/codex/app-server/runner.d.ts +9 -2
  68. package/dist/runner/codex/app-server/runner.js +30 -25
  69. package/dist/runner/codex/app-server/translator.d.ts +2 -5
  70. package/dist/runner/codex/app-server/translator.js +291 -238
  71. package/dist/runner/common/acp/acp-approval.d.ts +1 -1
  72. package/dist/runner/common/acp/acp-approval.js +1 -1
  73. package/dist/runner/common/acp/base-acp-runner.d.ts +5 -2
  74. package/dist/runner/common/acp/base-acp-runner.js +26 -65
  75. package/dist/runner/common/acp/base-translator.d.ts +12 -2
  76. package/dist/runner/common/acp/base-translator.js +50 -26
  77. package/dist/runner/common/acp/protocol-types.d.ts +1 -1
  78. package/dist/runner/common/acp/protocol-types.js +2 -2
  79. package/dist/runner/common/connection-based-runner.d.ts +32 -1
  80. package/dist/runner/common/connection-based-runner.js +32 -2
  81. package/dist/runner/common/jsonl-stream.js +154 -187
  82. package/dist/runner/common/jsonrpc/client.d.ts +29 -0
  83. package/dist/runner/common/jsonrpc/client.js +43 -12
  84. package/dist/runner/common/jsonrpc/connection-manager.d.ts +2 -2
  85. package/dist/runner/common/jsonrpc/connection-manager.js +4 -4
  86. package/dist/runner/common/spawning-runner.d.ts +6 -1
  87. package/dist/runner/common/spawning-runner.js +9 -4
  88. package/dist/runner/common/turn-started.d.ts +8 -0
  89. package/dist/runner/common/turn-started.js +11 -0
  90. package/dist/runner/dsh/approval.js +84 -51
  91. package/dist/runner/dsh/catalog-probe.d.ts +17 -0
  92. package/dist/runner/dsh/catalog-probe.js +10 -0
  93. package/dist/runner/dsh/client.d.ts +7 -11
  94. package/dist/runner/dsh/client.js +356 -289
  95. package/dist/runner/dsh/exit-cancel.d.ts +2 -2
  96. package/dist/runner/dsh/exit-cancel.js +2 -2
  97. package/dist/runner/dsh/inflight.d.ts +1 -1
  98. package/dist/runner/dsh/inflight.js +1 -1
  99. package/dist/runner/dsh/mux-stream.js +28 -8
  100. package/dist/runner/dsh/runner.d.ts +44 -4
  101. package/dist/runner/dsh/runner.js +296 -199
  102. package/dist/runner/dsh/translator.d.ts +1 -31
  103. package/dist/runner/dsh/translator.js +180 -178
  104. package/dist/runner/index.d.ts +1 -1
  105. package/dist/runner/kimi/acp/runner.d.ts +1 -2
  106. package/dist/runner/kimi/acp/runner.js +82 -46
  107. package/dist/runner/kimi/acp/translator.d.ts +1 -1
  108. package/dist/runner/kimi/acp/translator.js +3 -4
  109. package/dist/runner/opencode/acp/error-monitor.js +5 -5
  110. package/dist/runner/opencode/acp/runner.d.ts +2 -2
  111. package/dist/runner/opencode/acp/runner.js +4 -4
  112. package/dist/runner/pi/rpc/client.js +4 -13
  113. package/dist/runner/pi/rpc/protocol-types.d.ts +1 -1
  114. package/dist/runner/pi/rpc/protocol-types.js +1 -1
  115. package/dist/runner/pi/rpc/runner.d.ts +3 -0
  116. package/dist/runner/pi/rpc/runner.js +27 -42
  117. package/dist/runner/pi/rpc/translator.js +61 -60
  118. package/dist/runner/types.d.ts +2 -2
  119. package/dist/runner/types.js +1 -1
  120. package/dist/session/claude/sessions.d.ts +45 -0
  121. package/dist/session/claude/sessions.js +160 -122
  122. package/dist/session/codex/rollout-reader.js +181 -182
  123. package/dist/session/common/throw-session-list-error.d.ts +10 -0
  124. package/dist/session/common/throw-session-list-error.js +16 -0
  125. package/dist/session/dsh/sessions.js +291 -235
  126. package/dist/session/kimi/sessions.d.ts +40 -8
  127. package/dist/session/kimi/sessions.js +373 -301
  128. package/dist/session/opencode/sessions.d.ts +142 -1
  129. package/dist/session/opencode/sessions.js +378 -321
  130. package/dist/session/pi/sessions.js +4 -5
  131. package/dist/session/session-store.d.ts +9 -1
  132. package/dist/session/session-store.js +99 -99
  133. package/dist/update/version-check.js +2 -1
  134. package/dist/workspace/index.d.ts +1 -1
  135. package/dist/workspace/index.js +1 -1
  136. package/package.json +1 -1
@@ -133,8 +133,8 @@ export class OpencodeAcpRunner extends BaseAcpRunner {
133
133
  }
134
134
  // Apply the configured model (`provider/model`) via set_config_option.
135
135
  // Without this the session runs opencode's own default model (e.g.
136
- // opencode/big-pickle), which may be unusable — 2026-08-17 live smoke:
137
- // default zen model left session/prompt hanging with no response.
136
+ // opencode/big-pickle), which may be unusable: the default zen model can
137
+ // leave session/prompt hanging with no response.
138
138
  // Non-fatal on failure (unknown model → server InvalidModelError).
139
139
  if (this.model && this.currentModelValue !== this.model) {
140
140
  try {
@@ -179,7 +179,7 @@ export class OpencodeAcpRunner extends BaseAcpRunner {
179
179
  * before failing the turn.
180
180
  *
181
181
  * 单次 attempt 自身还要等 opencode 侧超时(~10s),所以真实窗口 ≈ Σdelay +
182
- * 每次的等待。2026-10-30 现场:daemon 刚重启,`/api/model` 因模型目录拉取
182
+ * 每次的等待。daemon 刚重启时 `/api/model` 会因模型目录拉取
183
183
  * 超时连续 503 约 7 分钟,旧退避 0.5/1.5/3s(合计 ~45s)全落在这段里,4 次
184
184
  * 重试无一成功。现在铺到 ~75s + 每次超时 ≈ 2 分钟,覆盖一次冷启动窗口,
185
185
  * 又不至于让真故障的用户干等太久。
@@ -194,7 +194,7 @@ export class OpencodeAcpRunner extends BaseAcpRunner {
194
194
  /**
195
195
  * 用户可见文案。这里刻意**不**建议 `opencode service restart`:卡住的是
196
196
  * opencode 的模型目录/位置服务冷启动,重启只会清掉刚热好的状态、重新回到
197
- * 同一个窗口(2026-10-30 现场:照提示重启两次仍然失败,等几分钟自己好了)。
197
+ * 同一个窗口(照提示重启两次仍然失败,等几分钟自己好了)。
198
198
  */
199
199
  enrichSessionSetupError(err) {
200
200
  if (!this.isRetryableSessionSetupError(err)) {
@@ -10,7 +10,7 @@
10
10
  * Response correlation is by command id; every non-response line is forwarded
11
11
  * as an event to the active hooks (the runner's translator).
12
12
  */
13
- import { ConnectionLostError } from '../../common/jsonrpc/client.js';
13
+ import { ConnectionLostError, disposeRpcClientInstance, rejectAllPending, } from '../../common/jsonrpc/client.js';
14
14
  export class PiRpcClient {
15
15
  transport;
16
16
  requestTimeoutMs;
@@ -78,13 +78,8 @@ export class PiRpcClient {
78
78
  return;
79
79
  this.transport.write({ ...command, id: `req_${this.nextId++}` });
80
80
  }
81
- async dispose() {
82
- if (this._disposed)
83
- return;
84
- this._disposed = true;
85
- this._ready = false;
86
- this.failPending(new ConnectionLostError('pi rpc client disposed'));
87
- await this.transport.close();
81
+ dispose() {
82
+ return disposeRpcClientInstance(this, this.pending, () => this.transport.close(), 'pi rpc client disposed');
88
83
  }
89
84
  // =========================================================================
90
85
  // Internal
@@ -104,11 +99,7 @@ export class PiRpcClient {
104
99
  this.runHooks?.onEvent?.(message);
105
100
  }
106
101
  failPending(err) {
107
- for (const [_id, pending] of this.pending) {
108
- clearTimeout(pending.timer);
109
- pending.reject(err);
110
- }
111
- this.pending.clear();
102
+ rejectAllPending(this.pending, err);
112
103
  }
113
104
  }
114
105
  //# sourceMappingURL=client.js.map
@@ -7,7 +7,7 @@
7
7
  * and events (session lifecycle / message / compaction) as bare
8
8
  * JSON objects (no envelope).
9
9
  *
10
- * Turn semantics (verified live against pi 0.84.1, 2026-08-18):
10
+ * Turn semantics (verified against pi 0.84.1):
11
11
  * - `prompt` returns an ack (`success:true`) immediately; the turn then streams
12
12
  * events and completes with an `agent_settled` event.
13
13
  * - `compact` runs synchronously and returns its response at completion; it also
@@ -7,7 +7,7 @@
7
7
  * and events (session lifecycle / message / compaction) as bare
8
8
  * JSON objects (no envelope).
9
9
  *
10
- * Turn semantics (verified live against pi 0.84.1, 2026-08-18):
10
+ * Turn semantics (verified against pi 0.84.1):
11
11
  * - `prompt` returns an ack (`success:true`) immediately; the turn then streams
12
12
  * events and completes with an `agent_settled` event.
13
13
  * - `compact` runs synchronously and returns its response at completion; it also
@@ -59,6 +59,7 @@ export declare class PiRpcRunner extends ConnectionBasedRunner<PiRpcClient, PiRp
59
59
  protected clearTurnState(): void;
60
60
  protected releaseConnection(cwd: string): Promise<void>;
61
61
  protected notifyIdle(cwd: string): void;
62
+ protected notifyConnectionActivity(cwd: string): void;
62
63
  protected disposeConnections(): Promise<void>;
63
64
  getStatusInfo(): AgentStatusInfo;
64
65
  /**
@@ -72,4 +73,6 @@ export declare class PiRpcRunner extends ConnectionBasedRunner<PiRpcClient, PiRp
72
73
  */
73
74
  runCompact(_message: string, opts: SpawnOptions): AsyncGenerator<AgentEvent>;
74
75
  private handleEvent;
76
+ /** Wire the acquired client's hooks into the turn dispatcher (anchor-guarded). */
77
+ private installTurnHooks;
75
78
  }
@@ -20,6 +20,7 @@
20
20
  import { ConnectionBasedRunner } from '../../common/connection-based-runner.js';
21
21
  import { ConnectionManager } from '../../common/jsonrpc/connection-manager.js';
22
22
  import { getLogger } from '../../../logger/index.js';
23
+ import { PI_DEFAULTS } from '../../../config/index.js';
23
24
  import { PiRpcClient } from './client.js';
24
25
  import { PiRpcTranslator } from './translator.js';
25
26
  export class PiRpcRunner extends ConnectionBasedRunner {
@@ -36,10 +37,12 @@ export class PiRpcRunner extends ConnectionBasedRunner {
36
37
  sessionReader: opts.sessionReader,
37
38
  turnIdleTimeoutMs: opts.turnIdleTimeoutMs,
38
39
  });
39
- this.defaultModel = opts.model ?? 'glm-5.2';
40
- this.provider = opts.provider ?? 'Volcano';
41
- this.thinking = opts.thinking ?? 'medium';
42
- const tools = (opts.tools ?? ['read', 'bash', 'edit', 'write', 'grep', 'find', 'ls']).join(',');
40
+ this.defaultModel = opts.model ?? PI_DEFAULTS.model;
41
+ this.provider = opts.provider ?? PI_DEFAULTS.provider;
42
+ this.thinking = opts.thinking ?? PI_DEFAULTS.thinking;
43
+ // PI_DEFAULTS.tools 是逗号串(config schema 的 tools 同形态),与原数组兜底
44
+ // join(',') 结果逐字相等;opts.tools 自身的数组语义(含空数组 → 空串)不变。
45
+ const tools = opts.tools?.join(',') ?? PI_DEFAULTS.tools;
43
46
  this.manager = new ConnectionManager({
44
47
  binary: opts.binary ?? 'pi',
45
48
  buildArgs: (req) => buildPiArgs(this.provider, this.defaultModel, this.thinking, tools, req.sessionId),
@@ -93,6 +96,9 @@ export class PiRpcRunner extends ConnectionBasedRunner {
93
96
  notifyIdle(cwd) {
94
97
  this.manager.notifyIdle(cwd);
95
98
  }
99
+ notifyConnectionActivity(cwd) {
100
+ this.manager.notifyActivity(cwd);
101
+ }
96
102
  async disposeConnections() {
97
103
  await this.manager.disposeAll();
98
104
  }
@@ -110,28 +116,13 @@ export class PiRpcRunner extends ConnectionBasedRunner {
110
116
  * `agent_settled` event produces the result.
111
117
  */
112
118
  async setupTurn(message, opts) {
113
- const client = await this.manager.acquire(opts.cwd, { sessionId: opts.sessionId });
114
- this.manager.notifyActivity(opts.cwd);
115
- this.currentClient = client;
116
- // 轮次代际锚点(与 BaseAcpRunner 同口径):pi 的 shouldReuse 要求
117
- // sessionId 匹配,/new、/resume 切换后的首条消息会让 acquire **同步
118
- // dispose** 旧连接;terminator 等到进程退出后,旧 client 上残留的上一轮
119
- // onClose 闭包会把假错误推进新 turn 的共享队列(用户消息未发出即丢失)。
120
- // 旧轮迟到的 agent_settled 同理(新 turn 空转立即"成功")。所有 hook
121
- // 落地前必须过 isCurrentTurn。
122
- const generation = this.turnGeneration;
123
- client.setHooks({
124
- onEvent: (evt) => {
125
- if (!this.isCurrentTurn(generation, client))
126
- return;
127
- this.handleEvent(evt);
128
- },
129
- onClose: () => {
130
- if (!this.isCurrentTurn(generation, client))
131
- return;
132
- this.failTurn('Pi RPC connection closed');
133
- },
134
- });
119
+ const anchor = await this.acquireForTurn(opts.cwd, () => this.manager.acquire(opts.cwd, { sessionId: opts.sessionId }));
120
+ // pi 的 shouldReuse 要求 sessionId 匹配:/new、/resume 切换后的首条消息会让
121
+ // acquire **同步 dispose** 旧连接。旧 client 残留的上一轮 onClose / 迟到的
122
+ // agent_settled 必须被 anchor 守卫挡掉,否则假错误会推进新 turn 的共享队列
123
+ // (语义见 acquireForTurn / TurnAnchor)。
124
+ this.installTurnHooks(anchor);
125
+ const { client } = anchor;
135
126
  let sessionId = opts.sessionId;
136
127
  if (!sessionId) {
137
128
  const state = await client.request({ type: 'get_state' });
@@ -169,24 +160,11 @@ export class PiRpcRunner extends ConnectionBasedRunner {
169
160
  if (!opts.sessionId) {
170
161
  throw new Error('compact requires a sessionId');
171
162
  }
172
- const client = await this.manager.acquire(opts.cwd, { sessionId: opts.sessionId });
173
- this.manager.notifyActivity(opts.cwd);
174
- this.currentClient = client;
175
163
  // 同 setupTurn:hook 带轮次代际锚点,旧连接的迟到回调不得进入本轮。
176
- const generation = this.turnGeneration;
177
- client.setHooks({
178
- onEvent: (evt) => {
179
- if (!this.isCurrentTurn(generation, client))
180
- return;
181
- this.handleEvent(evt);
182
- },
183
- onClose: () => {
184
- if (!this.isCurrentTurn(generation, client))
185
- return;
186
- this.failTurn('Pi RPC connection closed');
187
- },
188
- });
164
+ const anchor = await this.acquireForTurn(opts.cwd, () => this.manager.acquire(opts.cwd, { sessionId: opts.sessionId }));
165
+ this.installTurnHooks(anchor);
189
166
  this.activeSessionId = opts.sessionId;
167
+ const { client } = anchor;
190
168
  const translator = new PiRpcTranslator();
191
169
  translator.setSessionId(opts.sessionId);
192
170
  translator.setOperationKind('compact');
@@ -210,6 +188,13 @@ export class PiRpcRunner extends ConnectionBasedRunner {
210
188
  handleEvent(evt) {
211
189
  this.pushEvents(this.currentTranslator?.handleEvent(evt) ?? []);
212
190
  }
191
+ /** Wire the acquired client's hooks into the turn dispatcher (anchor-guarded). */
192
+ installTurnHooks(anchor) {
193
+ anchor.client.setHooks({
194
+ onEvent: this.forCurrentTurn(anchor, 'event', (evt) => this.handleEvent(evt)),
195
+ onClose: this.forCurrentTurn(anchor, 'connection close', () => this.failTurn('Pi RPC connection closed')),
196
+ });
197
+ }
213
198
  }
214
199
  /** Build pi `--mode rpc` spawn args. `sessionId` is bound at spawn (pi has no
215
200
  * RPC resume-by-id command); omitting it spawns a brand-new session. */
@@ -10,7 +10,56 @@
10
10
  * `result` from that response.
11
11
  * - `agent_settled` is ignored for compaction turns (no double result).
12
12
  */
13
- import { makeTurnStartedEvent } from '../../common/turn-started.js';
13
+ import { produceTurnStartedEvent } from '../../common/turn-started.js';
14
+ /** 首见:push 一个空 text/thinking 块并把游标移到它。 */
15
+ function textLikeStart(kind, state, _evt) {
16
+ state.content.push(kind === 'text' ? { type: 'text', text: '' } : { type: 'thinking', thinking: '' });
17
+ state.currentContentIndex++;
18
+ }
19
+ /** 增量:把 delta 追加到当前 text/thinking 块(类型不匹配则忽略)。 */
20
+ function textLikeDelta(kind, state, evt) {
21
+ const idx = state.currentContentIndex;
22
+ const block = state.content[idx];
23
+ if (idx >= 0 && block?.type === kind) {
24
+ block[kind] += evt.delta ?? '';
25
+ }
26
+ }
27
+ /** 收尾:以全量 content 覆盖当前 text/thinking 块(无 content 则忽略)。 */
28
+ function textLikeEnd(kind, state, evt) {
29
+ const idx = state.currentContentIndex;
30
+ const block = state.content[idx];
31
+ if (idx >= 0 && block?.type === kind && evt.content) {
32
+ block[kind] = evt.content;
33
+ }
34
+ }
35
+ function toolCallStart(state, _evt) {
36
+ // 信息保真 不再 push 空壳 tool_use(空 id/name 会被渲染成
37
+ // 无意义工具块)。只保留 bookkeeping,toolcall_end 补发完整事件。
38
+ state.pendingToolCall = true;
39
+ }
40
+ function toolCallEnd(state, evt) {
41
+ if (state.pendingToolCall && evt.toolCall) {
42
+ state.content.push({
43
+ type: 'tool_use',
44
+ id: evt.toolCall.id,
45
+ name: evt.toolCall.name,
46
+ input: evt.toolCall.arguments,
47
+ });
48
+ state.currentContentIndex = state.content.length - 1;
49
+ state.pendingToolCall = false;
50
+ }
51
+ }
52
+ /** 查表分派 message_update;toolcall_delta 等无专属 handler 的类型为 no-op。 */
53
+ const PI_UPDATE_HANDLERS = new Map([
54
+ ['text_start', textLikeStart.bind(null, 'text')],
55
+ ['text_delta', textLikeDelta.bind(null, 'text')],
56
+ ['text_end', textLikeEnd.bind(null, 'text')],
57
+ ['thinking_start', textLikeStart.bind(null, 'thinking')],
58
+ ['thinking_delta', textLikeDelta.bind(null, 'thinking')],
59
+ ['thinking_end', textLikeEnd.bind(null, 'thinking')],
60
+ ['toolcall_start', toolCallStart],
61
+ ['toolcall_end', toolCallEnd],
62
+ ]);
14
63
  export class PiRpcTranslator {
15
64
  content = [];
16
65
  currentContentIndex = -1;
@@ -135,8 +184,9 @@ export class PiRpcTranslator {
135
184
  }
136
185
  /** Self-produced turn_started (no wire equivalent). */
137
186
  produceTurnStarted(sessionId, turnId) {
138
- this.sessionId = sessionId;
139
- return makeTurnStartedEvent(sessionId, turnId, this.operationKind);
187
+ return produceTurnStartedEvent(sessionId, turnId, this.operationKind, (s) => {
188
+ this.sessionId = s;
189
+ });
140
190
  }
141
191
  /** Result for a completed normal turn (agent_settled). */
142
192
  produceResultFromSettled() {
@@ -236,63 +286,14 @@ export class PiRpcTranslator {
236
286
  this.lastTotalTokens = u.totalTokens;
237
287
  }
238
288
  applyUpdate(evt) {
239
- switch (evt.type) {
240
- case 'text_start':
241
- this.content.push({ type: 'text', text: '' });
242
- this.currentContentIndex++;
243
- break;
244
- case 'text_delta':
245
- if (this.currentContentIndex >= 0 &&
246
- this.content[this.currentContentIndex]?.type === 'text') {
247
- this.content[this.currentContentIndex].text += evt.delta ?? '';
248
- }
249
- break;
250
- case 'text_end':
251
- if (this.currentContentIndex >= 0 &&
252
- this.content[this.currentContentIndex]?.type === 'text' &&
253
- evt.content) {
254
- this.content[this.currentContentIndex].text = evt.content;
255
- }
256
- break;
257
- case 'thinking_start':
258
- this.content.push({ type: 'thinking', thinking: '' });
259
- this.currentContentIndex++;
260
- break;
261
- case 'thinking_delta':
262
- if (this.currentContentIndex >= 0 &&
263
- this.content[this.currentContentIndex]?.type === 'thinking') {
264
- this.content[this.currentContentIndex].thinking +=
265
- evt.delta ?? '';
266
- }
267
- break;
268
- case 'thinking_end':
269
- if (this.currentContentIndex >= 0 &&
270
- this.content[this.currentContentIndex]?.type === 'thinking' &&
271
- evt.content) {
272
- this.content[this.currentContentIndex].thinking = evt.content;
273
- }
274
- break;
275
- case 'toolcall_start':
276
- // 信息保真 不再 push 空壳 tool_use(空 id/name 会被渲染成
277
- // 无意义工具块)。只保留 bookkeeping,toolcall_end 补发完整事件。
278
- this.pendingToolCall = true;
279
- break;
280
- case 'toolcall_end':
281
- if (this.pendingToolCall && evt.toolCall) {
282
- this.content.push({
283
- type: 'tool_use',
284
- id: evt.toolCall.id,
285
- name: evt.toolCall.name,
286
- input: evt.toolCall.arguments,
287
- });
288
- this.currentContentIndex = this.content.length - 1;
289
- this.pendingToolCall = false;
290
- }
291
- break;
292
- default:
293
- // toolcall_delta: pi provides complete arguments at toolcall_end, ignore.
294
- break;
295
- }
289
+ const state = {
290
+ content: this.content,
291
+ currentContentIndex: this.currentContentIndex,
292
+ pendingToolCall: this.pendingToolCall,
293
+ };
294
+ PI_UPDATE_HANDLERS.get(evt.type)?.(state, evt);
295
+ this.currentContentIndex = state.currentContentIndex;
296
+ this.pendingToolCall = state.pendingToolCall;
296
297
  }
297
298
  }
298
299
  /**
@@ -302,7 +302,7 @@ export interface ResultEvent {
302
302
  * 省略)` 标记——渲染层再怎么「回答优先」也找不回被丢掉的头部。runner 在
303
303
  * 收 turn 时拿到的是**完整** assistant 文本(一条消息,不是逐 delta 拼接),
304
304
  * 把它挂在 result 上,`promoteFinalAnswer` 用它建立 finalAnswer,回答因此
305
- * 不再被进度窗口截断(2026-11-01 卡片输出块被截断缺陷)。
305
+ * 不再被进度窗口截断。
306
306
  */
307
307
  finalAnswerText?: string;
308
308
  usage?: {
@@ -582,7 +582,7 @@ export { AGENT_DISPLAY_NAMES } from '../agent-kinds.js';
582
582
  * 键集就是 `AgentKind`:新增 agent 时缺一项 = TypeScript 报错,必须显式回答
583
583
  * 「这个 agent 能不能压缩」。历史教训:能力只由 runner 上有没有 `runCompact`
584
584
  * 这个鸭子方法隐式决定,dsh 接入时漏了实现,run 卡片直接不渲染按钮——没有
585
- * 报错、没有置灰、没有日志,用户只能看到一个缺失的按钮(2026-10-17)。
585
+ * 报错、没有置灰、没有日志,用户只能看到一个缺失的按钮。
586
586
  *
587
587
  * 声明与实现的唯一性由 `src/runner/runner-structure.test.ts` 的矩阵用例保证:
588
588
  * 每个 kind 的声明值必须等于对应 runner 类是否实现 `runCompact`。bridge 侧
@@ -11,7 +11,7 @@ export { AGENT_DISPLAY_NAMES } from '../agent-kinds.js';
11
11
  * 键集就是 `AgentKind`:新增 agent 时缺一项 = TypeScript 报错,必须显式回答
12
12
  * 「这个 agent 能不能压缩」。历史教训:能力只由 runner 上有没有 `runCompact`
13
13
  * 这个鸭子方法隐式决定,dsh 接入时漏了实现,run 卡片直接不渲染按钮——没有
14
- * 报错、没有置灰、没有日志,用户只能看到一个缺失的按钮(2026-10-17)。
14
+ * 报错、没有置灰、没有日志,用户只能看到一个缺失的按钮。
15
15
  *
16
16
  * 声明与实现的唯一性由 `src/runner/runner-structure.test.ts` 的矩阵用例保证:
17
17
  * 每个 kind 的声明值必须等于对应 runner 类是否实现 `runCompact`。bridge 侧
@@ -25,6 +25,50 @@ export declare function getNewestSession(cwd: string, opts?: {
25
25
  export declare function isClaudeSessionActive(sessionId: string, cwd: string, opts?: {
26
26
  projectsDir?: string;
27
27
  }): boolean;
28
+ /** claude wire 一条 assistant 行携带的原始 usage 字段(可选,缺按 0 计)。 */
29
+ interface ClaudeRawUsage {
30
+ input_tokens?: number;
31
+ output_tokens?: number;
32
+ cache_read_input_tokens?: number;
33
+ cache_creation_input_tokens?: number;
34
+ }
35
+ /** per-message-id 合并后的 usage 采样(四个分量 + 采集时的真人 turn 序号)。 */
36
+ export interface ClaudePerMessageUsage {
37
+ input: number;
38
+ output: number;
39
+ cacheRead: number;
40
+ cacheCreation: number;
41
+ /** 该 message 属于第几个真人 turn(0 = 还没有真人 prompt)。 */
42
+ turn: number;
43
+ }
44
+ /**
45
+ * 两条采样逐字段取 max(纯函数;UsageAccumulator 的 per-field max 语义)。
46
+ *
47
+ * RED LINE(token 口径单源的一部分):一条 streaming assistant 消息跨多行
48
+ * jsonl、共享同一 message id;部分后端(第三方网关,如 DeepSeek)在早期占位
49
+ * 行(空 thinking 块)写全零 usage,真实用量只在带 stop_reason 的末行。
50
+ * first-occurrence-wins 会把零记录钉死、丢掉真实用量(regression 2026-08-19:
51
+ * auto-resume 卡 per-run 统计全 0)。逐字段 max 与行序无关地重建完整用量;
52
+ * 官方 API 每行重复相同 usage,max 是 no-op。
53
+ *
54
+ * 产生:合并后的一条采样。依赖:入参。不改任何外部状态。
55
+ */
56
+ export declare function maxUsageRecord(prev: ClaudePerMessageUsage | undefined, usage: ClaudeRawUsage, turnSeq: number): ClaudePerMessageUsage;
57
+ /**
58
+ * usage 装配(纯函数):每个 message id 一条最优记录、按首次出现顺序喂
59
+ * UsageAccumulator(Map 保序),acc.last = 末条 message 的完整用量,totals
60
+ * 让每条 API 响应恰好多计一次。per-run flow 只取末个真人 turn 的记录——
61
+ * 工具往返的 turn 有多条 assistant 消息(一次模型请求一条),只取 `last`
62
+ * 会低报。
63
+ *
64
+ * 产生:AgentSessionUsage。依赖:入参 records + 标量。不改任何外部状态
65
+ * (UsageAccumulator 在函数内新建)。
66
+ */
67
+ export declare function buildClaudeUsage(records: Iterable<ClaudePerMessageUsage>, opts: {
68
+ turnSeq: number;
69
+ lastPostTokens: number;
70
+ compactCount: number;
71
+ }): AgentSessionUsage;
28
72
  export declare function readSessionContent(sessionId: string, cwd: string, opts?: {
29
73
  projectsDir?: string;
30
74
  maxEvents?: number;
@@ -35,3 +79,4 @@ export declare function readSessionContent(sessionId: string, cwd: string, opts?
35
79
  recap?: string;
36
80
  displayTitle?: string;
37
81
  };
82
+ export {};