lark-remote 0.5.12 → 0.5.14

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 (172) hide show
  1. package/CHANGELOG.md +28 -2
  2. package/README.en.md +1 -14
  3. package/README.md +1 -14
  4. package/dist/agent-kinds.d.ts +27 -0
  5. package/dist/agent-kinds.js +38 -0
  6. package/dist/bridge/approval-actions.d.ts +95 -0
  7. package/dist/bridge/approval-actions.js +104 -0
  8. package/dist/bridge/approval-coordinator.d.ts +1 -0
  9. package/dist/bridge/approval-coordinator.js +12 -24
  10. package/dist/bridge/final-usage.d.ts +8 -17
  11. package/dist/bridge/final-usage.js +1 -18
  12. package/dist/bridge/index.d.ts +52 -38
  13. package/dist/bridge/index.js +255 -433
  14. package/dist/bridge/queue-manager.d.ts +25 -1
  15. package/dist/bridge/queue-manager.js +179 -160
  16. package/dist/bridge/run-approval.d.ts +16 -0
  17. package/dist/bridge/run-approval.js +41 -0
  18. package/dist/bridge/run-usage.d.ts +32 -0
  19. package/dist/bridge/run-usage.js +87 -0
  20. package/dist/bridge/session-writeback.d.ts +19 -0
  21. package/dist/bridge/session-writeback.js +36 -0
  22. package/dist/card/approval-render.d.ts +3 -2
  23. package/dist/card/approval-render.js +3 -2
  24. package/dist/card/bash-card-session.js +2 -5
  25. package/dist/{router → card}/card-helpers.d.ts +7 -7
  26. package/dist/{router → card}/card-helpers.js +8 -8
  27. package/dist/card/card-session.d.ts +6 -2
  28. package/dist/card/card-session.js +28 -28
  29. package/dist/card/card-shared.d.ts +4 -4
  30. package/dist/card/card-shared.js +4 -10
  31. package/dist/card/collapsible.js +3 -2
  32. package/dist/card/final-answer-overflow.d.ts +1 -0
  33. package/dist/card/final-answer-overflow.js +46 -0
  34. package/dist/card/run-card-session.d.ts +2 -2
  35. package/dist/card/run-card-session.js +4 -9
  36. package/dist/card/run-renderer.d.ts +14 -0
  37. package/dist/card/run-renderer.js +126 -23
  38. package/dist/card/run-state.d.ts +40 -51
  39. package/dist/card/run-state.js +96 -34
  40. package/dist/card/usage-format.d.ts +30 -0
  41. package/dist/{router → card}/usage-format.js +3 -10
  42. package/dist/clone.js +6 -1
  43. package/dist/config/codex-config.js +4 -4
  44. package/dist/config/index.d.ts +18 -20
  45. package/dist/config/index.js +6 -20
  46. package/dist/config/pi-config.js +6 -4
  47. package/dist/connector/index.d.ts +2 -4
  48. package/dist/connector/index.js +7 -11
  49. package/dist/{runner/dsh → dsh}/auth.js +3 -4
  50. package/dist/dsh/home.d.ts +2 -0
  51. package/dist/dsh/home.js +8 -0
  52. package/dist/dsh/protocol.d.ts +9 -0
  53. package/dist/dsh/protocol.js +61 -0
  54. package/dist/inbound/turn-assembler.js +6 -0
  55. package/dist/index.js +25 -414
  56. package/dist/message-handlers.d.ts +26 -0
  57. package/dist/message-handlers.js +275 -0
  58. package/dist/model-catalog.d.ts +14 -0
  59. package/dist/model-catalog.js +25 -0
  60. package/dist/platform/path.d.ts +9 -0
  61. package/dist/platform/path.js +18 -0
  62. package/dist/router/active-run-usage.d.ts +4 -0
  63. package/dist/router/active-run-usage.js +12 -0
  64. package/dist/router/card-action-payload.d.ts +1 -1
  65. package/dist/router/card-action-payload.js +1 -1
  66. package/dist/router/commands/ls.d.ts +9 -0
  67. package/dist/router/commands/ls.js +243 -0
  68. package/dist/router/commands/order.d.ts +12 -0
  69. package/dist/router/commands/order.js +178 -0
  70. package/dist/router/commands/resume.d.ts +48 -0
  71. package/dist/router/commands/resume.js +246 -0
  72. package/dist/router/commands/ws.d.ts +15 -0
  73. package/dist/router/commands/ws.js +189 -0
  74. package/dist/router/config/claude.js +5 -4
  75. package/dist/router/config/codex.js +2 -6
  76. package/dist/router/config/common/render.d.ts +5 -1
  77. package/dist/router/config/common/render.js +6 -13
  78. package/dist/router/config/common/turn-idle.d.ts +2 -0
  79. package/dist/router/config/common/turn-idle.js +9 -0
  80. package/dist/router/config/dsh.d.ts +1 -1
  81. package/dist/router/config/index.d.ts +1 -1
  82. package/dist/router/config/kimi.js +2 -6
  83. package/dist/router/config/opencode.js +2 -6
  84. package/dist/router/config/types.d.ts +11 -3
  85. package/dist/router/index.d.ts +55 -33
  86. package/dist/router/index.js +233 -1376
  87. package/dist/router/paging.d.ts +13 -0
  88. package/dist/router/paging.js +20 -0
  89. package/dist/router/path-validation.d.ts +27 -0
  90. package/dist/router/path-validation.js +45 -0
  91. package/dist/runner/agent-choices-common.d.ts +1 -1
  92. package/dist/runner/claude/runner.d.ts +23 -6
  93. package/dist/runner/claude/runner.js +6 -20
  94. package/dist/runner/claude/session.js +1 -1
  95. package/dist/runner/codex/app-server/runner.d.ts +3 -1
  96. package/dist/runner/codex/app-server/translator.d.ts +13 -0
  97. package/dist/runner/codex/app-server/translator.js +114 -151
  98. package/dist/runner/common/acp/base-acp-runner.d.ts +3 -1
  99. package/dist/runner/common/acp/base-translator.d.ts +3 -8
  100. package/dist/runner/common/acp/base-translator.js +16 -7
  101. package/dist/runner/common/connection-based-runner.d.ts +3 -1
  102. package/dist/runner/common/jsonrpc/transport.js +17 -7
  103. package/dist/runner/common/spawning-runner.js +10 -4
  104. package/dist/runner/common/turn-started.d.ts +2 -0
  105. package/dist/runner/common/turn-started.js +10 -0
  106. package/dist/runner/dsh/client.d.ts +2 -10
  107. package/dist/runner/dsh/client.js +13 -220
  108. package/dist/runner/dsh/daemon.d.ts +0 -2
  109. package/dist/runner/dsh/daemon.js +9 -7
  110. package/dist/runner/dsh/errors.d.ts +5 -0
  111. package/dist/runner/dsh/errors.js +11 -0
  112. package/dist/runner/dsh/exit-cancel.d.ts +0 -2
  113. package/dist/runner/dsh/exit-cancel.js +2 -2
  114. package/dist/runner/dsh/inflight.d.ts +0 -2
  115. package/dist/runner/dsh/inflight.js +1 -1
  116. package/dist/runner/dsh/mux-stream.d.ts +26 -0
  117. package/dist/runner/dsh/mux-stream.js +176 -0
  118. package/dist/runner/dsh/runner.d.ts +21 -1
  119. package/dist/runner/dsh/runner.js +76 -73
  120. package/dist/runner/dsh/translator.d.ts +1 -11
  121. package/dist/runner/dsh/translator.js +1 -1
  122. package/dist/runner/index.d.ts +1 -1
  123. package/dist/runner/kimi/acp/runner.d.ts +22 -1
  124. package/dist/runner/kimi/acp/runner.js +83 -60
  125. package/dist/runner/kimi/acp/translator.js +1 -12
  126. package/dist/runner/opencode/acp/error-monitor.d.ts +11 -0
  127. package/dist/runner/opencode/acp/error-monitor.js +31 -2
  128. package/dist/runner/opencode/acp/runner.d.ts +3 -1
  129. package/dist/runner/opencode/acp/translator.js +1 -12
  130. package/dist/runner/pi/rpc/runner.d.ts +2 -1
  131. package/dist/runner/pi/rpc/translator.d.ts +2 -8
  132. package/dist/runner/pi/rpc/translator.js +2 -7
  133. package/dist/runner/probe.d.ts +1 -1
  134. package/dist/runner/probe.js +10 -9
  135. package/dist/runner/registry.d.ts +2 -20
  136. package/dist/runner/registry.js +0 -32
  137. package/dist/runner/types.d.ts +37 -155
  138. package/dist/runner/types.js +4 -24
  139. package/dist/session/claude/session-index.d.ts +11 -4
  140. package/dist/session/claude/session-index.js +34 -10
  141. package/dist/session/claude/session-reader.d.ts +1 -1
  142. package/dist/session/claude/sessions.d.ts +2 -2
  143. package/dist/session/claude/sessions.js +41 -26
  144. package/dist/session/codex/rollout-reader.d.ts +1 -1
  145. package/dist/session/codex/rollout-reader.js +14 -13
  146. package/dist/session/codex/sessions.d.ts +1 -1
  147. package/dist/session/codex/sessions.js +4 -2
  148. package/dist/session/common/jsonl.js +7 -1
  149. package/dist/session/common/pagination.d.ts +2 -0
  150. package/dist/session/common/pagination.js +1 -1
  151. package/dist/session/common/recency.d.ts +6 -0
  152. package/dist/session/common/recency.js +7 -5
  153. package/dist/session/common/two-pass.d.ts +1 -1
  154. package/dist/session/common/usage-accumulator.d.ts +1 -1
  155. package/dist/session/dsh/sessions.d.ts +1 -1
  156. package/dist/session/dsh/sessions.js +37 -12
  157. package/dist/session/kimi/sessions.d.ts +1 -1
  158. package/dist/session/kimi/sessions.js +37 -10
  159. package/dist/session/opencode/sessions.d.ts +1 -1
  160. package/dist/session/opencode/sessions.js +53 -48
  161. package/dist/session/pi/sessions.d.ts +1 -1
  162. package/dist/session/pi/sessions.js +23 -9
  163. package/dist/session/registry.d.ts +2 -1
  164. package/dist/session/session-store.d.ts +1 -2
  165. package/dist/session/session-store.js +14 -7
  166. package/dist/session-contract.d.ts +178 -0
  167. package/dist/session-contract.js +9 -0
  168. package/package.json +1 -1
  169. package/dist/router/usage-format.d.ts +0 -60
  170. /package/dist/{runner/dsh → dsh}/auth.d.ts +0 -0
  171. /package/dist/{runner/dsh → dsh}/types.d.ts +0 -0
  172. /package/dist/{runner/dsh → dsh}/types.js +0 -0
