lark-remote 0.5.6 → 0.5.8

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 (85) hide show
  1. package/CHANGELOG.md +46 -6
  2. package/README.en.md +2 -2
  3. package/README.md +2 -2
  4. package/dist/bridge/index.d.ts +63 -1
  5. package/dist/bridge/index.js +147 -14
  6. package/dist/bridge/queue-manager.js +41 -58
  7. package/dist/card/approval-render.js +2 -1
  8. package/dist/card/bash-card-session.d.ts +0 -1
  9. package/dist/card/bash-card-session.js +0 -8
  10. package/dist/card/bash-renderer.js +2 -1
  11. package/dist/card/card-budget.d.ts +37 -0
  12. package/dist/card/card-budget.js +143 -24
  13. package/dist/card/card-session.d.ts +3 -13
  14. package/dist/card/card-session.js +4 -18
  15. package/dist/card/card-shared.d.ts +11 -0
  16. package/dist/card/card-shared.js +19 -13
  17. package/dist/card/run-card-session.d.ts +0 -1
  18. package/dist/card/run-card-session.js +0 -6
  19. package/dist/card/run-renderer.d.ts +18 -0
  20. package/dist/card/run-renderer.js +330 -62
  21. package/dist/card/run-state.d.ts +26 -0
  22. package/dist/card/run-state.js +45 -0
  23. package/dist/card/text-truncate.d.ts +3 -5
  24. package/dist/card/text-truncate.js +3 -5
  25. package/dist/card/time.d.ts +14 -0
  26. package/dist/card/time.js +30 -0
  27. package/dist/card/tool-render.d.ts +15 -2
  28. package/dist/card/tool-render.js +42 -82
  29. package/dist/common/text-width.d.ts +24 -0
  30. package/dist/common/text-width.js +93 -0
  31. package/dist/common/tool-summary.d.ts +19 -0
  32. package/dist/common/tool-summary.js +171 -0
  33. package/dist/common/turn-failure.d.ts +13 -0
  34. package/dist/common/turn-failure.js +18 -0
  35. package/dist/config/index.d.ts +1 -1
  36. package/dist/config/index.js +1 -1
  37. package/dist/connector/index.js +1 -1
  38. package/dist/index.js +66 -13
  39. package/dist/order/index.d.ts +2 -0
  40. package/dist/order/index.js +2 -0
  41. package/dist/platform/command.js +1 -1
  42. package/dist/platform/shell.d.ts +2 -2
  43. package/dist/platform/spawn.d.ts +1 -1
  44. package/dist/platform/spawn.js +2 -2
  45. package/dist/platform/types.d.ts +1 -1
  46. package/dist/router/card-helpers.d.ts +18 -16
  47. package/dist/router/card-helpers.js +88 -46
  48. package/dist/router/config/codex.js +2 -3
  49. package/dist/router/config/common/model-patch.d.ts +8 -0
  50. package/dist/router/config/common/model-patch.js +13 -0
  51. package/dist/router/config/common/render.js +30 -77
  52. package/dist/router/config/dsh.js +2 -1
  53. package/dist/router/config/kimi.js +3 -3
  54. package/dist/router/index.d.ts +9 -11
  55. package/dist/router/index.js +165 -356
  56. package/dist/router/order-exec-dispatch.js +2 -1
  57. package/dist/runner/claude/session.d.ts +1 -1
  58. package/dist/runner/claude/session.js +3 -3
  59. package/dist/runner/common/defaults.d.ts +2 -0
  60. package/dist/runner/common/defaults.js +3 -0
  61. package/dist/runner/common/jsonrpc/connection-manager.js +1 -1
  62. package/dist/runner/common/jsonrpc/transport.d.ts +1 -1
  63. package/dist/runner/common/jsonrpc/transport.js +3 -3
  64. package/dist/runner/common/spawning-runner.d.ts +1 -1
  65. package/dist/runner/common/spawning-runner.js +2 -2
  66. package/dist/runner/dsh/auth.js +2 -1
  67. package/dist/runner/dsh/client.d.ts +100 -3
  68. package/dist/runner/dsh/client.js +290 -6
  69. package/dist/runner/dsh/exit-cancel.d.ts +32 -0
  70. package/dist/runner/dsh/exit-cancel.js +72 -0
  71. package/dist/runner/dsh/inflight.d.ts +62 -0
  72. package/dist/runner/dsh/inflight.js +140 -0
  73. package/dist/runner/dsh/runner.d.ts +90 -4
  74. package/dist/runner/dsh/runner.js +436 -18
  75. package/dist/runner/dsh/translator.d.ts +1 -1
  76. package/dist/runner/dsh/translator.js +103 -1
  77. package/dist/runner/dsh/types.d.ts +60 -0
  78. package/dist/runner/types.d.ts +95 -2
  79. package/dist/runner/types.js +9 -0
  80. package/dist/session/claude/session-index.d.ts +0 -4
  81. package/dist/session/claude/session-index.js +0 -8
  82. package/dist/session/claude/sessions.js +2 -2
  83. package/dist/session/dsh/sessions.js +157 -17
  84. package/dist/update/install.js +1 -1
  85. package/package.json +1 -1
