lark-remote 0.5.10 → 0.5.13

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 (126) hide show
  1. package/CHANGELOG.md +51 -2
  2. package/README.en.md +1 -14
  3. package/README.md +1 -14
  4. package/dist/binder.js +7 -1
  5. package/dist/bridge/approval-coordinator.d.ts +1 -0
  6. package/dist/bridge/approval-coordinator.js +12 -24
  7. package/dist/bridge/index.d.ts +7 -0
  8. package/dist/bridge/index.js +89 -230
  9. package/dist/bridge/run-approval.d.ts +15 -0
  10. package/dist/bridge/run-approval.js +41 -0
  11. package/dist/bridge/run-usage.d.ts +46 -0
  12. package/dist/bridge/run-usage.js +96 -0
  13. package/dist/bridge/session-writeback.d.ts +18 -0
  14. package/dist/bridge/session-writeback.js +36 -0
  15. package/dist/card/approval-render.d.ts +14 -0
  16. package/dist/card/approval-render.js +155 -29
  17. package/dist/card/bash-card-session.js +10 -5
  18. package/dist/card/bash-renderer.js +11 -23
  19. package/dist/card/card-budget.js +24 -4
  20. package/dist/card/card-session.d.ts +2 -0
  21. package/dist/card/card-session.js +20 -22
  22. package/dist/card/card-shared.d.ts +4 -4
  23. package/dist/card/card-shared.js +3 -9
  24. package/dist/card/collapsible.js +3 -2
  25. package/dist/card/final-answer-overflow.d.ts +7 -0
  26. package/dist/card/final-answer-overflow.js +46 -0
  27. package/dist/card/run-card-session.js +2 -7
  28. package/dist/card/run-renderer.d.ts +14 -0
  29. package/dist/card/run-renderer.js +150 -10
  30. package/dist/card/run-state.d.ts +12 -1
  31. package/dist/card/run-state.js +73 -34
  32. package/dist/common/text-width.js +5 -0
  33. package/dist/common/tool-summary.js +4 -2
  34. package/dist/config/codex-config.js +5 -3
  35. package/dist/config/index.d.ts +4 -2
  36. package/dist/config/index.js +19 -5
  37. package/dist/inbound/turn-assembler.js +6 -0
  38. package/dist/index.js +15 -391
  39. package/dist/instance-lock.d.ts +9 -1
  40. package/dist/instance-lock.js +42 -7
  41. package/dist/message-handlers.d.ts +26 -0
  42. package/dist/message-handlers.js +275 -0
  43. package/dist/platform/identity.js +4 -3
  44. package/dist/platform/path.js +3 -1
  45. package/dist/platform/terminator-win32.d.ts +13 -1
  46. package/dist/platform/terminator-win32.js +4 -14
  47. package/dist/router/card-action-payload.d.ts +1 -1
  48. package/dist/router/card-action-payload.js +1 -1
  49. package/dist/router/commands/ls.d.ts +9 -0
  50. package/dist/router/commands/ls.js +243 -0
  51. package/dist/router/commands/order.d.ts +12 -0
  52. package/dist/router/commands/order.js +178 -0
  53. package/dist/router/commands/resume.d.ts +47 -0
  54. package/dist/router/commands/resume.js +246 -0
  55. package/dist/router/commands/ws.d.ts +15 -0
  56. package/dist/router/commands/ws.js +189 -0
  57. package/dist/router/config/claude.js +3 -3
  58. package/dist/router/config/codex.js +2 -6
  59. package/dist/router/config/common/turn-idle.d.ts +2 -0
  60. package/dist/router/config/common/turn-idle.js +9 -0
  61. package/dist/router/config/kimi.js +2 -6
  62. package/dist/router/config/opencode.js +2 -6
  63. package/dist/router/index.d.ts +14 -22
  64. package/dist/router/index.js +93 -1133
  65. package/dist/router/paging.d.ts +13 -0
  66. package/dist/router/paging.js +20 -0
  67. package/dist/router/path-validation.d.ts +27 -0
  68. package/dist/router/path-validation.js +45 -0
  69. package/dist/runner/claude/runner.d.ts +3 -4
  70. package/dist/runner/claude/runner.js +4 -5
  71. package/dist/runner/claude/session.js +1 -1
  72. package/dist/runner/codex/app-server/runner.js +12 -0
  73. package/dist/runner/codex/app-server/translator.d.ts +2 -0
  74. package/dist/runner/codex/app-server/translator.js +65 -9
  75. package/dist/runner/common/acp/base-translator.d.ts +15 -8
  76. package/dist/runner/common/acp/base-translator.js +38 -7
  77. package/dist/runner/common/jsonl-stream.js +12 -1
  78. package/dist/runner/common/jsonrpc/connection-manager.js +11 -1
  79. package/dist/runner/common/jsonrpc/transport.d.ts +2 -1
  80. package/dist/runner/common/jsonrpc/transport.js +7 -1
  81. package/dist/runner/common/spawning-runner.js +10 -4
  82. package/dist/runner/common/turn-started.d.ts +2 -0
  83. package/dist/runner/common/turn-started.js +10 -0
  84. package/dist/runner/dsh/approval.js +16 -3
  85. package/dist/runner/dsh/auth.js +2 -3
  86. package/dist/runner/dsh/client.d.ts +32 -7
  87. package/dist/runner/dsh/client.js +115 -149
  88. package/dist/runner/dsh/daemon.d.ts +0 -2
  89. package/dist/runner/dsh/daemon.js +1 -6
  90. package/dist/runner/dsh/errors.d.ts +5 -0
  91. package/dist/runner/dsh/errors.js +11 -0
  92. package/dist/runner/dsh/exit-cancel.d.ts +0 -2
  93. package/dist/runner/dsh/exit-cancel.js +63 -24
  94. package/dist/runner/dsh/home.d.ts +2 -0
  95. package/dist/runner/dsh/home.js +8 -0
  96. package/dist/runner/dsh/inflight.d.ts +0 -2
  97. package/dist/runner/dsh/inflight.js +1 -1
  98. package/dist/runner/dsh/mux-stream.d.ts +26 -0
  99. package/dist/runner/dsh/mux-stream.js +157 -0
  100. package/dist/runner/dsh/runner.d.ts +7 -0
  101. package/dist/runner/dsh/runner.js +42 -5
  102. package/dist/runner/dsh/translator.d.ts +0 -10
  103. package/dist/runner/dsh/translator.js +1 -1
  104. package/dist/runner/index.d.ts +1 -1
  105. package/dist/runner/kimi/acp/runner.js +42 -5
  106. package/dist/runner/kimi/acp/translator.d.ts +8 -0
  107. package/dist/runner/kimi/acp/translator.js +11 -12
  108. package/dist/runner/opencode/acp/translator.js +1 -12
  109. package/dist/runner/pi/rpc/runner.js +33 -5
  110. package/dist/runner/pi/rpc/translator.d.ts +2 -8
  111. package/dist/runner/pi/rpc/translator.js +2 -7
  112. package/dist/runner/registry.d.ts +0 -19
  113. package/dist/runner/registry.js +0 -32
  114. package/dist/runner/resolve-agent-choices.js +5 -1
  115. package/dist/runner/sync-agent-choices.js +4 -1
  116. package/dist/runner/types.d.ts +17 -1
  117. package/dist/session/claude/session-index.js +13 -4
  118. package/dist/session/codex/rollout-reader.js +51 -26
  119. package/dist/session/common/usage-accumulator.d.ts +10 -1
  120. package/dist/session/common/usage-accumulator.js +10 -2
  121. package/dist/session/dsh/sessions.js +29 -9
  122. package/dist/session/kimi/sessions.js +11 -7
  123. package/dist/session/opencode/sessions.js +15 -7
  124. package/dist/session/pi/sessions.js +3 -2
  125. package/dist/session/session-store.js +4 -3
  126. package/package.json +1 -1
