lark-remote 0.1.0

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 (201) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/LICENSE +21 -0
  3. package/README.en.md +162 -0
  4. package/README.md +161 -0
  5. package/dist/binder.d.ts +57 -0
  6. package/dist/binder.js +105 -0
  7. package/dist/bridge/index.d.ts +338 -0
  8. package/dist/bridge/index.js +1281 -0
  9. package/dist/bridge/queue-manager.d.ts +277 -0
  10. package/dist/bridge/queue-manager.js +729 -0
  11. package/dist/card/bash-card-session.d.ts +78 -0
  12. package/dist/card/bash-card-session.js +174 -0
  13. package/dist/card/bash-renderer.d.ts +24 -0
  14. package/dist/card/bash-renderer.js +187 -0
  15. package/dist/card/card-budget.d.ts +31 -0
  16. package/dist/card/card-budget.js +280 -0
  17. package/dist/card/card-session.d.ts +83 -0
  18. package/dist/card/card-session.js +185 -0
  19. package/dist/card/card-shared.d.ts +28 -0
  20. package/dist/card/card-shared.js +99 -0
  21. package/dist/card/collapsible.d.ts +26 -0
  22. package/dist/card/collapsible.js +87 -0
  23. package/dist/card/run-card-session.d.ts +78 -0
  24. package/dist/card/run-card-session.js +165 -0
  25. package/dist/card/run-renderer.d.ts +66 -0
  26. package/dist/card/run-renderer.js +841 -0
  27. package/dist/card/run-state.d.ts +103 -0
  28. package/dist/card/run-state.js +313 -0
  29. package/dist/card/text-truncate.d.ts +56 -0
  30. package/dist/card/text-truncate.js +184 -0
  31. package/dist/card/time.d.ts +1 -0
  32. package/dist/card/time.js +14 -0
  33. package/dist/card/tool-render.d.ts +25 -0
  34. package/dist/card/tool-render.js +176 -0
  35. package/dist/cli.d.ts +28 -0
  36. package/dist/cli.js +89 -0
  37. package/dist/common/guards.d.ts +37 -0
  38. package/dist/common/guards.js +65 -0
  39. package/dist/config/codex-config.d.ts +113 -0
  40. package/dist/config/codex-config.js +428 -0
  41. package/dist/config/dir.d.ts +17 -0
  42. package/dist/config/dir.js +81 -0
  43. package/dist/config/index.d.ts +219 -0
  44. package/dist/config/index.js +388 -0
  45. package/dist/config/kimi-config.d.ts +33 -0
  46. package/dist/config/kimi-config.js +111 -0
  47. package/dist/config/opencode-config.d.ts +20 -0
  48. package/dist/config/opencode-config.js +124 -0
  49. package/dist/config/pi-config.d.ts +49 -0
  50. package/dist/config/pi-config.js +151 -0
  51. package/dist/config/wizard.d.ts +12 -0
  52. package/dist/config/wizard.js +84 -0
  53. package/dist/connector/file-limits.d.ts +7 -0
  54. package/dist/connector/file-limits.js +8 -0
  55. package/dist/connector/index.d.ts +86 -0
  56. package/dist/connector/index.js +340 -0
  57. package/dist/error-classification.d.ts +15 -0
  58. package/dist/error-classification.js +24 -0
  59. package/dist/index.d.ts +1 -0
  60. package/dist/index.js +580 -0
  61. package/dist/instance-lock.d.ts +30 -0
  62. package/dist/instance-lock.js +184 -0
  63. package/dist/logger/index.d.ts +57 -0
  64. package/dist/logger/index.js +178 -0
  65. package/dist/order/index.d.ts +27 -0
  66. package/dist/order/index.js +90 -0
  67. package/dist/persistence/atomic-write.d.ts +10 -0
  68. package/dist/persistence/atomic-write.js +65 -0
  69. package/dist/persistence/load-json-file.d.ts +9 -0
  70. package/dist/persistence/load-json-file.js +23 -0
  71. package/dist/restart.d.ts +24 -0
  72. package/dist/restart.js +73 -0
  73. package/dist/router/card-helpers.d.ts +28 -0
  74. package/dist/router/card-helpers.js +65 -0
  75. package/dist/router/config/claude.d.ts +14 -0
  76. package/dist/router/config/claude.js +72 -0
  77. package/dist/router/config/codex.d.ts +22 -0
  78. package/dist/router/config/codex.js +100 -0
  79. package/dist/router/config/common/render.d.ts +16 -0
  80. package/dist/router/config/common/render.js +158 -0
  81. package/dist/router/config/index.d.ts +18 -0
  82. package/dist/router/config/index.js +38 -0
  83. package/dist/router/config/kimi.d.ts +21 -0
  84. package/dist/router/config/kimi.js +63 -0
  85. package/dist/router/config/opencode.d.ts +12 -0
  86. package/dist/router/config/opencode.js +44 -0
  87. package/dist/router/config/pi.d.ts +12 -0
  88. package/dist/router/config/pi.js +50 -0
  89. package/dist/router/config/types.d.ts +43 -0
  90. package/dist/router/config/types.js +8 -0
  91. package/dist/router/index.d.ts +318 -0
  92. package/dist/router/index.js +2715 -0
  93. package/dist/router/order-exec-dispatch.d.ts +30 -0
  94. package/dist/router/order-exec-dispatch.js +48 -0
  95. package/dist/router/utils.d.ts +44 -0
  96. package/dist/router/utils.js +116 -0
  97. package/dist/runner/bash/index.d.ts +4 -0
  98. package/dist/runner/bash/index.js +5 -0
  99. package/dist/runner/bash/runner.d.ts +55 -0
  100. package/dist/runner/bash/runner.js +213 -0
  101. package/dist/runner/claude/index.d.ts +4 -0
  102. package/dist/runner/claude/index.js +5 -0
  103. package/dist/runner/claude/runner.d.ts +29 -0
  104. package/dist/runner/claude/runner.js +83 -0
  105. package/dist/runner/codex/argv.d.ts +33 -0
  106. package/dist/runner/codex/argv.js +40 -0
  107. package/dist/runner/codex/index.d.ts +1 -0
  108. package/dist/runner/codex/index.js +2 -0
  109. package/dist/runner/codex/jsonl.d.ts +37 -0
  110. package/dist/runner/codex/jsonl.js +221 -0
  111. package/dist/runner/codex/runner.d.ts +65 -0
  112. package/dist/runner/codex/runner.js +124 -0
  113. package/dist/runner/common/exec-translator.d.ts +73 -0
  114. package/dist/runner/common/exec-translator.js +105 -0
  115. package/dist/runner/common/jsonl-stream.d.ts +39 -0
  116. package/dist/runner/common/jsonl-stream.js +203 -0
  117. package/dist/runner/common/process-stopper.d.ts +28 -0
  118. package/dist/runner/common/process-stopper.js +79 -0
  119. package/dist/runner/common/runner-utils.d.ts +21 -0
  120. package/dist/runner/common/runner-utils.js +65 -0
  121. package/dist/runner/common/spawn-heartbeat.d.ts +33 -0
  122. package/dist/runner/common/spawn-heartbeat.js +51 -0
  123. package/dist/runner/common/spawning-runner.d.ts +321 -0
  124. package/dist/runner/common/spawning-runner.js +634 -0
  125. package/dist/runner/common/usage.d.ts +31 -0
  126. package/dist/runner/common/usage.js +29 -0
  127. package/dist/runner/index.d.ts +19 -0
  128. package/dist/runner/index.js +24 -0
  129. package/dist/runner/kimi/index.d.ts +1 -0
  130. package/dist/runner/kimi/index.js +2 -0
  131. package/dist/runner/kimi/jsonl.d.ts +28 -0
  132. package/dist/runner/kimi/jsonl.js +143 -0
  133. package/dist/runner/kimi/runner.d.ts +50 -0
  134. package/dist/runner/kimi/runner.js +105 -0
  135. package/dist/runner/opencode/argv.d.ts +32 -0
  136. package/dist/runner/opencode/argv.js +35 -0
  137. package/dist/runner/opencode/index.d.ts +1 -0
  138. package/dist/runner/opencode/index.js +2 -0
  139. package/dist/runner/opencode/jsonl.d.ts +68 -0
  140. package/dist/runner/opencode/jsonl.js +295 -0
  141. package/dist/runner/opencode/runner.d.ts +63 -0
  142. package/dist/runner/opencode/runner.js +93 -0
  143. package/dist/runner/pi/index.d.ts +1 -0
  144. package/dist/runner/pi/index.js +2 -0
  145. package/dist/runner/pi/jsonl.d.ts +128 -0
  146. package/dist/runner/pi/jsonl.js +249 -0
  147. package/dist/runner/pi/runner.d.ts +37 -0
  148. package/dist/runner/pi/runner.js +76 -0
  149. package/dist/runner/registry.d.ts +54 -0
  150. package/dist/runner/registry.js +75 -0
  151. package/dist/runner/resolve-agent-choices.d.ts +7 -0
  152. package/dist/runner/resolve-agent-choices.js +76 -0
  153. package/dist/runner/sync-agent-choices.d.ts +6 -0
  154. package/dist/runner/sync-agent-choices.js +73 -0
  155. package/dist/runner/types.d.ts +250 -0
  156. package/dist/runner/types.js +8 -0
  157. package/dist/session/claude/index.d.ts +1 -0
  158. package/dist/session/claude/index.js +2 -0
  159. package/dist/session/claude/session-reader.d.ts +34 -0
  160. package/dist/session/claude/session-reader.js +46 -0
  161. package/dist/session/claude/sessions.d.ts +53 -0
  162. package/dist/session/claude/sessions.js +528 -0
  163. package/dist/session/codex/index.d.ts +1 -0
  164. package/dist/session/codex/index.js +2 -0
  165. package/dist/session/codex/rollout-reader.d.ts +88 -0
  166. package/dist/session/codex/rollout-reader.js +473 -0
  167. package/dist/session/codex/sessions.d.ts +21 -0
  168. package/dist/session/codex/sessions.js +40 -0
  169. package/dist/session/common/catch-up.d.ts +12 -0
  170. package/dist/session/common/catch-up.js +31 -0
  171. package/dist/session/common/constants.d.ts +11 -0
  172. package/dist/session/common/constants.js +12 -0
  173. package/dist/session/common/content-blocks.d.ts +37 -0
  174. package/dist/session/common/content-blocks.js +68 -0
  175. package/dist/session/common/jsonl.d.ts +77 -0
  176. package/dist/session/common/jsonl.js +351 -0
  177. package/dist/session/common/usage-accumulator.d.ts +58 -0
  178. package/dist/session/common/usage-accumulator.js +72 -0
  179. package/dist/session/index.d.ts +7 -0
  180. package/dist/session/index.js +11 -0
  181. package/dist/session/kimi/index.d.ts +1 -0
  182. package/dist/session/kimi/index.js +2 -0
  183. package/dist/session/kimi/sessions.d.ts +81 -0
  184. package/dist/session/kimi/sessions.js +527 -0
  185. package/dist/session/opencode/index.d.ts +1 -0
  186. package/dist/session/opencode/index.js +2 -0
  187. package/dist/session/opencode/sessions.d.ts +81 -0
  188. package/dist/session/opencode/sessions.js +388 -0
  189. package/dist/session/pi/index.d.ts +1 -0
  190. package/dist/session/pi/index.js +2 -0
  191. package/dist/session/pi/sessions.d.ts +27 -0
  192. package/dist/session/pi/sessions.js +399 -0
  193. package/dist/session/registry.d.ts +12 -0
  194. package/dist/session/registry.js +20 -0
  195. package/dist/session/session-store.d.ts +86 -0
  196. package/dist/session/session-store.js +450 -0
  197. package/dist/startup-contact.d.ts +18 -0
  198. package/dist/startup-contact.js +51 -0
  199. package/dist/workspace/index.d.ts +19 -0
  200. package/dist/workspace/index.js +53 -0
  201. package/package.json +93 -0