@@ -12,7 +12,7 @@
12
12
  * completed → success; aborted/interrupted → interrupted;
13
13
  * blocked/error/max-tokens → error (errorMessage carries the reason)
14
14
  *
15
- * Token accounting (ccusage 口径, AGENTS.md §9.21): DSH emits ONE usage per model
15
+ * Token accounting (ccusage 口径): DSH emits ONE usage per model
16
16
  * call (per step) — a tool-looping turn has several `assistant/message` events.
17
17
  * The result event therefore carries **the sum of the turn's samplings**, not the
18
18
  * last one (codex `token_count` has the same shape). `lastUsage` is kept only as
@@ -118,6 +118,48 @@ export class DshTranslator {
118
118
  }
119
119
  return [];
120
120
  }
121
+ case 'tool/call': {
122
+ // DSH 把「模型发起调用」落成独立事件(name/arguments 是模型原始输出)。
123
+ // 这是卡片工具行的唯一来源:assistant/message 里只有完成后的正文块,
124
+ // 不含调用轨迹。
125
+ const call = ev.data;
126
+ const id = typeof call.callId === 'string' ? call.callId : '';
127
+ const name = typeof call.name === 'string' ? call.name : '';
128
+ if (!id || !name)
129
+ return [];
130
+ return [
131
+ {
132
+ type: 'assistant',
133
+ message: {
134
+ content: [{ type: 'tool_use', id, name, input: parseToolArguments(call.arguments) }],
135
+ },
136
+ timestamp: new Date(ev.time).toISOString(),
137
+ },
138
+ ];
139
+ }
140
+ case 'tool/result': {
141
+ const data = ev.data;
142
+ const msg = data.message;
143
+ const toolCallId = typeof msg?.toolCallId === 'string' ? msg.toolCallId : '';
144
+ if (!toolCallId)
145
+ return [];
146
+ return [
147
+ {
148
+ type: 'user',
149
+ message: {
150
+ content: [
151
+ {
152
+ type: 'tool_result',
153
+ tool_use_id: toolCallId,
154
+ content: toolResultText(msg?.content, data.error),
155
+ is_error: msg?.isError === true,
156
+ },
157
+ ],
158
+ },
159
+ timestamp: new Date(ev.time).toISOString(),
160
+ },
161
+ ];
162
+ }
121
163
  case 'turn/end': {
122
164
  const turnReason = ev.data.reason;
123
165
  const reason = turnReason?.kind;
@@ -153,6 +195,66 @@ export class DshTranslator {
153
195
  }
154
196
  }
155
197
  }