@@ -263,6 +263,22 @@ export interface AssistantEvent {
263
263
  content: AssistantContent[];
264
264
  };
265
265
  }
266
+ /**
267
+ * Authoritative final answer from an agent protocol.
268
+ *
269
+ * This is deliberately separate from ordinary assistant text: progress
270
+ * narration and tool-loop messages have lower display priority than the answer
271
+ * the user actually asked for. Agents without an explicit protocol marker
272
+ * promote their last text block when the turn finishes.
273
+ */
274
+ export interface FinalAnswerEvent {
275
+ type: 'final_answer';
276
+ text: string;
277
+ /** Source item id, used to remove the streamed progress copy of the answer. */
278
+ itemId?: string;
279
+ complete?: boolean;
280
+ timestamp?: string;
281
+ }
266
282
  export interface UserEvent {
267
283
  type: 'user';
268
284
  timestamp?: string;
@@ -470,7 +486,7 @@ export interface TurnDiffEvent {
470
486
  export type RunnerLifetime = 'turn' | 'workspace';
471
487
  /** 工具渲染 hint:runner 自愿声明,渲染层只消费不推断。 */
472
488
  export type ToolHint = 'shell' | 'file' | 'search' | 'web' | 'patch';
473
- export type AgentEvent = SystemInitEvent | SystemCompactEvent | ApprovalRequestedEvent | ApprovalResolvedEvent | ApprovalViewUpdatedEvent | ApprovalExpiredEvent | AssistantEvent | UserEvent | ResultEvent | PlanEvent | FileChangeEvent | TurnStartedEvent | TurnDiffEvent | NoticeEvent | SessionInfoEvent | RunStatusEvent | BackgroundJobsEvent | GoalStateEvent;
489
+ export type AgentEvent = SystemInitEvent | SystemCompactEvent | ApprovalRequestedEvent | ApprovalResolvedEvent | ApprovalViewUpdatedEvent | ApprovalExpiredEvent | AssistantEvent | FinalAnswerEvent | UserEvent | ResultEvent | PlanEvent | FileChangeEvent | TurnStartedEvent | TurnDiffEvent | NoticeEvent | SessionInfoEvent | RunStatusEvent | BackgroundJobsEvent | GoalStateEvent;
474
490
  export interface SpawnOptions {
475
491
  cwd: string;
476
492
  sessionId?: string;
@@ -62,8 +62,15 @@ export function parseSessionJsonl(filePath) {
62
62
  if (typeof obj.cwd === 'string' && obj.cwd) {
63
63
  cwdSet.add(obj.cwd);
64
64
  }
65
- // Find first real user message for summary (same logic as summarizeSession)
66
- if (!foundSummary && obj.type === 'user' && !!obj.message) {
65
+ // Find first real user message for summary (same logic as summarizeSession).
66
+ // 过滤谓词与 scalarScan 的 lastUserMessage 同源:isMeta(/clear 等命令回显
67
+ // 与项目规则注入)和 isCompactSummary(压缩摘要)不是用户说的话,当列表
68
+ // 标题会误导(codex 修过同型缺陷:注入文本被当成「最近输入」)。
69
+ if (!foundSummary &&
70
+ obj.type === 'user' &&
71
+ !obj.isCompactSummary &&
72
+ !obj.isMeta &&
73
+ !!obj.message) {
67
74
  const msg = obj.message;
68
75
  if (msg.content) {
69
76
  const text = extractText(msg.content);
@@ -270,14 +277,16 @@ export class SessionIndex {
270
277
  if (!st.isDirectory())
271
278
  continue;
272
279
  }
273
- catch {
280
+ catch (err) {
281
+ getLogger().debug(`[session-index] skipped unreadable project dir ${subdir}: ${err?.message ?? String(err)}`);
274
282
  continue;
275
283
  }
276
284
  let files;
277
285
  try {
278
286
  files = fs.readdirSync(subdir).filter((f) => f.endsWith('.jsonl'));
279
287
  }
280
- catch {
288
+ catch (err) {
289
+ getLogger().debug(`[session-index] skipped unreadable project subdir ${subdir}: ${err?.message ?? String(err)}`);
281
290
  continue;
282
291
  }
283
292
  for (const f of files) {
@@ -455,6 +455,14 @@ export function readCodexSessionContent(sessionId, opts = {}) {
455
455
  if (!entry) {
456
456
  return { events: [] };
457
457
  }
458
+ // subagent rollout 不得当父会话历史返回:子线程 rollout 的
459
+ // session_meta.session_id 是父 id(§9.27),索引冲突解决在主文件缺失时会让
460
+ // 子文件占据该 id 的槽位,threadId 检查因等于父 id 而天然通过——list/active
461
+ // 都已过滤(isSubagent),按 id 读取必须同口径,否则「列表没有、active 报
462
+ // false、读内容却成功」三处语义互相矛盾。
463
+ if (entry.isSubagent) {
464
+ return { events: [] };
465
+ }
458
466
  // Cwd guard: when a cwd is provided, the session's working directory must
459
467
  // match. Without this, /resume <id> finds sessions by global ID lookup and
460
468
  // allows resuming a session from cwd B while the user is in cwd A.
@@ -495,6 +503,10 @@ export function readCodexSessionSummary(sessionId, opts = {}) {
495
503
  if (!entry) {
496
504
  return {};
497
505
  }
506
+ // subagent rollout 同样不得当父会话摘要(与 readCodexSessionContent 同口径)。
507
+ if (entry.isSubagent) {
508
+ return {};
509
+ }
498
510
  // Cwd guard: same semantics as readCodexSessionContent — when a cwd is
499
511
  // provided, the session's working directory must match.
500
512
  if (opts.cwd && !samePath(entry.cwd, opts.cwd)) {
@@ -560,42 +572,55 @@ function walkRolloutFiles(sessionsDir) {
560
572
  if (!fs.existsSync(sessionsDir))
561
573
  return [];
562
574
  const result = [];
563
- try {
564
- const years = fs.readdirSync(sessionsDir);
565
- for (const year of years) {
566
- if (!/^\d{4}$/.test(year))
575
+ // per-entry try/catch:目录在 readdir 与 stat 之间被删(并发归档/清理)时,
576
+ // 旧实现整个 walk 提前终止并返回部分结果——索引漏掉后续文件,会话从
577
+ // /resume 列表与 total 消失直到 5s TTL 重建。单条目失败只跳过自己。
578
+ const years = safeReaddir(sessionsDir);
579
+ for (const year of years) {
580
+ if (!/^\d{4}$/.test(year))
581
+ continue;
582
+ const yearPath = path.join(sessionsDir, year);
583
+ if (!safeIsDir(yearPath))
584
+ continue;
585
+ for (const month of safeReaddir(yearPath)) {
586
+ if (!/^\d{2}$/.test(month))
567
587
  continue;
568
- const yearPath = path.join(sessionsDir, year);
569
- if (!fs.statSync(yearPath).isDirectory())
588
+ const monthPath = path.join(yearPath, month);
589
+ if (!safeIsDir(monthPath))
570
590
  continue;
571
- const months = fs.readdirSync(yearPath);
572
- for (const month of months) {
573
- if (!/^\d{2}$/.test(month))
591
+ for (const day of safeReaddir(monthPath)) {
592
+ if (!/^\d{2}$/.test(day))
574
593
  continue;
575
- const monthPath = path.join(yearPath, month);
576
- if (!fs.statSync(monthPath).isDirectory())
594
+ const dayPath = path.join(monthPath, day);
595
+ if (!safeIsDir(dayPath))
577
596
  continue;
578
- const days = fs.readdirSync(monthPath);
579
- for (const day of days) {
580
- if (!/^\d{2}$/.test(day))
581
- continue;
582
- const dayPath = path.join(monthPath, day);
583
- if (!fs.statSync(dayPath).isDirectory())
584
- continue;
585
- const files = fs
586
- .readdirSync(dayPath)
587
- .filter((f) => f.startsWith('rollout-') && f.endsWith('.jsonl'));
588
- for (const file of files) {
589
- result.push(path.join(dayPath, file));
590
- }
597
+ const files = safeReaddir(dayPath).filter((f) => f.startsWith('rollout-') && f.endsWith('.jsonl'));
598
+ for (const file of files) {
599
+ result.push(path.join(dayPath, file));
591
600
  }
592
601
  }
593
602
  }
594
603
  }
604
+ return result;
605
+ }
606
+ /** readdir 的竞态安全版:目录消失/不可读返回空并记 debug(外层不再中断)。 */
607
+ function safeReaddir(dir) {
608
+ try {
609
+ return fs.readdirSync(dir);
610
+ }
595
611
  catch (err) {
596
- getLogger().warn(`[codex-rollout-reader] failed to walk sessions dir: ${err}`);
612
+ getLogger().debug(`[codex-rollout-reader] readdir failed (raced delete?): ${err}`);
613
+ return [];
614
+ }
615
+ }
616
+ /** statSync 的竞态安全目录判定。 */
617
+ function safeIsDir(p) {
618
+ try {
619
+ return fs.statSync(p).isDirectory();
620
+ }
621
+ catch {
622
+ return false;
597
623
  }
598
- return result;
599
624
  }
600
625
  const INDEX_TTL_MS = 5_000; // 5 seconds — covers a single /resume flow
601
626
  const sessionIndexCache = new Map();
@@ -55,9 +55,18 @@ export declare class UsageAccumulator {
55
55
  */
56
56
  bumpCompact(): void;
57
57
  }
58
- /** 统一契约:上下文窗口占用 = 末轮 input + cacheRead + cacheCreation(不含 output/reasoning)。 */
58
+ /**
59
+ * 统一契约:上下文窗口占用 = 末轮 input + output + cacheRead + cacheCreation。
60
+ *
61
+ * 口径以 design.md §9.13/§9.21 为准(文档在 6 处钉死该公式):上下文占用衡量
62
+ * 「本轮结束后窗口里有多少 token」,末轮 output 已进入历史、下一个 prompt 会
63
+ * 全量携带;与 max() 另一侧的 compact postTokens(含全部内容的压缩后水位)
64
+ * 语义对齐。此前实现漏加 output(长收尾 turn 可低估数千~3 万 token),且
65
+ * compact 会话与非 compact 会话两口径不可比。
66
+ */
59
67
  export declare function contextWindowOccupancy(l: {
60
68
  input: number;
69
+ output: number;
61
70
  cacheRead: number;
62
71
  cacheCreation: number;
63
72
  }): number;
@@ -69,9 +69,17 @@ export class UsageAccumulator {
69
69
  this._compactCount++;
70
70
  }
71
71
  }
72
- /** 统一契约:上下文窗口占用 = 末轮 input + cacheRead + cacheCreation(不含 output/reasoning)。 */
72
+ /**
73
+ * 统一契约:上下文窗口占用 = 末轮 input + output + cacheRead + cacheCreation。
74
+ *
75
+ * 口径以 design.md §9.13/§9.21 为准(文档在 6 处钉死该公式):上下文占用衡量
76
+ * 「本轮结束后窗口里有多少 token」,末轮 output 已进入历史、下一个 prompt 会
77
+ * 全量携带;与 max() 另一侧的 compact postTokens(含全部内容的压缩后水位)
78
+ * 语义对齐。此前实现漏加 output(长收尾 turn 可低估数千~3 万 token),且
79
+ * compact 会话与非 compact 会话两口径不可比。
80
+ */
73
81
  export function contextWindowOccupancy(l) {
74
- return l.input + l.cacheRead + l.cacheCreation;
82
+ return l.input + l.output + l.cacheRead + l.cacheCreation;
75
83
  }
76
84
  /** 统一契约:session 全量累计字段组装(4 个 reader 共用:claude/dsh/kimi/pi;
77
85
  * codex 走 last_token_usage 口径、opencode 保留自己的条件赋值,原为逐字段复制的 4 连拷贝)。 */
@@ -285,7 +285,11 @@ export class DshSessionReader {
285
285
  const input = lastUsage.inputTokens ?? 0;
286
286
  const cacheRead = lastUsage.cacheReadTokens ?? 0;
287
287
  const cacheWrite = lastUsage.cacheWriteTokens ?? 0;
288
- const flow = lastTurnUsage ?? {
288
+ // 在途 turn(turn/start 无 turn/end,run 进行中读历史)的采样优先于
289
+ // 上一轮:旧实现只认已结算的 lastTurnUsage,mid-run 读取会把当前 turn
290
+ // 已消耗的量显示成上一个完整 turn 的数字。
291
+ const flow = lastTurnUsage ??
292
+ turnUsage ?? {
289
293
  input,
290
294
  output: lastUsage.outputTokens ?? 0,
291
295
  cacheRead,
@@ -294,7 +298,8 @@ export class DshSessionReader {
294
298
  sessionUsage = {
295
299
  inputTokens: flow.input,
296
300
  outputTokens: flow.output,
297
- contextLength: input + cacheRead + cacheWrite,
301
+ // §9.21 口径:含末轮 output(下一 prompt 全量携带,与 postTokens 水位同语义)。
302
+ contextLength: input + flow.output + cacheRead + cacheWrite,
298
303
  cacheReadTokens: flow.cacheRead,
299
304
  cacheCreationTokens: flow.cacheWrite,
300
305
  totalTokens: flow.input + flow.output + flow.cacheRead + flow.cacheWrite,
@@ -432,11 +437,27 @@ const TEXT_MAX_BYTES = 2_000;
432
437
  /** 工具原始参数上限(标题已有摘要,正文只用于看完整命令)。 */
433
438
  const TOOL_INPUT_MAX_BYTES = 600;
434
439
  /** Map one DSH SessionEvent to display content events (+ title/usage side effects). */
440
+ /**
441
+ * 单条畸形事件只跳过自己,不炸整张卡:DSH 服务端格式漂移时(time 非法、
442
+ * content 非数组),旧实现会让异常冒到外层 catch 把整个 resume 卡变成空卡
443
+ * (fail-whole)。时间戳缺失由卡片层兜底;content 非数组按无块处理。
444
+ */
445
+ function safeEventTimestamp(time) {
446
+ if (typeof time !== 'string' && typeof time !== 'number')
447
+ return undefined;
448
+ const d = new Date(time);
449
+ return Number.isNaN(d.getTime()) ? undefined : d.toISOString();
450
+ }
451
+ function asBlocks(value) {
452
+ if (!Array.isArray(value))
453
+ return [];
454
+ return value.filter((b) => !!b && typeof b === 'object');
455
+ }
435
456
  function mapEvent(ev) {
436
457
  const out = { events: [] };
437
458
  switch (ev.type) {
438
459
  case 'user/message': {
439
- const content = ev.data.content ?? [];
460
+ const content = asBlocks(ev.data.content);
440
461
  const text = content
441
462
  .filter((c) => c.type === 'text')
442
463
  .map((c) => c.text ?? '')
@@ -456,14 +477,13 @@ function mapEvent(ev) {
456
477
  type: isNotice ? 'notice' : 'user',
457
478
  ...(isNotice ? { title: '🔔 通知' } : {}),
458
479
  content: isNotice ? truncateUtf8(text.trim(), NOTICE_MAX_BYTES) : text,
459
- timestamp: new Date(ev.time).toISOString(),
480
+ timestamp: safeEventTimestamp(ev.time),
460
481
  });
461
482
  break;
462
483
  }
463
484
  case 'assistant/message': {
464
- const content = ev.data.message
465
- ?.content ?? [];
466
- const timestamp = new Date(ev.time).toISOString();
485
+ const content = asBlocks(ev.data.message?.content);
486
+ const timestamp = safeEventTimestamp(ev.time);
467
487
  // DSH 的块类型是 text / reasoning / tool-call(不是 Anthropic 的
468
488
  // thinking)。只认 text 会让 resume 卡"只有工具调用、没有思考与输出"
469
489
  // (2026-10-19 反馈:一个 31 turn 的会话里 reasoning 块 371 个、text 12 个,
@@ -503,7 +523,7 @@ function mapEvent(ev) {
503
523
  type: 'tool_use',
504
524
  title: summary ? `🔧 ${name} — ${summary}` : `🔧 ${name}`,
505
525
  content: truncateUtf8(args, TOOL_INPUT_MAX_BYTES),
506
- timestamp: new Date(ev.time).toISOString(),
526
+ timestamp: safeEventTimestamp(ev.time),
507
527
  };
508
528
  break;
509
529
  }
@@ -515,7 +535,7 @@ function mapEvent(ev) {
515
535
  type: 'tool_result',
516
536
  title: isError ? '🔴 结果(失败)' : '🟢 结果',
517
537
  content: truncateUtf8(raw, TOOL_RESULT_MAX_BYTES),
518
- timestamp: new Date(ev.time).toISOString(),
538
+ timestamp: safeEventTimestamp(ev.time),
519
539
  });
520
540
  break;
521
541
  }
@@ -215,8 +215,8 @@ export class KimiSessionReader {
215
215
  state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
216
216
  stateParsed = true;
217
217
  }
218
- catch {
219
- // Corrupt state.json — cannot determine cwd → skip below
218
+ catch (err) {
219
+ getLogger().debug(`[kimi-session-reader] corrupt state.json skipped session=${entry.sessionId}: ${err?.message ?? String(err)}`);
220
220
  }
221
221
  }
222
222
  // Index workDir is v1-only; v2 sessions may have empty/missing workDir.
@@ -227,7 +227,10 @@ export class KimiSessionReader {
227
227
  entryWorkDir = extractWorkDir(state);
228
228
  // Unreadable state — cannot determine cwd, entryWorkDir stays undefined
229
229
  }
230
- if (entryWorkDir !== realCwd) {
230
+ // cwd 判定与守卫(checkCwdGuard 的 samePath)同口径:严格字符串比较会
231
+ // 把大小写(darwin)/尾分隔符形态不同的会话从列表里滤掉,而 /resume <id>
232
+ // 的守卫却判等通过——「列表没有、按 id 却能读」。
233
+ if (entryWorkDir === undefined || !samePath(entryWorkDir, realCwd)) {
231
234
  continue;
232
235
  }
233
236
  let summary = 'New Session';
@@ -332,9 +335,10 @@ export class KimiSessionReader {
332
335
  cacheCreation: inputCacheCreation,
333
336
  });
334
337
  if (updateContextLength) {
335
- // unified contract: input + cacheRead + cacheCreation
336
- // (excludes output — generated, not part of the input window).
337
- mainContextLength = inputOther + inputCacheRead + inputCacheCreation;
338
+ // unified contract (§9.21): input + output + cacheRead + cacheCreation.
339
+ // 末轮 output 已进入历史、下一 prompt 全量携带——与 compact postTokens
340
+ // 水位同语义(此前漏加 output 会系统性低估长收尾 turn)。
341
+ mainContextLength = inputOther + output + inputCacheRead + inputCacheCreation;
338
342
  }
339
343
  };
340
344
  for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
@@ -593,7 +597,7 @@ export class KimiSessionReader {
593
597
  // different cwd when state.json is unavailable.
594
598
  if (!cwdGuardPassed) {
595
599
  const indexWorkDir = indexEntry?.workDir;
596
- if (indexWorkDir && indexWorkDir !== realCwd) {
600
+ if (indexWorkDir && !samePath(indexWorkDir, realCwd)) {
597
601
  return null;
598
602
  }
599
603
  // v2 sessions have empty index workDir (v1-only field) and may lack
@@ -243,14 +243,16 @@ export class OpencodeSessionReader {
243
243
  // all render 0). Skip zero-total steps so per-turn usage reflects
244
244
  // the last step that actually consumed tokens.
245
245
  // ccusage-aligned: cache.write -> cacheCreationTokens.
246
- // contextLength = input + cacheRead + cacheCreation (excludes
247
- // output/reasoning) — the unified context-window occupancy
248
- // contract across all five readers.
246
+ // contextLength = input + output + cacheRead + cacheCreation —
247
+ // the unified context-window occupancy contract across all readers
248
+ // (§9.21:末轮 output 已进入历史、下一 prompt 全量携带,与 compact
249
+ // postTokens 水位同语义).
249
250
  if ((part.tokens.total ?? 0) !== 0) {
250
251
  usage = {
251
252
  inputTokens: part.tokens.input,
252
253
  outputTokens: part.tokens.output,
253
- contextLength: part.tokens.input +
254
+ contextLength: (part.tokens.input ?? 0) +
255
+ (part.tokens.output ?? 0) +
254
256
  (part.tokens.cache?.read ?? 0) +
255
257
  (part.tokens.cache?.write ?? 0),
256
258
  cacheReadTokens: part.tokens.cache?.read,
@@ -460,7 +462,8 @@ export class OpencodeSessionReader {
460
462
  latest = {
461
463
  inputTokens: tokens.input,
462
464
  outputTokens: tokens.output,
463
- contextLength: tokens.input + cacheRead + cacheWrite,
465
+ // §9.21 口径:含末轮 output(与 postTokens 水位同语义)。
466
+ contextLength: tokens.input + tokens.output + cacheRead + cacheWrite,
464
467
  cacheReadTokens: cacheRead,
465
468
  cacheCreationTokens: cacheWrite,
466
469
  totalTokens: total,
@@ -507,7 +510,8 @@ export class OpencodeSessionReader {
507
510
  // Check if updated within STALE_MS
508
511
  return !isStale(entry.updated);
509
512
  }
510
- catch {
513
+ catch (err) {
514
+ getLogger().debug(`[opencode-session-reader] isSessionActive failed session=${sessionId}: ${err?.message ?? String(err)}`);
511
515
  return false;
512
516
  }
513
517
  }
@@ -628,7 +632,11 @@ export class OpencodeSessionReader {
628
632
  * stdout when it is redirected to a file fd, so we read the file instead.
629
633
  */
630
634
  captureExportViaFileFd(sessionId) {
631
- const tmp = path.join(os.tmpdir(), `opencode-export-${sessionId}-${process.pid}-${Date.now()}.json`);
635
+ // sessionId 经 path.basename 净化:它来自用户 /resume <id> 输入或畸形 CLI
636
+ // 数据,含 / 或 .. 时 path.join 会归一化出 tmpdir 之外的路径(写入内容为
637
+ // CLI stdout,可能截断/覆盖目标文件)。
638
+ const safeId = path.basename(sessionId);
639
+ const tmp = path.join(os.tmpdir(), `opencode-export-${safeId}-${process.pid}-${Date.now()}.json`);
632
640
  let fd;
633
641
  try {
634
642
  const exportOpts = {
@@ -10,6 +10,7 @@ import { encodeProjectDirName, samePath } from '../../platform/path.js';
10
10
  import { paginate } from '../common/pagination.js';
11
11
  import { sortByRecencyDesc } from '../common/recency.js';
12
12
  import { TtlCache } from '../../common/ttl-cache.js';
13
+ import { getLogger } from '../../logger/index.js';
13
14
  function defaultPiDir() {
14
15
  return path.join(os.homedir(), '.pi', 'agent');
15
16
  }
@@ -356,8 +357,8 @@ export class PiSessionReader {
356
357
  mtime: st.mtimeMs,
357
358
  });
358
359
  }
359
- catch {
360
- // skip
360
+ catch (err) {
361
+ getLogger().debug(`[pi-session-reader] skipped ${full}: ${err?.message ?? String(err)}`);
361
362
  }
362
363
  }
363
364
  // Same-mtime ties: secondary key keeps the full order deterministic.
@@ -230,9 +230,10 @@ export class SessionStore {
230
230
  for (const [userId, data] of Object.entries(parsed)) {
231
231
  if (typeof data === 'object' && data !== null) {
232
232
  const entry = data;
233
- // All four session fields are required; a missing one indicates a
234
- // corrupt record — skip it entirely. cwd is allowed to be absent
235
- // (legitimately-empty cwd state, see round7 tests).
233
+ // 四个 session 字段缺失 = 损坏记录,整条跳过。cwd 允许缺失:
234
+ // 全新用户(未 /cd)先切 agent 时 previous/arrival 停车位已落盘,
235
+ // cwd 尚为空——不能因 cwd 缺失把停车位整条丢掉
236
+ // (anchor_r7_parked_previous_survives_rebuild_without_cwd)。
236
237
  if (!entry.sessions ||
237
238
  !entry.previousSessions ||
238
239
  !entry.arrivalSessions ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lark-remote",
3
- "version": "0.5.10",
3
+ "version": "0.5.13",
4
4
  "description": "Feishu (Lark) private-chat bridge to local coding agents — Claude Code / Codex / opencode / pi / kimi, with live-streaming CardKit 2.0 cards",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",