@@ -6,6 +6,7 @@ import { isStale } from '../common/constants.js';
6
6
  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
+ import { getLogger } from '../../logger/index.js';
9
10
  import { UsageAccumulator, contextWindowOccupancy, cumulativeUsageFields, } from '../common/usage-accumulator.js';
10
11
  import { SessionIndex, extractText, isTaskNotificationText, CLAUDE_MAPPING, } from './session-index.js';
11
12
  /**
@@ -61,7 +62,10 @@ function findSessionFileInProjects(sessionId, projectsDir, cwd) {
61
62
  try {
62
63
  entries = fs.readdirSync(projectsDir);
63
64
  }
64
- catch {
65
+ catch (err) {
66
+ if (err.code !== 'ENOENT') {
67
+ getLogger().debug(`[claude-session-reader] cannot read projects dir ${projectsDir}: ${err?.message ?? String(err)}`);
68
+ }
65
69
  return undefined;
66
70
  }
67
71
  const name = `${sessionId}.jsonl`;
@@ -123,6 +127,13 @@ export function listClaudeSessions(cwd, opts = {}) {
123
127
  summary: e.summary,
124
128
  mtime: e.mtimeMs,
125
129
  }));
130
+ // 空列表 + 上次扫描是非 ENOENT 失败 → 是「读不到」而非「真的没有」。
131
+ // 与 opencode/kimi/pi 同口径:router 显示「列表失败」,别假装空态。
132
+ if (sessions.length === 0 && index.lastScanError !== undefined) {
133
+ const err = index.lastScanError;
134
+ const msg = err instanceof Error ? err.message : String(err);
135
+ throw new Error(`claude session list 读取失败: ${msg}`, { cause: err });
136
+ }
126
137
  return limit === undefined ? sessions : sessions.slice(0, limit);
127
138
  }
128
139
  /** Get the newest session for a given cwd. Returns undefined if none exist. */