198
+ /**
199
+ * Parse a `tool/call.arguments` payload (a JSON string on the wire) into the
200
+ * object shape the card renderer consumes. Non-JSON arguments (a truncated or
201
+ * provider-mangled payload) are kept verbatim so the header can still show
202
+ * something instead of silently rendering an empty summary.
203
+ */
204
+ function parseToolArguments(raw) {
205
+ if (typeof raw !== 'string')
206
+ return raw ?? {};
207
+ const trimmed = raw.trim();
208
+ if (!trimmed)
209
+ return {};
210
+ try {
211
+ return JSON.parse(trimmed);
212
+ }
213
+ catch {
214
+ return trimmed;
215
+ }
216
+ }
217
+ /**
218
+ * Flatten a `tool/result.message.content` block list into display text.
219
+ *
220
+ * DSH content is a block array (`{type:'text',text}` / `{type:'image',...}`),
221
+ * the same shape the LLM sees. Images collapse to a marker: the card cannot
222
+ * render them and the raw base64 would blow the byte budget.
223
+ */
224
+ function toolResultText(content, error) {
225
+ const err = error;
226
+ const errorHint = typeof err?.message === 'string' ? err.message : '';
227
+ if (content == null)
228
+ return errorHint;
229
+ if (typeof content === 'string')
230
+ return content;
231
+ if (Array.isArray(content)) {
232
+ const parts = [];
233
+ for (const block of content) {
234
+ if (typeof block === 'string') {
235
+ parts.push(block);
236
+ continue;
237
+ }
238
+ if (!block || typeof block !== 'object')
239
+ continue;
240
+ const rec = block;
241
+ if (typeof rec.text === 'string') {
242
+ parts.push(rec.text);
243
+ }
244
+ else if (rec.type === 'image') {
245
+ parts.push('[image]');
246
+ }
247
+ }
248
+ const text = parts.join('\n');
249
+ return text || errorHint;
250
+ }
251
+ try {
252
+ return JSON.stringify(content);
253
+ }
254
+ catch {
255
+ return String(content);
256
+ }
257
+ }
156
258
  /** Map DSH TokenUsage → ResultEvent.usage (cacheWrite = cache creation). */
