lark-remote 0.5.20 → 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 (55) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/bridge/inbound-media.js +71 -45
  3. package/dist/bridge/index.d.ts +9 -0
  4. package/dist/bridge/index.js +230 -203
  5. package/dist/card/approval-render.js +32 -33
  6. package/dist/card/card-budget.js +123 -98
  7. package/dist/card/run-renderer.js +82 -68
  8. package/dist/card/run-state.d.ts +0 -14
  9. package/dist/card/run-state.js +335 -207
  10. package/dist/card/tool-render.js +20 -16
  11. package/dist/card/usage-format.d.ts +12 -9
  12. package/dist/card/usage-format.js +111 -97
  13. package/dist/common/text-width.js +36 -24
  14. package/dist/common/tool-summary.js +132 -90
  15. package/dist/index.js +226 -190
  16. package/dist/message-handlers.js +253 -236
  17. package/dist/router/commands/ls.d.ts +3 -0
  18. package/dist/router/commands/ls.js +256 -175
  19. package/dist/router/commands/order.js +184 -159
  20. package/dist/router/commands/resume.js +189 -133
  21. package/dist/router/commands/ws.js +219 -174
  22. package/dist/router/index.js +192 -201
  23. package/dist/runner/bash/runner.js +2 -4
  24. package/dist/runner/codex/app-server/translator.d.ts +2 -0
  25. package/dist/runner/codex/app-server/translator.js +287 -221
  26. package/dist/runner/common/acp/base-translator.js +19 -17
  27. package/dist/runner/common/jsonl-stream.js +154 -187
  28. package/dist/runner/common/jsonrpc/client.d.ts +29 -0
  29. package/dist/runner/common/jsonrpc/client.js +43 -12
  30. package/dist/runner/common/spawning-runner.d.ts +5 -0
  31. package/dist/runner/common/spawning-runner.js +8 -3
  32. package/dist/runner/common/turn-started.d.ts +8 -0
  33. package/dist/runner/common/turn-started.js +11 -0
  34. package/dist/runner/dsh/approval.js +84 -51
  35. package/dist/runner/dsh/client.d.ts +7 -9
  36. package/dist/runner/dsh/client.js +356 -287
  37. package/dist/runner/dsh/mux-stream.js +28 -8
  38. package/dist/runner/dsh/runner.d.ts +4 -3
  39. package/dist/runner/dsh/runner.js +88 -64
  40. package/dist/runner/dsh/translator.d.ts +1 -31
  41. package/dist/runner/dsh/translator.js +180 -178
  42. package/dist/runner/kimi/acp/runner.js +81 -44
  43. package/dist/runner/opencode/acp/error-monitor.js +5 -5
  44. package/dist/runner/pi/rpc/client.js +4 -13
  45. package/dist/runner/pi/rpc/translator.js +61 -60
  46. package/dist/session/claude/sessions.js +113 -101
  47. package/dist/session/codex/rollout-reader.js +179 -180
  48. package/dist/session/dsh/sessions.js +281 -231
  49. package/dist/session/kimi/sessions.d.ts +0 -7
  50. package/dist/session/kimi/sessions.js +129 -109
  51. package/dist/session/opencode/sessions.d.ts +0 -11
  52. package/dist/session/opencode/sessions.js +161 -137
  53. package/dist/session/session-store.d.ts +8 -0
  54. package/dist/session/session-store.js +98 -98
  55. package/package.json +1 -1
@@ -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
  /**
@@ -7,6 +7,7 @@ import { samePath, encodeProjectDirName } from '../../platform/path.js';
7
7
  import { extractContentBlocks } from '../common/content-blocks.js';
8
8
  import { scanJsonlOnce, tailOffsetAfter, scanTailEvents } from '../common/two-pass.js';
9
9
  import { getLogger } from '../../logger/index.js';
10
+ import { logNonEnoentDebug } from '../session-store.js';
10
11
  import { UsageAccumulator, contextWindowOccupancy, cumulativeUsageFields, } from '../common/usage-accumulator.js';
11
12
  import { SessionIndex, extractText, isTaskNotificationText, CLAUDE_MAPPING, } from './session-index.js';
12
13
  /**
@@ -190,9 +191,7 @@ function hasRecentFileActivity(filePath, mtimeMs, now = Date.now()) {
190
191
  * 否则「文件不可读」会被当成「会话不活跃」。
191
192
  */
192
193
  function logStatFailure(filePath, err) {
193
- if (err.code === 'ENOENT')
194
- return;
195
- getLogger().debug(`[claude-session-reader] stat ${filePath} failed: ${err?.message ?? String(err)}`);
194
+ logNonEnoentDebug(err, (m) => `[claude-session-reader] stat ${filePath} failed: ${m}`);
196
195
  }
