lark-remote 0.5.3 → 0.5.5
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.
- package/CHANGELOG.md +38 -0
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/dist/bridge/approval-coordinator.d.ts +18 -1
- package/dist/bridge/approval-coordinator.js +57 -10
- package/dist/bridge/index.js +90 -17
- package/dist/card/approval-render.js +18 -0
- package/dist/card/run-state.js +10 -5
- package/dist/config/index.d.ts +16 -0
- package/dist/config/index.js +46 -6
- package/dist/config/opencode-config.d.ts +3 -3
- package/dist/config/opencode-config.js +38 -22
- package/dist/inbound/authorize.d.ts +34 -0
- package/dist/inbound/authorize.js +31 -0
- package/dist/index.js +116 -63
- package/dist/instance-lock.d.ts +9 -0
- package/dist/instance-lock.js +113 -42
- package/dist/router/config/dsh.d.ts +51 -21
- package/dist/router/config/dsh.js +255 -66
- package/dist/router/config/opencode.js +16 -6
- package/dist/router/config/types.d.ts +6 -3
- package/dist/router/index.d.ts +1 -0
- package/dist/router/index.js +13 -5
- package/dist/runner/codex/app-server/translator.js +21 -7
- package/dist/runner/common/acp/base-acp-runner.d.ts +12 -0
- package/dist/runner/common/acp/base-acp-runner.js +83 -8
- package/dist/runner/common/acp/protocol-types.d.ts +10 -2
- package/dist/runner/common/acp/protocol-types.js +3 -1
- package/dist/runner/common/connection-based-runner.d.ts +15 -0
- package/dist/runner/common/connection-based-runner.js +40 -5
- package/dist/runner/common/jsonrpc/connection-manager.d.ts +15 -0
- package/dist/runner/common/jsonrpc/connection-manager.js +83 -50
- package/dist/runner/dsh/approval.d.ts +53 -0
- package/dist/runner/dsh/approval.js +161 -0
- package/dist/runner/dsh/auth.d.ts +36 -0
- package/dist/runner/dsh/auth.js +118 -0
- package/dist/runner/dsh/client.d.ts +161 -42
- package/dist/runner/dsh/client.js +551 -244
- package/dist/runner/dsh/daemon.d.ts +85 -0
- package/dist/runner/dsh/daemon.js +272 -0
- package/dist/runner/dsh/merge.d.ts +9 -0
- package/dist/runner/dsh/merge.js +51 -0
- package/dist/runner/dsh/runner.d.ts +94 -11
- package/dist/runner/dsh/runner.js +553 -67
- package/dist/runner/dsh/translator.d.ts +43 -3
- package/dist/runner/dsh/translator.js +115 -38
- package/dist/runner/dsh/types.d.ts +189 -27
- package/dist/runner/dsh/types.js +11 -5
- package/dist/runner/kimi/acp/runner.js +15 -3
- package/dist/runner/opencode/acp/error-monitor.d.ts +2 -2
- package/dist/runner/opencode/acp/error-monitor.js +2 -2
- package/dist/runner/opencode/acp/runner.d.ts +9 -0
- package/dist/runner/opencode/acp/runner.js +40 -1
- package/dist/runner/opencode/acp/translator.d.ts +4 -2
- package/dist/runner/opencode/acp/translator.js +12 -3
- package/dist/runner/pi/rpc/translator.d.ts +2 -0
- package/dist/runner/pi/rpc/translator.js +27 -1
- package/dist/runner/types.d.ts +34 -0
- package/dist/runner/types.js +24 -0
- package/dist/session/codex/rollout-reader.js +44 -7
- package/dist/session/dsh/sessions.d.ts +45 -6
- package/dist/session/dsh/sessions.js +171 -27
- package/dist/session/opencode/sessions.d.ts +18 -7
- package/dist/session/opencode/sessions.js +294 -15
- package/dist/session/pi/sessions.js +6 -2
- package/package.json +3 -1
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The AgentSessionReader contract is fully synchronous (file-style readers),
|
|
5
5
|
* but DSH exposes only async HTTP. The reader therefore bridges with
|
|
6
|
-
* `spawnSync('curl', ...)` for its unary calls (session
|
|
6
|
+
* `spawnSync('curl', ...)` for its unary calls (session/list / session/page)
|
|
7
7
|
* — a synchronous HTTP round-trip to the local DSH host. This keeps the
|
|
8
8
|
* router/bridge interface contract intact without touching any upstream logic.
|
|
9
9
|
*
|
|
10
|
-
* listSessions: session
|
|
10
|
+
* listSessions: session/list → filter by cwd → sort by updatedAt desc → local
|
|
11
11
|
* pagination (DSH v1 has no cursor pagination).
|
|
12
|
-
* readSessionContent: session
|
|
13
|
-
* session-wide usage accumulation (assistant/message.usage)
|
|
12
|
+
* readSessionContent: session/projections → session/page → map SessionEvents +
|
|
13
|
+
* session-wide usage accumulation (assistant/message.usage);本 run 的 flow
|
|
14
|
+
* 字段 = 末个 turn(turn/start–turn/end)内所有采样之和,与 codex 同口径。
|
|
14
15
|
*/
|
|
15
16
|
import { spawnProcessSync } from '../../platform/spawn.js';
|
|
16
17
|
import { cumulativeUsageFields } from '../common/usage-accumulator.js';
|
|
@@ -18,22 +19,25 @@ import { sortByRecencyDesc } from '../common/recency.js';
|
|
|
18
19
|
import { getLogger } from '../../logger/index.js';
|
|
19
20
|
import { DEFAULT_DSH_HOST } from '../../config/index.js';
|
|
20
21
|
import { samePath } from '../../platform/path.js';
|
|
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';
|
|
21
25
|
import { capEvents, paginate } from '../common/pagination.js';
|
|
22
26
|
import { truncateUtf8, TOOL_RESULT_MAX_BYTES } from '../../common/truncate.js';
|
|
23
27
|
/**
|
|
24
|
-
* history 拉取上限。DSH session
|
|
28
|
+
* history 拉取上限。DSH session/page 的 maxMessages 同时限制返回条数,
|
|
25
29
|
* 若用它同时充当展示截断,>50 事件的会话累计 usage 会少算。这里拉取一个足够大的
|
|
26
30
|
* 上限保证 usage 累加覆盖整个会话;展示仍由 capEvents(opts?.maxEvents ?? 50) 截断。
|
|
27
31
|
* (若 DSH 支持分页 cursor 可改成分页,当前用有界大拉取)
|
|
28
32
|
*/
|
|
29
33
|
const DSH_HISTORY_FETCH_LIMIT = 10_000;
|
|
30
34
|
/** Synchronous unary POST via curl (the reader interface is synchronous). */
|
|
31
|
-
function defaultSyncRequest(baseUrl, method, payload) {
|
|
35
|
+
function defaultSyncRequest(baseUrl, method, payload, cookie) {
|
|
32
36
|
const body = JSON.stringify({
|
|
33
37
|
type: 'client-request',
|
|
34
38
|
rpcId: `rpc-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`,
|
|
35
39
|
method,
|
|
36
|
-
payload,
|
|
40
|
+
payload: { args: dshArgs(method, payload) },
|
|
37
41
|
});
|
|
38
42
|
const res = spawnProcessSync('curl', [
|
|
39
43
|
'-s',
|
|
@@ -42,6 +46,7 @@ function defaultSyncRequest(baseUrl, method, payload) {
|
|
|
42
46
|
`${baseUrl}/api/${method}`,
|
|
43
47
|
'-H',
|
|
44
48
|
'content-type: application/json',
|
|
49
|
+
...(cookie ? ['-H', `Cookie: ${cookie}`] : []),
|
|
45
50
|
'--data',
|
|
46
51
|
body,
|
|
47
52
|
], { encoding: 'utf-8', timeout: 15_000, maxBuffer: 64 * 1024 * 1024 });
|
|
@@ -62,25 +67,105 @@ function defaultSyncRequest(baseUrl, method, payload) {
|
|
|
62
67
|
}
|
|
63
68
|
return parsed.result.value;
|
|
64
69
|
}
|
|
70
|
+
/** Synchronous index bootstrap; returns the v1 `name=value` cookie pair. */
|
|
71
|
+
function defaultSyncConnect(baseUrl, token) {
|
|
72
|
+
const url = `${baseUrl}/?token=${encodeURIComponent(token)}`;
|
|
73
|
+
const res = spawnProcessSync('curl', ['-sS', '-D', '-', '-o', isWin32() ? 'NUL' : '/dev/null', '--max-time', '5', url], { encoding: 'utf-8', timeout: 10_000, maxBuffer: 1024 * 1024 });
|
|
74
|
+
if (res.error || res.status !== 0) {
|
|
75
|
+
throw new Error(`DSH auth failed: ${(res.error?.message ?? res.stderr) || 'unknown'}`);
|
|
76
|
+
}
|
|
77
|
+
const header = String(res.stdout);
|
|
78
|
+
const match = header.match(/^set-cookie:\s*(.+)$/im);
|
|
79
|
+
const cookie = match?.[1]?.split(';')[0]?.trim();
|
|
80
|
+
if (!cookie)
|
|
81
|
+
throw new Error('DSH auth failed: index handshake did not return a cookie');
|
|
82
|
+
return cookie;
|
|
83
|
+
}
|
|
65
84
|
export class DshSessionReader {
|
|
66
85
|
hostProvider;
|
|
86
|
+
dshHomeProvider;
|
|
87
|
+
tokenProvider;
|
|
88
|
+
credentialsFileProvider;
|
|
89
|
+
readCredentials;
|
|
90
|
+
now;
|
|
91
|
+
cookieLifetimeMs;
|
|
67
92
|
syncRequest;
|
|
93
|
+
syncConnect;
|
|
94
|
+
cookieCache = new Map();
|
|
68
95
|
constructor(opts = {}) {
|
|
69
96
|
const fixed = (opts.host ?? DEFAULT_DSH_HOST).replace(/\/$/, '');
|
|
70
97
|
this.hostProvider = opts.hostProvider ?? (() => fixed);
|
|
98
|
+
this.dshHomeProvider = opts.dshHomeProvider ?? (() => undefined);
|
|
99
|
+
const fixedToken = opts.token;
|
|
100
|
+
this.tokenProvider = opts.tokenProvider ?? (() => fixedToken);
|
|
101
|
+
const fixedCredentialsFile = opts.credentialsFile;
|
|
102
|
+
this.credentialsFileProvider = opts.credentialsFileProvider ?? (() => fixedCredentialsFile);
|
|
103
|
+
this.readCredentials = opts.readCredentials ?? readDshBrowserSecretSync;
|
|
104
|
+
this.now = opts.now ?? Date.now;
|
|
105
|
+
this.cookieLifetimeMs = opts.cookieLifetimeMs ?? DSH_COOKIE_LIFETIME_MS;
|
|
71
106
|
this.syncRequest = opts.syncRequest ?? defaultSyncRequest;
|
|
107
|
+
this.syncConnect = opts.syncConnect ?? defaultSyncConnect;
|
|
72
108
|
}
|
|
73
109
|
/** 解析当前 host(hostProvider 优先,允许 /config 热改 host)。 */
|
|
74
110
|
resolveBaseUrl() {
|
|
75
111
|
const host = this.hostProvider();
|
|
76
112
|
return (host && host.length > 0 ? host : DEFAULT_DSH_HOST).replace(/\/$/, '');
|
|
77
113
|
}
|
|
114
|
+
resolveCredentialsFile() {
|
|
115
|
+
return (this.credentialsFileProvider()?.trim() ||
|
|
116
|
+
resolveDshCredentialsFile(process.env, this.dshHomeProvider()));
|
|
117
|
+
}
|
|
78
118
|
unary(method, payload) {
|
|
79
|
-
|
|
119
|
+
const baseUrl = this.resolveBaseUrl();
|
|
120
|
+
try {
|
|
121
|
+
return this.syncRequest(baseUrl, method, payload, this.resolveCookie());
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// DSH may have restarted while lark-remote cached a token-derived cookie.
|
|
125
|
+
// Re-read the credentials file and retry once with a fresh cookie.
|
|
126
|
+
this.cookieCache.clear();
|
|
127
|
+
return this.syncRequest(baseUrl, method, payload, this.resolveCookie());
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** Mint from the durable secret, or bootstrap and cache a token cookie. */
|
|
131
|
+
resolveCookie() {
|
|
132
|
+
const baseUrl = this.resolveBaseUrl();
|
|
133
|
+
const fromCredentials = this.cookieFromCredentials(baseUrl);
|
|
134
|
+
if (fromCredentials)
|
|
135
|
+
return fromCredentials;
|
|
136
|
+
const token = extractDshToken(this.tokenProvider());
|
|
137
|
+
if (!token)
|
|
138
|
+
return undefined;
|
|
139
|
+
const key = `${baseUrl}\u0000${token}`;
|
|
140
|
+
const cached = this.cookieCache.get(key);
|
|
141
|
+
if (cached)
|
|
142
|
+
return cached;
|
|
143
|
+
const cookie = this.syncConnect(baseUrl, token);
|
|
144
|
+
this.cookieCache.set(key, cookie);
|
|
145
|
+
return cookie;
|
|
146
|
+
}
|
|
147
|
+
cookieFromCredentials(baseUrl) {
|
|
148
|
+
const credentialsFile = this.resolveCredentialsFile();
|
|
149
|
+
try {
|
|
150
|
+
const secret = this.readCredentials(credentialsFile);
|
|
151
|
+
return mintDshBrowserCookie(baseUrl, secret, this.now(), this.cookieLifetimeMs);
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
if (isDshCredentialsNotFound(err))
|
|
155
|
+
return undefined;
|
|
156
|
+
if (extractDshToken(this.tokenProvider())) {
|
|
157
|
+
getLogger().warn(`[dsh-session-reader] cannot use DSH credentials file ${credentialsFile}: ` +
|
|
158
|
+
`${err.message}; falling back to the launch token`);
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
throw new Error(`DSH auth credentials file ${credentialsFile}: ${err.message}`, {
|
|
162
|
+
cause: err,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
80
165
|
}
|
|
81
166
|
listSessions(cwd, opts) {
|
|
82
167
|
try {
|
|
83
|
-
const value = this.unary('session
|
|
168
|
+
const value = this.unary('session/list', {});
|
|
84
169
|
const items = value.items ?? [];
|
|
85
170
|
const filtered = items
|
|
86
171
|
.filter((i) => i.cwd !== undefined && samePath(i.cwd, cwd))
|
|
@@ -88,7 +173,7 @@ export class DshSessionReader {
|
|
|
88
173
|
sessionId: String(i.sessionId),
|
|
89
174
|
summary: '',
|
|
90
175
|
mtime: i.updatedAt,
|
|
91
|
-
...(i
|
|
176
|
+
...sessionPreset(i),
|
|
92
177
|
}));
|
|
93
178
|
sortByRecencyDesc(filtered, (i) => i.mtime, (i) => i.sessionId);
|
|
94
179
|
const { items: page, total } = paginate(filtered, opts ?? {});
|
|
@@ -102,10 +187,21 @@ export class DshSessionReader {
|
|
|
102
187
|
getNewestSession(cwd) {
|
|
103
188
|
return this.listSessions(cwd, { limit: 1 }).sessions[0] ?? null;
|
|
104
189
|
}
|
|
105
|
-
readSessionContent(sessionId,
|
|
190
|
+
readSessionContent(sessionId, cwd, opts) {
|
|
106
191
|
try {
|
|
107
|
-
|
|
108
|
-
|
|
192
|
+
// cwd 守卫:sessionId 可能属于另一个 workspace。
|
|
193
|
+
// 只按 id 查 history 会让 /resume 把 cwd A 的卡片绑到 cwd B 的会话上,
|
|
194
|
+
// agent 实际在 B 操作文件、resume.compact 还会压缩别人的会话。与
|
|
195
|
+
// Claude/Kimi/Pi 的 cwd 守卫同口径:找不到同 cwd 的会话 → 空内容(fail-closed)。
|
|
196
|
+
const session = this.resolveSession(sessionId, cwd);
|
|
197
|
+
if (!session) {
|
|
198
|
+
getLogger().warn(`[dsh-session-reader] readSessionContent: session ${sessionId} not found in cwd ${cwd}, refusing cross-workspace read`);
|
|
199
|
+
return { events: [] };
|
|
200
|
+
}
|
|
201
|
+
const throughSeq = this.resolveThroughSeq(sessionId, session);
|
|
202
|
+
const value = this.unary('session/page', {
|
|
203
|
+
address: { kind: 'session', sessionId },
|
|
204
|
+
throughSeq,
|
|
109
205
|
maxMessages: DSH_HISTORY_FETCH_LIMIT,
|
|
110
206
|
});
|
|
111
207
|
const events = [];
|
|
@@ -115,10 +211,25 @@ export class DshSessionReader {
|
|
|
115
211
|
let cumCacheRead = 0;
|
|
116
212
|
let cumCacheWrite = 0;
|
|
117
213
|
let lastUsage;
|
|
118
|
-
|
|
214
|
+
// 末个 turn 内各采样用量之和("本 run / 末轮" scope)。DSH 每次模型调用
|
|
215
|
+
// 一条 usage,工具往返的 turn 会写多条 assistant/message;只取末条会把
|
|
216
|
+
// 多步 run 低报(实测 231/689 显示成整轮真实消耗)。turn 边界取
|
|
217
|
+
// turn/start–turn/end,与 codex `task_started` 汇总同一口径(§9.21)。
|
|
218
|
+
// null = 历史里没有 turn 标记(旧数据/截断)→ 退回末条 usage 语义。
|
|
219
|
+
let turnUsage = null;
|
|
220
|
+
let lastTurnUsage = null;
|
|
221
|
+
for (const entry of value.records ?? []) {
|
|
119
222
|
const ev = entry.event;
|
|
120
223
|
if (!ev)
|
|
121
224
|
continue;
|
|
225
|
+
if (ev.type === 'turn/start') {
|
|
226
|
+
turnUsage = emptyUsageTotals();
|
|
227
|
+
}
|
|
228
|
+
else if (ev.type === 'turn/end') {
|
|
229
|
+
if (turnUsage)
|
|
230
|
+
lastTurnUsage = turnUsage;
|
|
231
|
+
turnUsage = null;
|
|
232
|
+
}
|
|
122
233
|
const contentEvents = mapEvent(ev);
|
|
123
234
|
events.push(...contentEvents.events);
|
|
124
235
|
if (contentEvents.title)
|
|
@@ -130,26 +241,35 @@ export class DshSessionReader {
|
|
|
130
241
|
cumOutput += u.outputTokens ?? 0;
|
|
131
242
|
cumCacheRead += u.cacheReadTokens ?? 0;
|
|
132
243
|
cumCacheWrite += u.cacheWriteTokens ?? 0;
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
244
|
+
// 本 run 的 flow 字段 = 末个 turn 的采样之和;contextLength 用末次采样
|
|
245
|
+
// 的窗口占用(input + cacheRead + cacheWrite,不含 output)。
|
|
246
|
+
if (turnUsage) {
|
|
247
|
+
turnUsage.input += u.inputTokens ?? 0;
|
|
248
|
+
turnUsage.output += u.outputTokens ?? 0;
|
|
249
|
+
turnUsage.cacheRead += u.cacheReadTokens ?? 0;
|
|
250
|
+
turnUsage.cacheWrite += u.cacheWriteTokens ?? 0;
|
|
251
|
+
}
|
|
137
252
|
lastUsage = u;
|
|
138
253
|
}
|
|
139
254
|
}
|
|
140
255
|
let sessionUsage;
|
|
141
256
|
if (lastUsage) {
|
|
142
257
|
const input = lastUsage.inputTokens ?? 0;
|
|
143
|
-
const output = lastUsage.outputTokens ?? 0;
|
|
144
258
|
const cacheRead = lastUsage.cacheReadTokens ?? 0;
|
|
145
259
|
const cacheWrite = lastUsage.cacheWriteTokens ?? 0;
|
|
260
|
+
const flow = lastTurnUsage ?? {
|
|
261
|
+
input,
|
|
262
|
+
output: lastUsage.outputTokens ?? 0,
|
|
263
|
+
cacheRead,
|
|
264
|
+
cacheWrite,
|
|
265
|
+
};
|
|
146
266
|
sessionUsage = {
|
|
147
|
-
inputTokens: input,
|
|
148
|
-
outputTokens: output,
|
|
267
|
+
inputTokens: flow.input,
|
|
268
|
+
outputTokens: flow.output,
|
|
149
269
|
contextLength: input + cacheRead + cacheWrite,
|
|
150
|
-
cacheReadTokens: cacheRead,
|
|
151
|
-
cacheCreationTokens: cacheWrite,
|
|
152
|
-
totalTokens: input + output + cacheRead + cacheWrite,
|
|
270
|
+
cacheReadTokens: flow.cacheRead,
|
|
271
|
+
cacheCreationTokens: flow.cacheWrite,
|
|
272
|
+
totalTokens: flow.input + flow.output + flow.cacheRead + flow.cacheWrite,
|
|
153
273
|
...cumulativeUsageFields({
|
|
154
274
|
input: cumInput,
|
|
155
275
|
output: cumOutput,
|
|
@@ -173,15 +293,39 @@ export class DshSessionReader {
|
|
|
173
293
|
}
|
|
174
294
|
isSessionActive(sessionId, cwd) {
|
|
175
295
|
try {
|
|
176
|
-
|
|
177
|
-
const found = (value.items ?? []).find((i) => String(i.sessionId) === sessionId && i.cwd !== undefined && samePath(i.cwd, cwd));
|
|
178
|
-
return found?.running === true;
|
|
296
|
+
return this.resolveSession(sessionId, cwd)?.running === true;
|
|
179
297
|
}
|
|
180
298
|
catch (error) {
|
|
181
299
|
getLogger().error(`[dsh-session-reader] isSessionActive error: ${error.message}`);
|
|
182
300
|
return false;
|
|
183
301
|
}
|
|
184
302
|
}
|
|
303
|
+
/**
|
|
304
|
+
* Find the session row in `session/list` that matches BOTH id and cwd.
|
|
305
|
+
* Returns undefined when the id belongs to another workspace (or is unknown) —
|
|
306
|
+
* every caller treats that as "not usable here".
|
|
307
|
+
*/
|
|
308
|
+
resolveSession(sessionId, cwd) {
|
|
309
|
+
const value = this.unary('session/list', {});
|
|
310
|
+
return (value.items ?? []).find((i) => String(i.sessionId) === sessionId && i.cwd !== undefined && samePath(i.cwd, cwd));
|
|
311
|
+
}
|
|
312
|
+
/** `session/page` needs an inclusive cursor; projections carry the watermark. */
|
|
313
|
+
resolveThroughSeq(sessionId, known) {
|
|
314
|
+
const projection = this.unary('session/projections', { sessionId });
|
|
315
|
+
if (typeof projection?.asOfSeq === 'number')
|
|
316
|
+
return projection.asOfSeq;
|
|
317
|
+
const row = known ??
|
|
318
|
+
this.unary('session/list', {}).items?.find((item) => String(item.sessionId) === sessionId);
|
|
319
|
+
return typeof row?.projections?.asOfSeq === 'number' ? row.projections.asOfSeq : -1;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/** Preset moved from the summary top level to projections in DSH 0.2.x. */
|
|
323
|
+
function sessionPreset(item) {
|
|
324
|
+
const preset = item.projections?.values?.agentPreset ?? item.agentPreset;
|
|
325
|
+
return preset ? { agentPreset: preset } : {};
|
|
326
|
+
}
|
|
327
|
+
function emptyUsageTotals() {
|
|
328
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
185
329
|
}
|
|
186
330
|
/** Map one DSH SessionEvent to display content events (+ title/usage side effects). */
|
|
187
331
|
function mapEvent(ev) {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OpencodeSessionReader: CLI-based session reader using `opencode session list`
|
|
2
|
+
* OpencodeSessionReader: CLI-based session reader using `opencode session list`
|
|
3
|
+
* and `opencode session export` (v2) / `opencode export` (v1).
|
|
3
4
|
*
|
|
4
5
|
* Design:
|
|
5
6
|
* - Uses official CLI commands instead of HTTP API.
|
|
6
7
|
* - `opencode session list --format json` returns sessions with `directory` field (first-class citizen).
|
|
7
|
-
* - `opencode export <sessionID>`
|
|
8
|
+
* - `opencode session export <sessionID>` (v2) or `opencode export <sessionID>`
|
|
9
|
+
* (v1) returns full session content.
|
|
8
10
|
* - Independent of runner (no setRunner circular dependency).
|
|
9
11
|
*
|
|
10
12
|
* Key features:
|
|
@@ -15,10 +17,12 @@
|
|
|
15
17
|
*/
|
|
16
18
|
import type { AgentSession, AgentSessionReader, SessionContent } from '../../runner/index.js';
|
|
17
19
|
interface OpencodeSessionReaderOptions {
|
|
20
|
+
/** Path to the opencode binary. Defaults to `opencode`. */
|
|
21
|
+
binary?: string;
|
|
18
22
|
/** Cache TTL for session list (ms). Default: 10_000. */
|
|
19
23
|
cacheTtlMs?: number;
|
|
20
24
|
/**
|
|
21
|
-
* Override the raw
|
|
25
|
+
* Override the raw export capture (returns the JSON string).
|
|
22
26
|
* Default routes stdout to a temp file fd to bypass opencode's ~128KB pipe
|
|
23
27
|
* truncation on large sessions. Injectable for testing.
|
|
24
28
|
*/
|
|
@@ -54,6 +58,12 @@ export declare class OpencodeSessionReader implements AgentSessionReader {
|
|
|
54
58
|
readSessionContent(sessionId: string, cwd: string, opts?: {
|
|
55
59
|
maxEvents?: number;
|
|
56
60
|
}): SessionContent;
|
|
61
|
+
/**
|
|
62
|
+
* v2 export: messages are flat (`type` + `text`/`content`) and assistant
|
|
63
|
+
* usage lives on the message instead of a `step-finish` part. Keep this
|
|
64
|
+
* mapping isolated from the v1 parser so v1 behaviour stays unchanged.
|
|
65
|
+
*/
|
|
66
|
+
private readV2SessionContent;
|
|
57
67
|
/**
|
|
58
68
|
* Check if a session is active (updated within 1 hour).
|
|
59
69
|
* Passes cwd to fetchSessionList so it returns sessions for the correct directory.
|
|
@@ -63,18 +73,19 @@ export declare class OpencodeSessionReader implements AgentSessionReader {
|
|
|
63
73
|
realpath(cwd: string): string;
|
|
64
74
|
private fetchSessionList;
|
|
65
75
|
/**
|
|
66
|
-
* Capture raw
|
|
76
|
+
* Capture raw export output (JSON string). Injectable
|
|
67
77
|
* via the constructor; the default routes stdout to a temp file fd to bypass
|
|
68
78
|
* opencode's ~128KB pipe truncation on large sessions.
|
|
69
79
|
*/
|
|
70
80
|
/** 导出会话内容(public:测试直接调用,替代 as unknown as)。 */
|
|
71
81
|
captureExport(sessionId: string): string;
|
|
72
82
|
/**
|
|
73
|
-
* default transport: run
|
|
83
|
+
* default transport: run the version-appropriate export command with stdout
|
|
84
|
+
* pointed at a
|
|
74
85
|
* regular-file fd (NOT a pipe). opencode truncates piped stdout at ~128KB for
|
|
75
86
|
* large sessions (huge tool_results), breaking JSON.parse; a file fd receives
|
|
76
|
-
* the full output (verified: 1.3MB parses OK).
|
|
77
|
-
* stdout is
|
|
87
|
+
* the full output (verified: 1.3MB parses OK). spawnProcessSync returns null
|
|
88
|
+
* stdout when it is redirected to a file fd, so we read the file instead.
|
|
78
89
|
*/
|
|
79
90
|
private captureExportViaFileFd;
|
|
80
91
|
}
|