157
259
  export function mapUsage(u) {
158
260
  return mapAggregatedUsage({
@@ -60,6 +60,27 @@ export interface DshSessionSummary {
60
60
  export interface DshSessionListValue {
61
61
  items: DshSessionSummary[];
62
62
  }
63
+ /**
64
+ * One pending inbox occurrence from the `inbox` projection.
65
+ *
66
+ * DSH 在 prompt 被受理但还没被 turn 取走时,把它记录成 `inbox.next-turn[]`
67
+ * 的一项;`source.rpcId` 回显 `session/prompt` 的 requestId,因此调用方可以
68
+ * 精确找到“我这条还没开始的消息”。`session/updateQueue` 用 `id` 删除它。
69
+ */
70
+ export interface DshInboxItem {
71
+ id: string;
72
+ source?: {
73
+ kind?: string;
74
+ rpcId?: string;
75
+ };
76
+ }
77
+ /** `session/projections` → value(只声明本仓库用到的部分)。 */
78
+ export interface DshProjectionSnapshot {
79
+ asOfSeq?: number;
80
+ values: Record<string, unknown>;
81
+ }
82
+ /** Whether the host currently has a turn open for a session. */
83
+ export type DshOpenTurnState = 'open' | 'closed' | 'unknown';
63
84
  /** session/page → value.records[]. */
64
85
  export interface DshPageRecord {
65
86
  event: DshSessionEvent;
@@ -309,3 +330,42 @@ export type DshStreamItem = {
309
330
  callId?: string;
310
331
  reason?: string;
311
332
  };
333
+ /**
334
+ * One finished projection value from the Host-wide `session/control` stream.
335
+ *
336
+ * DSH 的 `session/control` 是唯一的"服务端变一次推一次"通道:连接先收到一份
337
+ * baseline(覆盖当前所有活跃 session 的全部投影值),之后每个投影变化推一帧
338
+ * `{type:'projection', sessionId, key, value, seq}`。调用方按 sessionId 过滤。
339
+ */
340
+ export interface DshProjectionItem {
341
+ kind: 'projection';
342
+ sessionId: string;
343
+ key: string;
344
+ value: unknown;
345
+ }
346
+ /** `contextPressure` 投影(只声明本仓库消费的字段)。 */
347
+ export interface DshContextPressure {
348
+ /** 上一次采样实际占用的 token。 */
349
+ pressureTokens?: number;
350
+ /** 下一次请求的预估占用(官方 UI 优先用它算百分比)。 */
351
+ projectedTokens?: number;
352
+ /** 当前模型上下文窗口。 */
353
+ contextWindow?: number;
354
+ }
355
+ /** `sessionStats` 投影(只声明本仓库消费的字段)。 */
356
+ export interface DshSessionStats {
357
+ turns?: number;
358
+ steps?: number;
359
+ toolMs?: number;
360
+ }
361
+ /** One background job row from the `job/list` stream (fields lark-remote consumes). */
362
+ export interface DshJobView {
363
+ id: string;
364
+ kind?: string;
365
+ label?: string;
366
+ status?: 'running' | 'stopping' | 'completed' | 'killed' | 'failed';
367
+ progress?: string;
368
+ detail?: string;
369
+ startedAt?: number;
370
+ finishedAt?: number;
371
+ }
@@ -325,6 +325,45 @@ export interface SessionInfoEvent {
325
325
  mode?: string;
326
326
  timestamp?: string;
327
327
  }
328
+ /**
329
+ * 运行期状态投影(上下文水位 / 会话累计统计)。
330
+ *
331
+ * 不是内容块:卡片把它渲染在状态行(statusRow),不进内容流,因此不参与
332
+ * blocks 分组与 MAX_BLOCKS 裁剪。字段缺省表示该维度本次未变。
333
+ *
334
+ * 数据源是 agent 服务端自己算好的投影(DSH `session/control` 的
335
+ * contextPressure / sessionStats),bridge 不重算口径——百分比只在两端都
336
+ * 有值时才渲染。
337
+ */
338
+ export interface RunStatusEvent {
339
+ type: 'run_status';
340
+ /** 已投影 token(官方口径 projectedTokens ?? pressureTokens)。 */
341
+ contextUsed?: number;
342
+ /** 上下文窗口上限(contextWindow)。 */
343
+ contextLimit?: number;
344
+ /** 会话累计步骤数(sessionStats.steps)。 */
345
+ steps?: number;
346
+ /** 会话累计工具耗时(毫秒,sessionStats.toolMs)。 */
347
+ toolMs?: number;
348
+ timestamp?: string;
349
+ }
350
+ /**
351
+ * 运行结束后仍在后台运行的任务(终态卡展示 + 逐个停止入口)。
352
+ *
353
+ * 为什么必须出现在 run 卡片上:后台任务的生命周期跨 turn(`bash
354
+ * run_in_background` 起的东西能活过 run 结束),卡片收尾时如果不说,用户就
355
+ * 以为这一轮真的结束了——机器上却还在跑。
356
+ */
357
+ export interface BackgroundJobsEvent {
358
+ type: 'background_jobs';
359
+ jobs: Array<{
360
+ id: string;
361
+ label: string;
362
+ status?: string;
363
+ progress?: string;
364
+ }>;
365
+ timestamp?: string;
366
+ }
328
367
  /** File change event — for agents that report file modifications (e.g. Codex). */
329
368
  export interface FileChangeEvent {
330
369
  type: 'file_change';
@@ -392,7 +431,7 @@ export interface TurnDiffEvent {
392
431
  export type RunnerLifetime = 'turn' | 'workspace';
393
432
  /** 工具渲染 hint:runner 自愿声明,渲染层只消费不推断。 */
394
433
  export type ToolHint = 'shell' | 'file' | 'search' | 'web' | 'patch';
395
- export type AgentEvent = SystemInitEvent | SystemCompactEvent | ApprovalRequestedEvent | ApprovalResolvedEvent | ApprovalViewUpdatedEvent | ApprovalExpiredEvent | AssistantEvent | UserEvent | ResultEvent | PlanEvent | FileChangeEvent | TurnStartedEvent | TurnDiffEvent | NoticeEvent | SessionInfoEvent;
434
+ export type AgentEvent = SystemInitEvent | SystemCompactEvent | ApprovalRequestedEvent | ApprovalResolvedEvent | ApprovalViewUpdatedEvent | ApprovalExpiredEvent | AssistantEvent | UserEvent | ResultEvent | PlanEvent | FileChangeEvent | TurnStartedEvent | TurnDiffEvent | NoticeEvent | SessionInfoEvent | RunStatusEvent | BackgroundJobsEvent;
396
435
  export interface SpawnOptions {
397
436
  cwd: string;
398
437
  sessionId?: string;
@@ -402,6 +441,29 @@ export interface SpawnOptions {
402
441
  reasoningEffort?: string;
403
442
  settings?: string;
404
443
  }
444
+ /**
445
+ * What a stop request actually achieved.
446
+ *
447
+ * Local-process runners (`claude`/`codex`/…) can answer this from the kill
448
+ * itself and keep returning `void`. DSH turns live in the shared Web Host, so
449
+ * its stop is a session-scoped request: the outcome is the only way callers can
450
+ * tell "the turn is gone" apart from "we asked, but it is still running".
451
+ *
452
+ * - `withdrawn` — the prompt had not started yet and was removed from the
453
+ * host's inbox; no turn was cancelled.
454
+ * - `cancelled` — the active turn (or maintenance job) was cancelled and the
455
+ * host reported the session idle.
456
+ * - `unconfirmed` — the cancel was rejected/lost, or the session still reports
457
+ * an open turn afterwards. A turn may still be running.
458
+ * - `noop` — there was no session to stop (e.g. stop during setup).
459
+ */
460
+ export type StopStatus = 'withdrawn' | 'cancelled' | 'unconfirmed' | 'noop';
461
+ export interface StopOutcome {
462
+ status: StopStatus;
463
+ sessionId?: string;
464
+ /** Human-readable reason for `unconfirmed` (logged, not shown verbatim). */
465
+ detail?: string;
466
+ }
405
467
  /**
406
468
  * Seam (§3): the contract any agent runner (claude/codex/opencode/pi/kimi/dsh)
407
469
  * must satisfy for the router/bridge.
@@ -417,10 +479,14 @@ export interface Runner {
417
479
  * Stop the running process. SIGTERM → grace → SIGKILL on the per-workspace
418
480
  * proc. `immediate: true` skips the grace period and SIGKILLs directly
419
481
  * (used by user-initiated `/stop`).
482
+ *
483
+ * Runners whose work is remote (DSH) return a {@link StopOutcome} instead of
484
+ * `void` so callers can tell a confirmed stop from a best-effort request;
485
+ * process runners keep returning `void`.
420
486
  */
421
487
  stop(opts?: {
422
488
  immediate?: boolean;
423
- }): Promise<void>;
489
+ }): Promise<void | StopOutcome>;
424
490
  killOrphan(): void;
425
491
  registerExitHandlers(): void;
426
492
  /**
@@ -441,6 +507,8 @@ export interface Runner {
441
507
  */
442
508
  export declare const AGENT_KINDS: readonly ["claude", "codex", "opencode", "pi", "kimi", "dsh"];
443
509
  export type AgentKind = (typeof AGENT_KINDS)[number];
510
+ /** 用户可见 agent 显示名单源:启动注册与无 registry 的回退路径共用。 */
511
+ export declare const AGENT_DISPLAY_NAMES: Record<AgentKind, string>;
444
512
  /**
445
513
  * Compact 能力声明(运行时门控的能力清单,编译期强制完整)。
446
514
  *
@@ -463,11 +531,36 @@ export interface AgentSession {
463
531
  mtime: number;
464
532
  /** DSH:session 创建时固定的 preset;其他 agent 无此概念(undefined)。 */
465
533
  agentPreset?: string;
534
+ /**
535
+ * 服务端投影已有的会话标题(DSH `projections.values.title`)。
536
+ *
537
+ * 与 `summary` 的区别:`summary` 是「首条用户输入」的兜底口径(claude /
538
+ * codex / pi / kimi 无标题字段),`title` 是 agent 自己起的标题。列表卡
539
+ * 用它决定标签文案(标题 / 会话摘要),缺省时回退 `summary`。
540
+ */
541
+ title?: string;
542
+ /**
543
+ * 最后一轮用户输入(服务端投影,单行预览)。
544
+ *
545
+ * 只给「列表响应里已顺带返回」的 reader 用(DSH 的 `turnOutline` 末条),
546
+ * 目的是让列表卡不必为每行做一次全量内容读取。
547
+ */
548
+ lastUserMessage?: string;
549
+ /** 最后一轮助手输出(服务端投影已截断,单行预览)。 */
550
+ lastAssistantMessage?: string;
466
551
  }
467
552
  /** A single renderable event from session history. */
468
553
  export interface AgentSessionContentEvent {
469
554
  type: string;
555
+ /** 正文(展开后的面板内容)。 */
470
556
  content: string;
557
+ /**
558
+ * 面板标题(可选,覆盖 reader 的默认标签)。
559
+ *
560
+ * 折叠面板默认只显示标题:工具调用这种"标题即全部信息"的事件必须把
561
+ * `bash — npm test` 这类摘要放进标题,否则折叠态只剩 `🔧 工具调用`。
562
+ */
563
+ title?: string;
471
564
  timestamp?: string;
472
565
  }
473
566
  export interface AgentSessionUsage {
@@ -16,6 +16,15 @@
16
16
  * 新增 agent 只改这里。
17
17
  */
18
18
  export const AGENT_KINDS = ['claude', 'codex', 'opencode', 'pi', 'kimi', 'dsh'];
19
+ /** 用户可见 agent 显示名单源:启动注册与无 registry 的回退路径共用。 */
20
+ export const AGENT_DISPLAY_NAMES = {
21
+ claude: 'Claude',
22
+ codex: 'Codex',
23
+ opencode: 'Opencode',
24
+ pi: 'Pi',
25
+ kimi: 'Kimi',
26
+ dsh: 'DSH',
27
+ };
19
28
  /**
20
29
  * Compact 能力声明(运行时门控的能力清单,编译期强制完整)。
21
30
  *
@@ -74,10 +74,6 @@ export declare class SessionIndex {
74
74
  private readonly byCwd;
75
75
  private lastRefreshAt;
76
76
  private built;
77
- /** Test-only: count of parseSessionJsonl calls since creation */
78
- parseCount: number;
79
- /** Test-only: count of directory scan operations */
80
- scanCount: number;
81
77
  constructor(projectsDir: string, opts?: SessionIndexOptions);
82
78
  /** Whether a full scan has ever completed successfully (readdir succeeded). */
83
79
  get isBuilt(): boolean;
@@ -146,10 +146,6 @@ export class SessionIndex {
146
146
  byCwd = new Map();
147
147
  lastRefreshAt = 0;
148
148
  built = false;
149
- /** Test-only: count of parseSessionJsonl calls since creation */
150
- parseCount = 0;
151
- /** Test-only: count of directory scan operations */
152
- scanCount = 0;
153
149
  constructor(projectsDir, opts = {}) {
154
150
  this.projectsDir = projectsDir;
155
151
  this.refreshIntervalMs = opts.refreshIntervalMs ?? 5000;
@@ -161,7 +157,6 @@ export class SessionIndex {
161
157
  }
162
158
  // ── Build (initial full scan) ───────────────────────────
163
159
  build() {
164
- this.scanCount++;
165
160
  this.fullScan();
166
161
  this.lastRefreshAt = Date.now();
167
162
  }
@@ -171,7 +166,6 @@ export class SessionIndex {
171
166
  if (now - this.lastRefreshAt < this.refreshIntervalMs) {
172
167
  return; // throttled
173
168
  }
174
- this.scanCount++;
175
169
  this.fullScan();
176
170
  this.lastRefreshAt = now;
177
171
  }
@@ -232,7 +226,6 @@ export class SessionIndex {
232
226
  }
233
227
  // Fingerprint changed → re-parse this single file
234
228
  const parsed = parseSessionJsonl(p);
235
- this.parseCount++;
236
229
  try {
237
230
  const st = fs.statSync(p, { bigint: true });
238
231
  const updated = {
@@ -303,7 +296,6 @@ export class SessionIndex {
303
296
  // Concurrent-write guard: stat before and after parse, compare fingerprints
304
297
  const fpBefore = fp;
305
298
  const parsed = parseSessionJsonl(fullPath);
306
- this.parseCount++;
307
299
  const fpAfter = buildFingerprint(fullPath);
308
300
  if (fpBefore !== fpAfter) {
309
301
  // File was modified during parse → skip this round, retry next refresh
@@ -15,8 +15,8 @@ import { SessionIndex, extractText, isTaskNotificationText, CLAUDE_MAPPING, } fr
15
15
  * win32 上 `\` 同样是分隔符、`:` 是非法文件名字符,一并归一为 `-`
16
16
  * (`C:\Users\x\proj` -> `C--Users-x-proj`);见 encodeProjectDirName。
17
17
  *
18
- * Note: this encoding is LOSSY (`disk_d/foo` and `disk-d/foo` both encode to
19
- * `disk-d-foo`). It is only safe to use the result to **locate** files; never
18
+ * Note: this encoding is LOSSY (`my_data/foo` and `my-data/foo` both encode to
19
+ * `my-data-foo`). It is only safe to use the result to **locate** files; never
20
20
  * to **decode** a directory name back to a cwd. The cwd must always be read
21
21
  * from the JSONL content (regression 2026-06-21 /resume & /cd 路径错乱).
22
22
  */