@@ -0,0 +1,81 @@
1
+ /**
2
+ * OpencodeSessionReader: CLI-based session reader using `opencode session list` and `opencode export`.
3
+ *
4
+ * Design:
5
+ * - Uses official CLI commands instead of HTTP API.
6
+ * - `opencode session list --format json` returns sessions with `directory` field (first-class citizen).
7
+ * - `opencode export <sessionID>` returns full session content.
8
+ * - Independent of runner (no setRunner circular dependency).
9
+ *
10
+ * Key features:
11
+ * - listSessions: filter by directory field (the canonical session metadata).
12
+ * - readSessionContent: extract catch-up tail from export messages.
13
+ * - isSessionActive: use `updated` timestamp with 1 hour stale window.
14
+ * - Simple memory cache with TTL.
15
+ */
16
+ import type { AgentSession, AgentSessionReader, SessionContent } from '../../runner/index.js';
17
+ interface OpencodeSessionReaderOptions {
18
+ /** Path to opencode binary. Default: 'opencode' (in PATH). */
19
+ binary?: string;
20
+ /** Cache TTL for session list (ms). Default: 10_000. */
21
+ cacheTtlMs?: number;
22
+ /**
23
+ * Override the raw `opencode export <id>` capture (returns the JSON string).
24
+ * Default routes stdout to a temp file fd to bypass opencode's ~128KB pipe
25
+ * truncation on large sessions (L1). Injectable for testing.
26
+ */
27
+ captureExport?: (sessionId: string) => string;
28
+ }
29
+ export declare class OpencodeSessionReader implements AgentSessionReader {
30
+ private readonly binary;
31
+ private readonly cacheTtlMs;
32
+ /** Optional raw-export capture override (testing / alt transport). */
33
+ private readonly captureExportOverride?;
34
+ /** Cache keyed by cwd - each cwd has its own cache entry */
35
+ private listCache;
36
+ constructor(opts?: OpencodeSessionReaderOptions);
37
+ /**
38
+ * List sessions in a given cwd, using directory field for matching.
39
+ * Uses realpath for macOS /tmp -> /private/tmp compatibility.
40
+ * Passes user cwd to execFileSync so opencode CLI returns sessions for the correct directory.
41
+ */
42
+ listSessions(cwd: string, opts?: {
43
+ limit?: number;
44
+ offset?: number;
45
+ }): {
46
+ sessions: AgentSession[];
47
+ total: number;
48
+ };
49
+ /**
50
+ * Get the newest session in a given cwd.
51
+ */
52
+ getNewestSession(cwd: string): AgentSession | null;
53
+ /**
54
+ * Read session content: extract catch-up tail (last user message + everything after).
55
+ */
56
+ readSessionContent(sessionId: string, cwd: string, opts?: {
57
+ maxEvents?: number;
58
+ }): SessionContent;
59
+ /**
60
+ * Check if a session is active (updated within 1 hour).
61
+ * Passes cwd to fetchSessionList so it returns sessions for the correct directory.
62
+ */
63
+ isSessionActive(sessionId: string, cwd: string): boolean;
64
+ private realpath;
65
+ private fetchSessionList;
66
+ /**
67
+ * Capture raw `opencode export <sessionId>` output (JSON string). Injectable
68
+ * via the constructor; the default routes stdout to a temp file fd to bypass
69
+ * opencode's ~128KB pipe truncation on large sessions (L1).
70
+ */
71
+ private captureExport;
72
+ /**
73
+ * L1 default transport: run `opencode export` with stdout pointed at a
74
+ * regular-file fd (NOT a pipe). opencode truncates piped stdout at ~128KB for
75
+ * large sessions (huge tool_results), breaking JSON.parse; a file fd receives
76
+ * the full output (verified: 1.3MB parses OK). execFileSync returns null when
77
+ * stdout is not a pipe, so we read the file instead.
78
+ */
79
+ private captureExportViaFileFd;
80
+ }
81
+ export {};
@@ -0,0 +1,388 @@
1
+ /**
2
+ * OpencodeSessionReader: CLI-based session reader using `opencode session list` and `opencode export`.
3
+ *
4
+ * Design:
5
+ * - Uses official CLI commands instead of HTTP API.
6
+ * - `opencode session list --format json` returns sessions with `directory` field (first-class citizen).
7
+ * - `opencode export <sessionID>` returns full session content.
8
+ * - Independent of runner (no setRunner circular dependency).
9
+ *
10
+ * Key features:
11
+ * - listSessions: filter by directory field (the canonical session metadata).
12
+ * - readSessionContent: extract catch-up tail from export messages.
13
+ * - isSessionActive: use `updated` timestamp with 1 hour stale window.
14
+ * - Simple memory cache with TTL.
15
+ */
16
+ import { execFileSync } from 'node:child_process';
17
+ import fs from 'node:fs';
18
+ import os from 'node:os';
19
+ import path from 'node:path';
20
+ import { truncateUtf8, TOOL_RESULT_MAX_BYTES } from '../../card/text-truncate.js';
21
+ import { getLogger } from '../../logger/index.js';
22
+ import { STALE_MS } from '../common/constants.js';
23
+ export class OpencodeSessionReader {
24
+ binary;
25
+ cacheTtlMs;
26
+ /** Optional raw-export capture override (testing / alt transport). */
27
+ captureExportOverride;
28
+ /** Cache keyed by cwd - each cwd has its own cache entry */
29
+ listCache = new Map();
30
+ constructor(opts = {}) {
31
+ this.binary = opts.binary ?? 'opencode';
32
+ this.cacheTtlMs = opts.cacheTtlMs ?? 10_000;
33
+ this.captureExportOverride = opts.captureExport;
34
+ }
35
+ /**
36
+ * List sessions in a given cwd, using directory field for matching.
37
+ * Uses realpath for macOS /tmp -> /private/tmp compatibility.
38
+ * Passes user cwd to execFileSync so opencode CLI returns sessions for the correct directory.
39
+ */
40
+ listSessions(cwd, opts) {
41
+ const realCwd = this.realpath(cwd);
42
+ const entries = this.fetchSessionList(realCwd);
43
+ // Filter by directory === realpath(cwd)
44
+ const filtered = entries.filter((e) => e.directory === realCwd);
45
+ // Sort by updated descending; same-updated ties use id as a deterministic
46
+ // secondary key so CLI/list-cache rebuilds never reorder the page.
47
+ filtered.sort((a, b) => b.updated - a.updated || a.id.localeCompare(b.id));
48
+ // Compute total over the full sorted set, then paginate.
49
+ const total = filtered.length;
50
+ const offset = Math.max(0, opts?.offset ?? 0);
51
+ const limit = opts?.limit;
52
+ const page = limit === undefined ? filtered.slice(offset) : filtered.slice(offset, offset + limit);
53
+ return {
54
+ sessions: page.map((e) => ({
55
+ sessionId: e.id,
56
+ // CLI title 字段形状不保证恒为 string(null/缺字段时 router 的
57
+ // s.summary.trim() 会 TypeError)——非 string 给空串(Review R3 P3-3)。
58
+ summary: typeof e.title === 'string' ? e.title : '',
59
+ mtime: e.updated,
60
+ })),
61
+ total,
62
+ };
63
+ }
64
+ /**
65
+ * Get the newest session in a given cwd.
66
+ */
67
+ getNewestSession(cwd) {
68
+ return this.listSessions(cwd, { limit: 1 }).sessions[0] ?? null;
69
+ }
70
+ /**
71
+ * Read session content: extract catch-up tail (last user message + everything after).
72
+ */
73
+ readSessionContent(sessionId, cwd, opts) {
74
+ const realCwd = this.realpath(cwd);
75
+ // L1/L3: capture raw export via the injectable seam, then parse. Distinguish
76
+ // a truly-missing session (empty capture) from a truncated/corrupt one
77
+ // (non-empty but unparseable) so we never show the misleading "未找到" for
78
+ // a session whose content is merely too large to pipe through stdout.
79
+ let raw;
80
+ try {
81
+ raw = this.captureExport(sessionId);
82
+ }
83
+ catch (err) {
84
+ getLogger().warn(`[opencode-session-reader] captureExport failed: ${err}`);
85
+ return { events: [] };
86
+ }
87
+ const trimmed = raw?.trim() ?? '';
88
+ if (!trimmed) {
89
+ return { events: [] };
90
+ }
91
+ let data;
92
+ try {
93
+ data = JSON.parse(trimmed);
94
+ }
95
+ catch (err) {
96
+ getLogger().warn(`[opencode-session-reader] export JSON parse failed (truncated/corrupt?), raw bytes=${trimmed.length}: ${err}`);
97
+ return { events: [] };
98
+ }
99
+ try {
100
+ if (data.info.directory !== realCwd) {
101
+ return { events: [] };
102
+ }
103
+ const events = [];
104
+ let usage;
105
+ let aiTitle;
106
+ let displayTitle;
107
+ // Find the last user message position (for catch-up tail).
108
+ // messages are already parsed objects — scan directly instead of
109
+ // stringify→findLastUserIndex→parse round-trip (review P2-36).
110
+ let lastUserIndex = -1;
111
+ for (let i = 0; i < data.messages.length; i++) {
112
+ if (data.messages[i].info.role === 'user') {
113
+ lastUserIndex = i;
114
+ }
115
+ }
116
+ // Process messages from last user message onwards (catch-up tail)
117
+ const startIndex = lastUserIndex >= 0 ? lastUserIndex : 0;
118
+ for (let i = startIndex; i < data.messages.length; i++) {
119
+ const msg = data.messages[i];
120
+ const timestamp = msg.info.time?.created
121
+ ? new Date(msg.info.time.created).toISOString()
122
+ : undefined;
123
+ for (const part of msg.parts) {
124
+ if (part.type === 'text') {
125
+ // First user text after last user message is displayTitle
126
+ if (msg.info.role === 'user' && part.text && !displayTitle) {
127
+ displayTitle = part.text;
128
+ }
129
+ events.push({
130
+ type: 'text',
131
+ content: part.text ?? '',
132
+ timestamp,
133
+ });
134
+ }
135
+ else if (part.type === 'reasoning') {
136
+ events.push({
137
+ type: 'thinking',
138
+ content: part.text ?? '',
139
+ timestamp,
140
+ });
141
+ }
142
+ else if (part.type === 'tool') {
143
+ // Tool use
144
+ if (part.callID && part.state?.input) {
145
+ events.push({
146
+ type: 'tool_use',
147
+ content: JSON.stringify({
148
+ id: part.callID,
149
+ name: part.tool,
150
+ input: part.state.input,
151
+ }),
152
+ timestamp,
153
+ });
154
+ }
155
+ // Tool result (completed or error)
156
+ if (part.callID && part.state?.output) {
157
+ const isError = part.state.status === 'error';
158
+ const rawOut = isError
159
+ ? (part.state.error ?? part.state.output)
160
+ : (part.state.output ?? '');
161
+ // L2: pre-fold pathological tool_results (e.g. a 500KB file
162
+ // listing) via the shared truncateUtf8 primitive, reusing the
163
+ // same …(已截断) suffix as the live renderer / card budget.
164
+ events.push({
165
+ type: 'tool_result',
166
+ content: truncateUtf8(rawOut, TOOL_RESULT_MAX_BYTES),
167
+ timestamp,
168
+ });
169
+ }
170
+ }
171
+ }
172
+ // Track usage from step-finish parts
173
+ for (const part of msg.parts) {
174
+ if (part.type === 'step-finish' && part.tokens) {
175
+ // Use the last step-finish tokens (usually the final one).
176
+ // ccusage-aligned: cache.write -> cacheCreationTokens.
177
+ // contextLength = input + cacheRead + cacheCreation (excludes
178
+ // output/reasoning) — the unified context-window occupancy
179
+ // contract across all five readers (review P2-8).
180
+ usage = {
181
+ inputTokens: part.tokens.input,
182
+ outputTokens: part.tokens.output,
183
+ contextLength: part.tokens.input +
184
+ (part.tokens.cache?.read ?? 0) +
185
+ (part.tokens.cache?.write ?? 0),
186
+ cacheReadTokens: part.tokens.cache?.read,
187
+ cacheCreationTokens: part.tokens.cache?.write,
188
+ totalTokens: part.tokens.total,
189
+ };
190
+ }
191
+ }
192
+ }
193
+ // Cumulative (session-wide): sum input/output/total across ALL step-finish
194
+ // parts in the whole session. The catch-up loop above only scans from
195
+ // the last user message (= last run) for per-turn usage; cumulative
196
+ // needs a full scan so multi-run sessions report the true total.
197
+ let cumInput = 0;
198
+ let cumOutput = 0;
199
+ let cumTotal = 0;
200
+ let cumCacheRead = 0;
201
+ let cumCacheWrite = 0;
202
+ for (const msg of data.messages) {
203
+ for (const part of msg.parts) {
204
+ if (part.type === 'step-finish' && part.tokens) {
205
+ cumInput += part.tokens.input ?? 0;
206
+ cumOutput += part.tokens.output ?? 0;
207
+ cumTotal += part.tokens.total ?? 0;
208
+ cumCacheRead += part.tokens.cache?.read ?? 0;
209
+ cumCacheWrite += part.tokens.cache?.write ?? 0;
210
+ }
211
+ }
212
+ }
213
+ if (usage) {
214
+ usage.cumulativeTotalTokens = cumTotal;
215
+ usage.cumulativeInputTokens = cumInput;
216
+ usage.cumulativeOutputTokens = cumOutput;
217
+ usage.cumulativeCacheReadTokens = cumCacheRead;
218
+ usage.cumulativeCacheCreationTokens = cumCacheWrite;
219
+ }
220
+ else if (cumInput || cumOutput || cumTotal || cumCacheRead || cumCacheWrite) {
221
+ usage = {
222
+ inputTokens: 0,
223
+ outputTokens: 0,
224
+ contextLength: 0,
225
+ cumulativeTotalTokens: cumTotal,
226
+ cumulativeInputTokens: cumInput,
227
+ cumulativeOutputTokens: cumOutput,
228
+ cumulativeCacheReadTokens: cumCacheRead,
229
+ cumulativeCacheCreationTokens: cumCacheWrite,
230
+ };
231
+ }
232
+ // Use info.title as aiTitle
233
+ if (data.info.title) {
234
+ aiTitle = data.info.title;
235
+ }
236
+ // If no displayTitle yet, use first user text as fallback
237
+ if (!displayTitle) {
238
+ for (const msg of data.messages) {
239
+ if (msg.info.role === 'user') {
240
+ for (const part of msg.parts) {
241
+ if (part.type === 'text' && part.text) {
242
+ displayTitle = part.text;
243
+ break;
244
+ }
245
+ }
246
+ }
247
+ if (displayTitle)
248
+ break;
249
+ }
250
+ }
251
+ // Apply maxEvents cap: keep the LAST N events (most recent), matching
252
+ // CodexSessionReader's slice(-maxEvents). maxEvents limits the catch-up
253
+ // tail so auto-resume cards don't load the entire session.
254
+ // P2-7: maxEvents <= 0 returns [] (guards `slice(-0)` full-array trap).
255
+ const cappedEvents = opts?.maxEvents !== undefined
256
+ ? opts.maxEvents <= 0
257
+ ? []
258
+ : events.slice(-opts.maxEvents)
259
+ : events;
260
+ return {
261
+ events: cappedEvents,
262
+ usage,
263
+ // opencode run doesn't have background tasks
264
+ aiTitle,
265
+ displayTitle,
266
+ };
267
+ }
268
+ catch (err) {
269
+ getLogger().warn(`[opencode-session-reader] readSessionContent build failed: ${err}`);
270
+ return { events: [] };
271
+ }
272
+ }
273
+ /**
274
+ * Check if a session is active (updated within 1 hour).
275
+ * Passes cwd to fetchSessionList so it returns sessions for the correct directory.
276
+ */
277
+ isSessionActive(sessionId, cwd) {
278
+ const realCwd = this.realpath(cwd);
279
+ try {
280
+ const entries = this.fetchSessionList(realCwd);
281
+ const entry = entries.find((e) => e.id === sessionId && e.directory === realCwd);
282
+ if (!entry)
283
+ return false;
284
+ // Check if updated within STALE_MS
285
+ return Date.now() - entry.updated < STALE_MS;
286
+ }
287
+ catch {
288
+ return false;
289
+ }
290
+ }
291
+ // --- Private helpers ---
292
+ realpath(cwd) {
293
+ try {
294
+ return fs.realpathSync(cwd);
295
+ }
296
+ catch {
297
+ return cwd;
298
+ }
299
+ }
300
+ fetchSessionList(cwd) {
301
+ const realCwd = this.realpath(cwd);
302
+ const cached = this.listCache.get(realCwd);
303
+ if (cached && Date.now() - cached.ts < this.cacheTtlMs) {
304
+ return cached.data;
305
+ }
306
+ try {
307
+ // Pass cwd to execFileSync so opencode CLI returns sessions for the correct directory
308
+ const output = execFileSync(this.binary, ['session', 'list', '--format', 'json'], {
309
+ encoding: 'utf-8',
310
+ timeout: 10000,
311
+ // P1-15: default maxBuffer is 1MiB — a large session list (>1MiB JSON)
312
+ // throws ENOBUFS, which the catch below silently turns into [] and the
313
+ // user sees "no sessions" even though the CLI returned healthy data.
314
+ maxBuffer: 64 * 1024 * 1024,
315
+ cwd: realCwd, // Key fix: pass user cwd to CLI
316
+ }).trim();
317
+ // CLI returns empty string when no sessions exist - treat as empty array
318
+ if (!output) {
319
+ return [];
320
+ }
321
+ const data = JSON.parse(output);
322
+ // Update cache keyed by cwd
323
+ this.listCache.set(realCwd, { ts: Date.now(), data });
324
+ return data;
325
+ }
326
+ catch (err) {
327
+ // P1-15: 区分「读取失败」与「真空/陈旧 cwd」。
328
+ // - ENOENT(cwd 已删除或 binary 缺失):沿用既有契约返回 []——/resume 对
329
+ // 陈旧 cwd 保持优雅(sessions.test.ts「Stale-cwd coverage gap」+ round10
330
+ // 探针的跨 reader 空目录不变量都钉死此行为)。
331
+ // - 其余失败(exit≠0 / ENOBUFS / JSON 解析错误):上抛可诊断错误,router
332
+ // 显示「读取失败」——静默 [] 会让用户误以为没有 session,与真空不可区分。
333
+ if (err.code === 'ENOENT') {
334
+ return [];
335
+ }
336
+ getLogger().error(`[opencode-session-reader] session list failed: ${err}`);
337
+ const msg = err instanceof Error ? err.message : String(err);
338
+ throw new Error(`opencode session list 读取失败: ${msg}`, { cause: err });
339
+ }
340
+ }
341
+ /**
342
+ * Capture raw `opencode export <sessionId>` output (JSON string). Injectable
343
+ * via the constructor; the default routes stdout to a temp file fd to bypass
344
+ * opencode's ~128KB pipe truncation on large sessions (L1).
345
+ */
346
+ captureExport(sessionId) {
347
+ if (this.captureExportOverride)
348
+ return this.captureExportOverride(sessionId);
349
+ return this.captureExportViaFileFd(sessionId);
350
+ }
351
+ /**
352
+ * L1 default transport: run `opencode export` with stdout pointed at a
353
+ * regular-file fd (NOT a pipe). opencode truncates piped stdout at ~128KB for
354
+ * large sessions (huge tool_results), breaking JSON.parse; a file fd receives
355
+ * the full output (verified: 1.3MB parses OK). execFileSync returns null when
356
+ * stdout is not a pipe, so we read the file instead.
357
+ */
358
+ captureExportViaFileFd(sessionId) {
359
+ const tmp = path.join(os.tmpdir(), `opencode-export-${sessionId}-${process.pid}-${Date.now()}.json`);
360
+ let fd;
361
+ try {
362
+ fd = fs.openSync(tmp, 'w');
363
+ execFileSync(this.binary, ['export', sessionId], {
364
+ encoding: 'utf-8',
365
+ timeout: 30000,
366
+ stdio: ['ignore', fd, 'pipe'],
367
+ // P1-15: stderr is piped here; without maxBuffer a chatty CLI (>1MiB
368
+ // stderr) would throw ENOBUFS and lose the export. Keep the default
369
+ // stdout file-fd transport unaffected by raising the pipe cap.
370
+ maxBuffer: 64 * 1024 * 1024,
371
+ });
372
+ return fs.readFileSync(tmp, 'utf-8');
373
+ }
374
+ finally {
375
+ if (fd !== undefined) {
376
+ try {
377
+ fs.closeSync(fd);
378
+ }
379
+ catch { }
380
+ }
381
+ try {
382
+ fs.unlinkSync(tmp);
383
+ }
384
+ catch { }
385
+ }
386
+ }
387
+ }
388
+ //# sourceMappingURL=sessions.js.map
@@ -0,0 +1 @@
1
+ export { PiSessionReader } from './sessions.js';
@@ -0,0 +1,2 @@
1
+ export { PiSessionReader } from './sessions.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,27 @@
1
+ import type { AgentSession, AgentSessionReader, SessionContent } from '../../runner/index.js';
2
+ /**
3
+ * PiSessionReader: reads pi agent sessions from `~/.pi/agent/`.
4
+ *
5
+ * pi is file-system based (like Claude), so this implements AgentSessionReader
6
+ * directly. The session list comes from directory scanning.
7
+ */
8
+ export declare class PiSessionReader implements AgentSessionReader {
9
+ private readonly piDir;
10
+ private readonly sessionsDir;
11
+ constructor(opts?: {
12
+ piDir?: string;
13
+ });
14
+ listSessions(cwd: string, opts?: {
15
+ limit?: number;
16
+ offset?: number;
17
+ }): {
18
+ sessions: AgentSession[];
19
+ total: number;
20
+ };
21
+ getNewestSession(cwd: string): AgentSession | null;
22
+ readSessionContent(sessionId: string, cwd: string, opts?: {
23
+ maxEvents?: number;
24
+ }): SessionContent;
25
+ isSessionActive(sessionId: string, cwd: string): boolean;
26
+ private listSessionsByScan;
27
+ }