@@ -145,7 +156,15 @@ export function getNewestSession(cwd, opts = {}) {
145
156
  *
146
157
  * Override `now` for tests.
147
158
  */
148
- function isSessionActive(filePath, mtimeMs, now = Date.now()) {
159
+ /** 末行是这些类型之一 = 会话已收尾(模块级:逐文件判定不必每次重建 Set)。 */
160
+ const CLAUDE_TERMINAL_TYPES = new Set(['result', 'last-prompt', 'mode', 'permission-mode']);
161
+ /**
162
+ * 文件侧的活跃判定:mtime 新鲜 + 末行没有终态标记。
163
+ *
164
+ * 刻意不叫 `isSessionActive`——那是 reader 契约上的方法名,语义各家自己定;
165
+ * 本函数只看这一个文件,两者同名不同义曾让人把「最近有写入」当成「turn 在跑」。
166
+ */
167
+ function hasRecentFileActivity(filePath, mtimeMs, now = Date.now()) {
149
168
  // Hard signal: file untouched for over an hour → can't be from a live process
150
169
  if (isStale(mtimeMs, now))
151
170
  return false;
@@ -158,8 +177,7 @@ function isSessionActive(filePath, mtimeMs, now = Date.now()) {
158
177
  // `last-prompt` / `mode` (no `result` event) to mark turn end, so a
159
178
  // 24-minute-old session with a last-prompt tail is already done
160
179
  // (regression 2026-06-21: previously falsely reported as active).
161
- const TERMINAL_TYPES = new Set(['result', 'last-prompt', 'mode', 'permission-mode']);
162
- if (typeof obj.type === 'string' && TERMINAL_TYPES.has(obj.type))
180
+ if (typeof obj.type === 'string' && CLAUDE_TERMINAL_TYPES.has(obj.type))
163
181
  return false;
164
182
  }
165
183
  catch {
@@ -167,9 +185,18 @@ function isSessionActive(filePath, mtimeMs, now = Date.now()) {
167
185
  }
168
186
  return true;
169
187
  }
188
+ /**
189
+ * stat 失败:ENOENT = 文件在两次查询之间消失(正常竞态);其余 errno 必须留痕,
190
+ * 否则「文件不可读」会被当成「会话不活跃」。
191
+ */
192
+ function logStatFailure(filePath, err) {
193
+ if (err.code === 'ENOENT')
194
+ return;
195
+ getLogger().debug(`[claude-session-reader] stat ${filePath} failed: ${err?.message ?? String(err)}`);
196
+ }
170
197
  /**
171
198
  * Check if a Claude session (by sessionId + cwd) is still active.
172
- * Public wrapper around the internal `isSessionActive(filePath, mtimeMs, now)`
199
+ * Public wrapper around the internal `hasRecentFileActivity(filePath, mtimeMs, now)`
173
200
  * for use by `ClaudeSessionReader` and the multi-agent adapter layer.
174
201
  *
175
202
  * Resolves the jsonl path via `projectDirForCwd`, stats it for mtime, then
@@ -187,9 +214,10 @@ export function isClaudeSessionActive(sessionId, cwd, opts = {}) {
187
214
  if (entry) {
188
215
  try {
189
216
  const st = fs.statSync(entry.path);
190
- return isSessionActive(entry.path, st.mtimeMs);
217
+ return hasRecentFileActivity(entry.path, st.mtimeMs);
191
218
  }
192
- catch {
219
+ catch (err) {
220
+ logStatFailure(entry.path, err);
193
221
  return false;
194
222
  }
195
223
  }
@@ -210,9 +238,10 @@ export function isClaudeSessionActive(sessionId, cwd, opts = {}) {
210
238
  return false;
211
239
  try {
212
240
  const st = fs.statSync(filePath);
213
- return isSessionActive(filePath, st.mtimeMs);
241
+ return hasRecentFileActivity(filePath, st.mtimeMs);
214
242
  }
215
- catch {
243
+ catch (err) {
244
+ logStatFailure(filePath, err);
216
245
  return false;
217
246
  }
218
247
  }
@@ -411,23 +440,9 @@ export function readSessionContent(sessionId, cwd, opts = {}) {
411
440
  return { events: [] };
412
441
  }
413
442
  }
414
- // First pass STREAMS the file once via
415
- // `scanJsonlLines` (no `string[]` materialized), parsing each line once to
416
- // collect scalars (usage, tailOffset, aiTitle, recap, lastUserMessage)
417
- // while retaining NO parsed objects and NO whole-file line array. The
418
- // tailOffset is the byte offset where the tail begins (start of the line
419
- // after the last user message). A second pass re-reads ONLY the tail from
420
- // that offset via `readJsonlLinesFromOffset` and re-parses it for events —
421
- // O(tail) memory for both raw line strings and parsed objects, instead of
422
- // O(whole file).
423
- //
424
- // Parse ratio: ≈1.0–1.5× line count when a user message exists (streaming
425
- // full-file scan + tail re-parse of the usually-short post-user lines).
426
- // When the session has NO user message, tailOffset stays -1 and the tail
427
- // IS the whole file → scan(N) + tail(N) = 2.0× — the known asymptotic
428
- // upper bound of the body's two-phase design (still better than pre-'s
429
- // ~3×). Total parse count adds the index-miss fallback guard's fixed
430
- // early-stop overhead (~2 parses, independent of line count).
443
+ // Two-pass 扫描(pass 1 流式收标量 + pass 2 只重读 tail)的口径与解析比
444
+ // 见 `src/session/common/two-pass.ts` 头注释;claude 侧只多一项:索引 miss
445
+ // 时的 fallback 守卫另有固定 ~2 次早停解析,与行数无关。
431
446
  const scan = scalarScan(filePath);
432
447
  // 有 usage 即返回:非累计字段已是末轮 scope,命中判定用 session 累计兜底
433
448
  // (末轮可能 input/output 为 0 而仍有 cacheRead,累计口径不会被末轮遮挡)。
@@ -23,7 +23,7 @@
23
23
  * Note: This handles the **rollout file format** (historical logging), NOT the
24
24
  * `codex exec --json` stdout format(已随 exec 模式移除)。
25
25
  */
26
- import type { AgentSessionContentEvent, SessionContent, SessionSummary, AgentSessionUsage } from '../../runner/index.js';
26
+ import type { AgentSessionContentEvent, AgentSessionUsage, SessionContent, SessionSummary } from '../../session-contract.js';
27
27
  interface CodexRolloutEntry {
28
28
  threadId: string;
29
29
  cwd: string;
@@ -32,6 +32,7 @@ import { paginate, capEvents } from '../common/pagination.js';
32
32
  import { sortByRecencyDesc } from '../common/recency.js';
33
33
  import { getLogger } from '../../logger/index.js';
34
34
  import { isRecord, stringValue } from '../../common/guards.js';
35
+ import { TtlCache } from '../../common/ttl-cache.js';
35
36
  import { resolveCodexHome } from '../../config/codex-config.js';
36
37
  /** Field-wise difference (total - previous) for cumulative->incremental. */
37
38
  function subtractRawUsage(a, b) {
@@ -623,7 +624,12 @@ function safeIsDir(p) {
623
624
  }
624
625
  }
625
626
  const INDEX_TTL_MS = 5_000; // 5 seconds — covers a single /resume flow
626
- const sessionIndexCache = new Map();
627
+ /**
628
+ * codexHome → 会话索引。共享 `TtlCache`(TTL 由写入时间判定 + LRU 上限),
629
+ * 与 kimi/pi/opencode 三个 reader 同一套语义。生产侧 `CodexSessionReader`
630
+ * 构造时就把 codexHome 解析定死,实际只会用到 1 个键;上限是防漏的界。
631
+ */
632
+ const sessionIndexCache = new TtlCache(INDEX_TTL_MS, 4);
627
633
  /**
628
634
  * Build or return cached session index for a given resolved codexHome.
629
635
  * The index maps sessionId → {filePath, cwd, mtimeMs}.
@@ -635,19 +641,14 @@ const sessionIndexCache = new Map();
635
641
  * AFTER the cache was built — but still within the
636
642
  * 5s TTL — is found without waiting for expiry.
637
643
  *
638
- * Cache shape note: `sessionIndexCache` is module-level and keyed by the
639
- * resolved codexHome string. In production `CodexSessionReader` resolves
640
- * codexHome once in its constructor and reuses it for every call, so a single
641
- * bridge instance produces exactly one cache entry — the Map does NOT grow
642
- * unbounded. The TTL refreshes the value on each rebuild; entries are never
643
- * evicted, but there is effectively just one.
644
+ * Cache shape note: the cache is module-level and keyed by the resolved
645
+ * codexHome string; `TtlCache` caps entries (LRU) so a caller that injects
646
+ * many distinct codexHomes cannot grow memory without bound.
644
647
  */
645
648
  function getSessionIndex(resolvedCodexHome, forceRefresh = false) {
646
- const cacheKey = resolvedCodexHome;
647
- const cached = sessionIndexCache.get(cacheKey);
648
- if (!forceRefresh && cached && Date.now() - cached.builtAt < INDEX_TTL_MS) {
649
- return cached.index;
650
- }
649
+ const cached = forceRefresh ? undefined : sessionIndexCache.get(resolvedCodexHome);
650
+ if (cached)
651
+ return cached;
651
652
  const sessionsDir = path.join(resolvedCodexHome, 'sessions');
652
653
  const files = walkRolloutFiles(sessionsDir);
653
654
  const index = new Map();
@@ -704,7 +705,7 @@ function getSessionIndex(resolvedCodexHome, forceRefresh = false) {
704
705
  continue;
705
706
  }
706
707
  }
707
- sessionIndexCache.set(cacheKey, { index, builtAt: Date.now() });
708
+ sessionIndexCache.set(resolvedCodexHome, index);
708
709
  return index;
709
710
  }
710
711
  /** Clear the session index cache. Exposed for tests. */
@@ -1,4 +1,4 @@
1
- import type { AgentSession, SessionContent, SessionSummary, AgentSessionReader } from '../../runner/index.js';
1
+ import type { AgentSession, AgentSessionReader, SessionContent, SessionSummary } from '../../session-contract.js';
2
2
  interface CodexSessionReaderOptions {
3
3
  codexHome?: string;
4
4
  }
@@ -1,4 +1,6 @@
1
1
  import { resolveCodexHome } from '../../config/codex-config.js';
2
+ import { DEFAULT_LIST_LIMIT } from '../common/pagination.js';
3
+ import { newestOf } from '../common/recency.js';
2
4
  import { listCodexRollouts, readCodexSessionContent, readCodexSessionSummary, isCodexSessionActive, } from './rollout-reader.js';
3
5
  export class CodexSessionReader {
4
6
  codexHome;
@@ -9,7 +11,7 @@ export class CodexSessionReader {
9
11
  const result = listCodexRollouts({
10
12
  codexHome: this.codexHome,
11
13
  cwd,
12
- limit: opts?.limit ?? 20,
14
+ limit: opts?.limit ?? DEFAULT_LIST_LIMIT,
13
15
  offset: opts?.offset,
14
16
  });
15
17
  return {
@@ -25,7 +27,7 @@ export class CodexSessionReader {
25
27
  };
26
28
  }
27
29
  getNewestSession(cwd) {
28
- return this.listSessions(cwd, { limit: 1 }).sessions[0] ?? null;
30
+ return newestOf(this, cwd);
29
31
  }
30
32
  readSessionContent(sessionId, cwd, opts) {
31
33
  return readCodexSessionContent(sessionId, {
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs';
2
+ import { getLogger } from '../../logger/index.js';
2
3
  /**
3
4
  * Stream a JSONL file line-by-line using chunked reads.
4
5
  *
@@ -55,7 +56,12 @@ function forEachJsonlLineCore(filePath, opts) {
55
56
  try {
56
57
  fd = fs.openSync(filePath, 'r');
57
58
  }
58
- catch {
59
+ catch (err) {
60
+ // ENOENT 是正常空态(陈旧 cwd / 会话已被清理);其余(EACCES/EBUSY…)
61
+ // 必须留痕:否则「文件打不开」在 /resume 上与「没有会话」不可区分。
62
+ if (err.code !== 'ENOENT') {
63
+ getLogger().warn(`[jsonl] cannot open ${filePath}: ${err?.message ?? String(err)}`);
64
+ }
59
65
  return false;
60
66
  }
61
67
  const CHUNK_SIZE = 65536;
@@ -7,6 +7,8 @@
7
7
  * drifted on the default limit). Centralized here so the contract is defined
8
8
  * once: default page limit is 20, matching the majority of readers.
9
9
  */
10
+ /** Default page size for listSessions. */
11
+ export declare const DEFAULT_LIST_LIMIT = 20;
10
12
  /**
11
13
  * Paginate a (already-ordered) full item list by offset + limit.
12
14
  * Clamps negative offset to 0; caps limit to the remaining items.
@@ -8,7 +8,7 @@
8
8
  * once: default page limit is 20, matching the majority of readers.
9
9
  */
10
10
  /** Default page size for listSessions. */
11
- const DEFAULT_LIST_LIMIT = 20;
11
+ export const DEFAULT_LIST_LIMIT = 20;
12
12
  /**
13
13
  * Paginate a (already-ordered) full item list by offset + limit.
14
14
  * Clamps negative offset to 0; caps limit to the remaining items.
@@ -3,5 +3,11 @@
3
3
  * with a deterministic id tie-break so index rebuilds / walk-order changes
4
4
  * never reorder a page (codex red line: never break out of a partial order).
5
5
  */
6
+ import type { AgentSessionReader } from '../../session-contract.js';
6
7
  /** 「时间降序 + id 字典序 tie-break」原地排序(原先 6 处内联拷贝)。 */
7
8
  export declare function sortByRecencyDesc<T>(items: T[], time: (item: T) => number, id: (item: T) => string): void;
9
+ /**
10
+ * 列表首项 = 最新会话(`getNewestSession` 的唯一实现,此前 5 个 reader 逐字各抄一遍)。
11
+ * 「newest first」是 `AgentSessionReader.listSessions` 的契约,这里只取第一页第一条。
12
+ */
13
+ export declare function newestOf(reader: Pick<AgentSessionReader, 'listSessions'>, cwd: string): import("../../session-contract.js").AgentSession;
@@ -1,10 +1,12 @@
1
- /**
2
- * Session-list ordering invariant shared by every reader: newest first,
3
- * with a deterministic id tie-break so index rebuilds / walk-order changes
4
- * never reorder a page (codex red line: never break out of a partial order).
5
- */
6
1
  /** 「时间降序 + id 字典序 tie-break」原地排序(原先 6 处内联拷贝)。 */
7
2
  export function sortByRecencyDesc(items, time, id) {
8
3
  items.sort((a, b) => time(b) - time(a) || id(a).localeCompare(id(b)));
9
4
  }
5
+ /**
6
+ * 列表首项 = 最新会话(`getNewestSession` 的唯一实现,此前 5 个 reader 逐字各抄一遍)。
7
+ * 「newest first」是 `AgentSessionReader.listSessions` 的契约,这里只取第一页第一条。
8
+ */
9
+ export function newestOf(reader, cwd) {
10
+ return reader.listSessions(cwd, { limit: 1 }).sessions[0] ?? null;
11
+ }
10
12
  //# sourceMappingURL=recency.js.map
@@ -1,4 +1,4 @@
1
- import type { AgentSessionContentEvent } from '../../runner/types.js';
1
+ import type { AgentSessionContentEvent } from '../../session-contract.js';
2
2
  /**
3
3
  * Byte offset where the tail begins: the start of the line AFTER `line`.
4
4
  * `offset` is the byte offset of `line` itself; +1 accounts for the trailing
@@ -77,5 +77,5 @@ export declare function cumulativeUsageFields(t: {
77
77
  output: number;
78
78
  cacheRead: number;
79
79
  cacheCreation: number;
80
- }): Pick<import('../../runner/types.js').AgentSessionUsage, 'cumulativeTotalTokens' | 'cumulativeInputTokens' | 'cumulativeOutputTokens' | 'cumulativeCacheReadTokens' | 'cumulativeCacheCreationTokens'>;
80
+ }): Pick<import('../../session-contract.js').AgentSessionUsage, 'cumulativeTotalTokens' | 'cumulativeInputTokens' | 'cumulativeOutputTokens' | 'cumulativeCacheReadTokens' | 'cumulativeCacheCreationTokens'>;
81
81
  export {};
@@ -13,7 +13,7 @@
13
13
  * session-wide usage accumulation (assistant/message.usage);本 run 的 flow
14
14
  * 字段 = 末个 turn(turn/start–turn/end)内所有采样之和,与 codex 同口径。
15
15
  */
16
- import type { AgentSession, AgentSessionReader, SessionContent } from '../../runner/index.js';
16
+ import type { AgentSession, AgentSessionReader, SessionContent } from '../../session-contract.js';
17
17
  interface DshSessionReaderOptions {
18
18
  host?: string;
19
19
  /** Configured DSH home; selects `.credentials.yaml` when credentialsFile is unset. */
@@ -15,13 +15,13 @@
15
15
  */
16
16
  import { spawnProcessSync } from '../../platform/spawn.js';
17
17
  import { cumulativeUsageFields } from '../common/usage-accumulator.js';
18
- import { sortByRecencyDesc } from '../common/recency.js';
18
+ import { newestOf, sortByRecencyDesc } from '../common/recency.js';
19
19
  import { getLogger } from '../../logger/index.js';
20
20
  import { DEFAULT_DSH_HOST } from '../../config/index.js';
21
21
  import { samePath } from '../../platform/path.js';
22
22
  import { isWin32 } from '../../platform/select.js';
23
- import { dshArgs, extractDshToken } from '../../runner/dsh/client.js';
24
- import { DSH_COOKIE_LIFETIME_MS, isDshCredentialsNotFound, mintDshBrowserCookie, readDshBrowserSecretSync, resolveDshCredentialsFile, } from '../../runner/dsh/auth.js';
23
+ import { dshArgs, extractDshToken } from '../../dsh/protocol.js';
24
+ import { DSH_COOKIE_LIFETIME_MS, isDshCredentialsNotFound, mintDshBrowserCookie, readDshBrowserSecretSync, resolveDshCredentialsFile, } from '../../dsh/auth.js';
25
25
  import { capEvents, paginate } from '../common/pagination.js';
26
26
  import { truncateUtf8, TOOL_RESULT_MAX_BYTES } from '../../common/truncate.js';
27
27
  import { summarizeToolInput } from '../../common/tool-summary.js';
@@ -50,13 +50,23 @@ function defaultSyncRequest(baseUrl, method, payload, cookie) {
50
50
  ...(cookie ? ['-H', `Cookie: ${cookie}`] : []),
51
51
  '--data',
52
52
  body,
53
+ // 末尾追加 HTTP 状态码:401/403 要与「偶发失败」区分(cookie 失效才值得重试)。
54
+ '--write-out',
55
+ '\n%{http_code}',
53
56
  ], { encoding: 'utf-8', timeout: 15_000, maxBuffer: 64 * 1024 * 1024 });
54
57
  if (res.error || res.status !== 0) {
55
58
  throw new Error(`DSH ${method} failed: ${(res.error?.message ?? res.stderr) || 'unknown'}`);
56
59
  }
60
+ const raw = String(res.stdout);
61
+ const statusMatch = /\n(\d{3})$/.exec(raw);
62
+ const httpStatus = statusMatch ? Number(statusMatch[1]) : undefined;
63
+ const bodyText = statusMatch ? raw.slice(0, statusMatch.index) : raw;
64
+ if (httpStatus === 401 || httpStatus === 403) {
65
+ throw new Error(`DSH ${method} unauthorized (HTTP ${httpStatus})`);
66
+ }
57
67
  let parsed;
58
68
  try {
59
- parsed = JSON.parse(String(res.stdout));
69
+ parsed = JSON.parse(bodyText);
60
70
  }
61
71
  catch (err) {
62
72
  throw new Error(`DSH ${method} returned non-JSON: ${err.message}`, {
@@ -82,6 +92,11 @@ function defaultSyncConnect(baseUrl, token) {
82
92
  throw new Error('DSH auth failed: index handshake did not return a cookie');
83
93
  return cookie;
84
94
  }
95
+ /** Cookie 失效的判定:HTTP 401/403 或服务端回报 unauthorized/forbidden。 */
96
+ function isAuthFailure(err) {
97
+ const msg = err instanceof Error ? err.message : String(err);
98
+ return /\b401\b|\b403\b|unauthori[sz]ed|forbidden|invalid[ -]?cookie/i.test(msg);
99
+ }
85
100
  export class DshSessionReader {
86
101
  hostProvider;
87
102
  dshHomeProvider;
@@ -121,11 +136,23 @@ export class DshSessionReader {
121
136
  try {
122
137
  return this.syncRequest(baseUrl, method, payload, this.resolveCookie());
123
138
  }
124
- catch {
125
- // DSH may have restarted while lark-remote cached a token-derived cookie.
126
- // Re-read the credentials file and retry once with a fresh cookie.
139
+ catch (first) {
140
+ // 只有 cookie 失效值得清缓存重试。404/500/timeout 重试会让 /resume 延迟
141
+ // 翻倍,且第二次错误会盖掉真正的根因。
142
+ if (!isAuthFailure(first))
143
+ throw first;
144
+ getLogger().warn(`[dsh-session-reader] ${method} auth failed, refreshing cookie and retrying: ${first?.message ?? String(first)}`);
127
145
  this.cookieCache.clear();
128
- return this.syncRequest(baseUrl, method, payload, this.resolveCookie());
146
+ try {
147
+ return this.syncRequest(baseUrl, method, payload, this.resolveCookie());
148
+ }
149
+ catch (second) {
150
+ // 重试仍失败:首错(cookie 失效)才是根因。`cause` 必须是本次捕获的 err
151
+ // (lint preserve-caught-error),所以根因写进 message 一并留痕。
152
+ const firstMsg = first?.message ?? String(first);
153
+ const secondMsg = second?.message ?? String(second);
154
+ throw new Error(`DSH ${method} failed after cookie refresh: ${secondMsg} (root cause: ${firstMsg})`, { cause: second });
155
+ }
129
156
  }
130
157
  }
131
158
  /** Mint from the durable secret, or bootstrap and cache a token cookie. */
@@ -186,7 +213,7 @@ export class DshSessionReader {
186
213
  }
187
214
  }
188
215
  getNewestSession(cwd) {
189
- return this.listSessions(cwd, { limit: 1 }).sessions[0] ?? null;
216
+ return newestOf(this, cwd);
190
217
  }
191
218
  readSessionContent(sessionId, cwd, opts) {
192
219
  try {
@@ -354,9 +381,7 @@ export class DshSessionReader {
354
381
  const projection = this.unary('session/projections', { sessionId });
355
382
  if (typeof projection?.asOfSeq === 'number')
356
383
  return projection.asOfSeq;
357
- const row = known ??
358
- this.unary('session/list', {}).items?.find((item) => String(item.sessionId) === sessionId);
359
- return typeof row?.projections?.asOfSeq === 'number' ? row.projections.asOfSeq : -1;
384
+ return typeof known.projections?.asOfSeq === 'number' ? known.projections.asOfSeq : -1;
360
385
  }
361
386
  }
362
387
  /** Preset moved from the summary top level to projections in DSH 0.2.x. */
@@ -1,4 +1,4 @@
1
- import type { AgentSession, AgentSessionReader, SessionContent } from '../../runner/index.js';
1
+ import type { AgentSession, AgentSessionReader, SessionContent } from '../../session-contract.js';
2
2
  export interface KimiSessionState {
3
3
  version?: number;
4
4
  createdAt: string | number;
@@ -5,9 +5,9 @@ import { readJsonlLines as readJsonlLinesShared, readLastNJsonlLines } from '../
5
5
  import { UsageAccumulator, cumulativeUsageFields } from '../common/usage-accumulator.js';
6
6
  import { getLogger } from '../../logger/index.js';
7
7
  import { isStale } from '../common/constants.js';
8
- import { samePath } from '../../platform/path.js';
8
+ import { safeRealpath, samePath } from '../../platform/path.js';
9
9
  import { capEvents, paginate } from '../common/pagination.js';
10
- import { sortByRecencyDesc } from '../common/recency.js';
10
+ import { newestOf, sortByRecencyDesc } from '../common/recency.js';
11
11
  import { TtlCache } from '../../common/ttl-cache.js';
12
12
  import { truncateUtf8, TOOL_RESULT_MAX_BYTES } from '../../common/truncate.js';
13
13
  import { truncateToolInput } from '../common/content-blocks.js';
@@ -88,7 +88,12 @@ function readJsonlLines(filePath) {
88
88
  jsonlCache.set(filePath, { mtime, lines });
89
89
  return lines;
90
90
  }
91
- catch {
91
+ catch (err) {
92
+ // ENOENT = 会话文件在扫描间隙被清理(正常);其余 errno 留痕,否则该会话
93
+ // 静默从 /resume 消失。
94
+ if (err.code !== 'ENOENT') {
95
+ getLogger().warn(`[kimi-session-reader] cannot read ${filePath}: ${err?.message ?? String(err)}`);
96
+ }
92
97
  return [];
93
98
  }
94
99
  }
@@ -201,7 +206,13 @@ export class KimiSessionReader {
201
206
  */
202
207
  listSessions(cwd, opts) {
203
208
  try {
204
- const realCwd = fs.realpathSync(cwd);
209
+ // 归一失败(陈旧/无权限 cwd)回退原样输入,让逐条 samePath 比较去决定
210
+ // 空态——一次 realpathSync 抛错不得把整张列表清掉(与 opencode 同口径)。
211
+ const realCwd = safeRealpath(cwd, (err) => {
212
+ if (err.code === 'ENOENT')
213
+ return;
214
+ getLogger().debug(`[kimi-session-reader] realpath ${cwd} failed, using the path as given: ${err?.message ?? String(err)}`);
215
+ });
205
216
  const sessions = [];
206
217
  for (const entry of this.scanSessionIndex()) {
207
218
  // state.json 一次读取共用(cwd 兜底 + summary),I/O 减半。
@@ -215,8 +226,8 @@ export class KimiSessionReader {
215
226
  state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
216
227
  stateParsed = true;
217
228
  }
218
- catch {
219
- // Corrupt state.json — cannot determine cwd → skip below
229
+ catch (err) {
230
+ getLogger().debug(`[kimi-session-reader] corrupt state.json skipped session=${entry.sessionId}: ${err?.message ?? String(err)}`);
220
231
  }
221
232
  }
222
233
  // Index workDir is v1-only; v2 sessions may have empty/missing workDir.
@@ -258,7 +269,16 @@ export class KimiSessionReader {
258
269
  mtimeMs = fs.statSync(wirePath).mtimeMs;
259
270
  }
260
271
  catch {
261
- mtimeMs = fs.statSync(entry.sessionDir).mtimeMs;
272
+ try {
273
+ mtimeMs = fs.statSync(entry.sessionDir).mtimeMs;
274
+ }
275
+ catch (err) {
276
+ if (err.code !== 'ENOENT') {
277
+ getLogger().warn(`[kimi-session-reader] cannot stat ${entry.sessionDir}: ${err?.message ?? String(err)}`);
278
+ }
279
+ // 会话目录已不存在(或被清理):跳过该条,别让它拖垮整张列表。
280
+ continue;
281
+ }
262
282
  }
263
283
  sessions.push({
264
284
  sessionId: entry.sessionId,
@@ -276,15 +296,22 @@ export class KimiSessionReader {
276
296
  };
277
297
  }
278
298
  catch (error) {
279
- getLogger().error(`[kimi-session-reader] listSessions error: ${error}`);
280
- return { sessions: [], total: 0 };
299
+ // ENOENT(cwd 已删除):陈旧 cwd 保持优雅空态。
300
+ // 其余失败:error 日志 + 上抛,router 显示「列表失败」——静默 [] 会让
301
+ // 用户误以为没有 session,与真空不可区分(与 opencode 同口径)。
302
+ if (error.code === 'ENOENT') {
303
+ return { sessions: [], total: 0 };
304
+ }
305
+ getLogger().error(`[kimi-session-reader] listSessions failed: ${error}`);
306
+ const msg = error instanceof Error ? error.message : String(error);
307
+ throw new Error(`kimi session list 读取失败: ${msg}`, { cause: error });
281
308
  }
282
309
  }
283
310
  /**
284
311
  * Get the newest session for a given working directory
285
312
  */
286
313
  getNewestSession(cwd) {
287
- return this.listSessions(cwd, { limit: 1 }).sessions[0] ?? null;
314
+ return newestOf(this, cwd);
288
315
  }
289
316
  /**
290
317
  * Read session content (events + usage)
@@ -15,7 +15,7 @@
15
15
  * - isSessionActive: use `updated` timestamp with 1 hour stale window.
16
16
  * - Simple memory cache with TTL.
17
17
  */
18
- import type { AgentSession, AgentSessionReader, SessionContent } from '../../runner/index.js';
18
+ import type { AgentSession, AgentSessionReader, SessionContent } from '../../session-contract.js';
19
19
  interface OpencodeSessionReaderOptions {
20
20
  /** Path to the opencode binary. Defaults to `opencode`. */
21
21
  binary?: string;