197
196
  /**
198
197
  * Check if a Claude session (by sessionId + cwd) is still active.
@@ -353,110 +352,123 @@ export function buildClaudeUsage(records, opts) {
353
352
  ...cumulativeUsageFields(t),
354
353
  };
355
354
  }
356
- /**
357
- * first-pass scan: stream the file once via `scanJsonlLines`
358
- * (no `string[]` materialized), parse each line exactly once, and collect
359
- * all metadata needed by readSessionContent (usage, tailOffset, aiTitle,
360
- * recap, lastUserMessage). Does NOT retain parsed objects — the events tail
361
- * is re-parsed separately by `extractEventsFromTail` so memory for both raw
362
- * line strings and parsed objects is O(tail), not O(whole file).
363
- *
364
- * 主体是分发表:每类行只收集自己的标量(assistant → usage 并表、
365
- * compact_boundary → 计数 + postTokens、user → tail 定位/标题、ai-title);
366
- * usage 的逐字段 max 合并(maxUsageRecord)与装配(buildClaudeUsage)已
367
- * 独立成纯函数。
368
- */
369
- function scalarScan(filePath) {
355
+ function newScalarScanState() {
370
356
  // Per-message-id best usage record (per-field max). A streaming assistant
371
357
  // message spans multiple jsonl lines sharing the same message id; some
372
358
  // backends (third-party gateways, e.g. DeepSeek) write all-zero usage on
373
359
  // the early placeholder lines (empty thinking block) and the real usage
374
360
  // only on the final line carrying stop_reason. First-occurrence-wins dedup
375
- // would pin the zero record and discard the real one (auto-resume
376
- // card per-run stats all 0). Per-field max
377
- // reconstructs the full usage regardless of line order — on the official
378
- // API every line repeats identical usage, so max is a no-op there.
379
- const usageByMessageId = new Map();
380
- let lastPostTokens = 0;
381
- let compactCount = 0;
382
- let tailOffset = -1;
383
- /** 真人 prompt 序号;末个 turn = 当前值。sidechain/注入/工具结果不递增。 */
384
- let turnSeq = 0;
385
- let lastAiTitle;
386
- let lastRecap;
387
- let lastUserMessage;
388
- scanJsonlOnce(filePath, (obj, line, offset) => {
389
- // --- usage aggregation (was aggregateSessionUsage) ---
390
- if (obj.type === 'assistant') {
391
- const msg = obj.message;
392
- const usage = msg?.usage;
393
- if (usage) {
394
- const msgId = msg?.id;
395
- if (msgId) {
396
- usageByMessageId.set(msgId, maxUsageRecord(usageByMessageId.get(msgId), usage, turnSeq));
397
- }
398
- }
399
- }
400
- if (obj.type === 'system' && obj.subtype === 'compact_boundary') {
401
- compactCount++;
402
- const cm = obj.compactMetadata;
403
- if (cm?.postTokens) {
404
- lastPostTokens = cm.postTokens;
405
- }
406
- }
407
- // --- last user message offset ---
408
- // Record the byte offset where the tail should start: the beginning of
409
- // the NEXT line after this user message (see tailOffsetAfter).
410
- if (obj.type === 'user') {
411
- const msg = obj.message;
412
- if (msg?.role === 'user') {
413
- tailOffset = tailOffsetAfter(line, offset);
414
- }
415
- }
416
- // --- ai-title extraction ---
417
- if (obj.type === 'ai-title' && typeof obj.aiTitle === 'string') {
418
- lastAiTitle = obj.aiTitle;
419
- }
420
- // --- recap extraction (last isCompactSummary wins) ---
421
- if (obj.type === 'user' && obj.isCompactSummary === true) {
422
- const msg = obj.message;
423
- if (msg?.content) {
424
- const recapContent = extractText(msg.content);
425
- if (recapContent) {
426
- lastRecap = recapContent;
427
- }
428
- }
429
- }
430
- // --- last user message for displayTitle ---
431
- // Skip: 1) tool_result content, 2) isCompactSummary (recap),
432
- // 3) isMeta (skill injection / command echo / project rules)
433
- if (obj.type === 'user' && !obj.isCompactSummary && !obj.isMeta) {
434
- const msg = obj.message;
435
- if (msg?.role === 'user' && msg.content) {
436
- const content = msg.content;
437
- const isToolResult = Array.isArray(content) &&
438
- content.length > 0 &&
439
- content[0]?.type === 'tool_result';
440
- if (!isToolResult) {
441
- const userContent = extractText(content);
442
- if (userContent && !isTaskNotificationText(userContent)) {
443
- lastUserMessage = userContent;
444
- }
445
- }
446
- }
447
- }
448
- // --- turn boundary for per-run usage ---
449
- // 真人 prompt 开启新 turn;注入/工具结果/sidechain 不递增。usage 记录在
450
- // 解析到 assistant 行时已经带上当前 turnSeq,末尾只汇总末个 turn。
451
- if (isRealUserPrompt(obj))
452
- turnSeq++;
453
- });
361
+ // would pin the zero record and discard the real one (auto-resume card
362
+ // per-run stats all 0). Per-field max reconstructs the full usage
363
+ // regardless of line order — on the official API every line repeats
364
+ // identical usage, so max is a no-op there.
365
+ return {
366
+ usageByMessageId: new Map(),
367
+ lastPostTokens: 0,
368
+ compactCount: 0,
369
+ tailOffset: -1,
370
+ turnSeq: 0,
371
+ };
372
+ }
373
+ /** assistant 行 → 按 message id 逐字段 max 合并 usage(was aggregateSessionUsage)。 */
374
+ function collectAssistantUsage(state, obj) {
375
+ if (obj.type !== 'assistant')
376
+ return;
377
+ const msg = obj.message;
378
+ const usage = msg?.usage;
379
+ if (!usage)
380
+ return;
381
+ const msgId = msg?.id;
382
+ if (!msgId)
383
+ return;
384
+ state.usageByMessageId.set(msgId, maxUsageRecord(state.usageByMessageId.get(msgId), usage, state.turnSeq));
385
+ }
386
+ /** compact_boundary → 计数 + 压缩后 token 水位。 */
387
+ function collectCompactBoundary(state, obj) {
388
+ if (obj.type !== 'system' || obj.subtype !== 'compact_boundary')
389
+ return;
390
+ state.compactCount++;
391
+ const cm = obj.compactMetadata;
392
+ if (cm?.postTokens) {
393
+ state.lastPostTokens = cm.postTokens;
394
+ }
395
+ }
396
+ /** 记录 tail 起点:本 user 行之后的下一行字节偏移(见 tailOffsetAfter)。 */
397
+ function collectTailOffset(state, obj, line, offset) {
398
+ if (obj.type !== 'user')
399
+ return;
400
+ const msg = obj.message;
401
+ if (msg?.role === 'user') {
402
+ state.tailOffset = tailOffsetAfter(line, offset);
403
+ }
404
+ }
405
+ /** ai-title:最后一条生效。 */
406
+ function collectAiTitle(state, obj) {
407
+ if (obj.type === 'ai-title' && typeof obj.aiTitle === 'string') {
408
+ state.lastAiTitle = obj.aiTitle;
409
+ }
410
+ }
411
+ /** recap:最后一条 isCompactSummary 生效。 */
412
+ function collectRecap(state, obj) {
413
+ if (obj.type !== 'user' || obj.isCompactSummary !== true)
414
+ return;
415
+ const msg = obj.message;
416
+ if (!msg?.content)
417
+ return;
418
+ const recapContent = extractText(msg.content);
419
+ if (recapContent) {
420
+ state.lastRecap = recapContent;
421
+ }
422
+ }
423
+ /** displayTitle 用的最近真人输入:跳过 tool_result / recap / isMeta / 任务通知。 */
424
+ function collectLastUserMessage(state, obj) {
425
+ if (obj.type !== 'user' || obj.isCompactSummary || obj.isMeta)
426
+ return;
427
+ const msg = obj.message;
428
+ if (msg?.role !== 'user' || !msg.content)
429
+ return;
430
+ const content = msg.content;
431
+ const isToolResult = Array.isArray(content) &&
432
+ content.length > 0 &&
433
+ content[0]?.type === 'tool_result';
434
+ if (isToolResult)
435
+ return;
436
+ const userContent = extractText(content);
437
+ if (userContent && !isTaskNotificationText(userContent)) {
438
+ state.lastUserMessage = userContent;
439
+ }
440
+ }
441
+ /** 真人 prompt 开启新 turn;注入/工具结果/sidechain 不递增。 */
442
+ function countRealUserPrompt(state, obj) {
443
+ if (isRealUserPrompt(obj))
444
+ state.turnSeq++;
445
+ }
446
+ /**
447
+ * 单行分派:收集器调用顺序与原回调逐字一致(尤其 turnSeq 在最后递增,
448
+ * 保证 assistant usage 记录归属的是「本行之前」的 turn)。
449
+ */
450
+ function scanScalarLine(state, obj, line, offset) {
451
+ collectAssistantUsage(state, obj);
452
+ collectCompactBoundary(state, obj);
453
+ collectTailOffset(state, obj, line, offset);
454
+ collectAiTitle(state, obj);
455
+ collectRecap(state, obj);
456
+ collectLastUserMessage(state, obj);
457
+ countRealUserPrompt(state, obj);
458
+ }
459
+ function scalarScan(filePath) {
460
+ const state = newScalarScanState();
461
+ scanJsonlOnce(filePath, scanScalarLine.bind(null, state));
454
462
  return {
455
- tailOffset,
456
- usage: buildClaudeUsage(usageByMessageId.values(), { turnSeq, lastPostTokens, compactCount }),
457
- aiTitle: lastAiTitle,
458
- recap: lastRecap,
459
- lastUserMessage,
463
+ tailOffset: state.tailOffset,
464
+ usage: buildClaudeUsage(state.usageByMessageId.values(), {
465
+ turnSeq: state.turnSeq,
466
+ lastPostTokens: state.lastPostTokens,
467
+ compactCount: state.compactCount,
468
+ }),
469
+ aiTitle: state.lastAiTitle,
470
+ recap: state.lastRecap,
471
+ lastUserMessage: state.lastUserMessage,
460
472
  };
461
473
  }
462
474
  /**