@agentex/agent 0.0.30 → 0.0.31
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 +44 -0
- package/README.md +71 -2
- package/dist/derived.d.ts.map +1 -1
- package/dist/derived.js +35 -0
- package/dist/derived.js.map +1 -1
- package/dist/history/index.d.ts +1 -1
- package/dist/history/index.d.ts.map +1 -1
- package/dist/history/types.d.ts +74 -1
- package/dist/history/types.d.ts.map +1 -1
- package/dist/history/types.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/providers/opencode/history.d.ts +22 -8
- package/dist/providers/opencode/history.d.ts.map +1 -1
- package/dist/providers/opencode/history.js +119 -18
- package/dist/providers/opencode/history.js.map +1 -1
- package/dist/providers/opencode/index.d.ts.map +1 -1
- package/dist/providers/opencode/index.js +17 -0
- package/dist/providers/opencode/index.js.map +1 -1
- package/dist/providers/opencode/probe.d.ts.map +1 -1
- package/dist/providers/opencode/probe.js +2 -0
- package/dist/providers/opencode/probe.js.map +1 -1
- package/dist/providers/opencode/saved-history.d.ts +20 -0
- package/dist/providers/opencode/saved-history.d.ts.map +1 -0
- package/dist/providers/opencode/saved-history.js +361 -0
- package/dist/providers/opencode/saved-history.js.map +1 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/derived.ts +39 -0
- package/src/history/index.ts +10 -0
- package/src/history/types.ts +90 -1
- package/src/index.ts +11 -1
- package/src/providers/opencode/history.ts +156 -23
- package/src/providers/opencode/index.ts +17 -0
- package/src/providers/opencode/probe.ts +4 -0
- package/src/providers/opencode/saved-history.ts +467 -0
- package/src/types.ts +4 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SavedHistoryDiscoverOptions,
|
|
3
|
+
SavedHistoryOps,
|
|
4
|
+
SavedHistoryProbeOptions,
|
|
5
|
+
SavedHistoryReadOptions,
|
|
6
|
+
SavedHistorySession,
|
|
7
|
+
SavedHistoryYield,
|
|
8
|
+
} from "../../history/types.js";
|
|
9
|
+
import { cleanTitle } from "../../history/fs.js";
|
|
10
|
+
import type { OpenCodeClient } from "./client.js";
|
|
11
|
+
import { collectHistory, historicalEvents, type MessageEnvelope } from "./history.js";
|
|
12
|
+
import { acquireOpenCodeRuntime } from "./runtime.js";
|
|
13
|
+
|
|
14
|
+
const SESSION_PAGE_SIZE = 100;
|
|
15
|
+
const MAX_SESSION_PAGES = 100;
|
|
16
|
+
const MAX_SESSIONS = 10_000;
|
|
17
|
+
const MAX_SESSION_LIST_BYTES = 25 * 1024 * 1024;
|
|
18
|
+
const MESSAGE_SCAN_PAGE_SIZE = 100;
|
|
19
|
+
const MAX_MESSAGE_SCAN_PAGES = 100;
|
|
20
|
+
const MAX_MESSAGE_SCAN_BYTES = 25 * 1024 * 1024;
|
|
21
|
+
const MAX_DISCOVERY_INSPECTION_BYTES = 25 * 1024 * 1024;
|
|
22
|
+
const MAX_DISCOVERY_INSPECTION_MESSAGES = 10_000;
|
|
23
|
+
const INSPECTION_CONCURRENCY = 8;
|
|
24
|
+
|
|
25
|
+
type SessionListEndpoint = "experimental" | "legacy";
|
|
26
|
+
|
|
27
|
+
interface OpenCodeSessionEnvelope extends Record<string, unknown> {
|
|
28
|
+
id?: unknown;
|
|
29
|
+
directory?: unknown;
|
|
30
|
+
title?: unknown;
|
|
31
|
+
parentID?: unknown;
|
|
32
|
+
time?: unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface SessionPage {
|
|
36
|
+
endpoint: SessionListEndpoint;
|
|
37
|
+
sessions: OpenCodeSessionEnvelope[];
|
|
38
|
+
nextCursor: string | null;
|
|
39
|
+
bytes: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface InspectionBudget {
|
|
43
|
+
bytes: number;
|
|
44
|
+
messages: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class OpenCodeSavedHistoryDiscoveryLimitError extends Error {
|
|
48
|
+
readonly code = "history_discovery_limit";
|
|
49
|
+
|
|
50
|
+
constructor() {
|
|
51
|
+
super("OpenCode saved-session discovery exceeded the bounded import limit");
|
|
52
|
+
this.name = "OpenCodeSavedHistoryDiscoveryLimitError";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class OpenCodeSavedHistoryInvalidSessionError extends Error {
|
|
57
|
+
readonly code = "invalid_session";
|
|
58
|
+
|
|
59
|
+
constructor(message = "Invalid OpenCode saved-history session") {
|
|
60
|
+
super(message);
|
|
61
|
+
this.name = "OpenCodeSavedHistoryInvalidSessionError";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class OpenCodeSavedHistoryProtocolError extends Error {
|
|
66
|
+
readonly code = "history_protocol_error";
|
|
67
|
+
|
|
68
|
+
constructor(message: string) {
|
|
69
|
+
super(message);
|
|
70
|
+
this.name = "OpenCodeSavedHistoryProtocolError";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function rec(value: unknown): Record<string, unknown> | null {
|
|
75
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
76
|
+
? value as Record<string, unknown>
|
|
77
|
+
: null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function string(value: unknown): string | null {
|
|
81
|
+
return typeof value === "string" && value.trim().length > 0 ? value : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function millis(value: unknown): number | null {
|
|
85
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isoMillis(value: unknown): string | null {
|
|
89
|
+
const timestamp = millis(value);
|
|
90
|
+
if (timestamp === null) return null;
|
|
91
|
+
const date = new Date(timestamp);
|
|
92
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function normalizedLimit(limit: number | undefined): number | null {
|
|
96
|
+
if (limit === undefined) return null;
|
|
97
|
+
if (!Number.isFinite(limit)) return null;
|
|
98
|
+
return Math.max(0, Math.floor(limit));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function parseResponseArray(text: string, description: string): unknown[] {
|
|
102
|
+
let parsed: unknown;
|
|
103
|
+
try {
|
|
104
|
+
parsed = JSON.parse(text) as unknown;
|
|
105
|
+
} catch {
|
|
106
|
+
throw new OpenCodeSavedHistoryProtocolError(
|
|
107
|
+
`OpenCode ${description} response is not valid JSON`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
if (!Array.isArray(parsed)) {
|
|
111
|
+
throw new OpenCodeSavedHistoryProtocolError(
|
|
112
|
+
`OpenCode ${description} response is not an array`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
return parsed;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function sessionQuery(
|
|
119
|
+
options: SavedHistoryDiscoverOptions | SavedHistoryProbeOptions,
|
|
120
|
+
limit: number,
|
|
121
|
+
endpoint: SessionListEndpoint,
|
|
122
|
+
cursor?: string,
|
|
123
|
+
discovery = false,
|
|
124
|
+
): URLSearchParams {
|
|
125
|
+
const query = new URLSearchParams({ limit: String(limit) });
|
|
126
|
+
const discover = options as SavedHistoryDiscoverOptions;
|
|
127
|
+
if (discovery && (discover.mainSessionsOnly ?? true)) {
|
|
128
|
+
query.set("roots", "true");
|
|
129
|
+
}
|
|
130
|
+
if (discovery && discover.directory) query.set("directory", discover.directory);
|
|
131
|
+
if (discovery && endpoint === "experimental" && (discover.includeArchived ?? true)) {
|
|
132
|
+
query.set("archived", "true");
|
|
133
|
+
}
|
|
134
|
+
if (endpoint === "experimental" && cursor) query.set("cursor", cursor);
|
|
135
|
+
return query;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function parseSessionPage(
|
|
139
|
+
response: Response,
|
|
140
|
+
endpoint: SessionListEndpoint,
|
|
141
|
+
): Promise<SessionPage> {
|
|
142
|
+
if (!response.ok) {
|
|
143
|
+
throw new Error(`OpenCode saved-session request failed (${response.status})`);
|
|
144
|
+
}
|
|
145
|
+
const text = await response.text();
|
|
146
|
+
const parsed = parseResponseArray(text, "saved-session");
|
|
147
|
+
return {
|
|
148
|
+
endpoint,
|
|
149
|
+
sessions: parsed.map((value) => rec(value)).filter((value): value is OpenCodeSessionEnvelope => !!value),
|
|
150
|
+
nextCursor: endpoint === "experimental" ? response.headers.get("x-next-cursor") : null,
|
|
151
|
+
bytes: Buffer.byteLength(text),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function firstSessionPage(
|
|
156
|
+
client: OpenCodeClient,
|
|
157
|
+
options: SavedHistoryDiscoverOptions | SavedHistoryProbeOptions,
|
|
158
|
+
limit: number,
|
|
159
|
+
discovery = false,
|
|
160
|
+
): Promise<SessionPage> {
|
|
161
|
+
const experimentalQuery = sessionQuery(options, limit, "experimental", undefined, discovery);
|
|
162
|
+
const experimental = await client.request(`/experimental/session?${experimentalQuery}`);
|
|
163
|
+
if (experimental.status !== 404) return parseSessionPage(experimental, "experimental");
|
|
164
|
+
|
|
165
|
+
// OpenCode versions before the global endpoint exposed GET /session as a
|
|
166
|
+
// global list. It has no cursor or archived-session switch, but remains a
|
|
167
|
+
// useful compatibility path for active sessions.
|
|
168
|
+
const legacyQuery = sessionQuery(
|
|
169
|
+
options,
|
|
170
|
+
discovery ? MAX_SESSIONS : limit,
|
|
171
|
+
"legacy",
|
|
172
|
+
undefined,
|
|
173
|
+
discovery,
|
|
174
|
+
);
|
|
175
|
+
return parseSessionPage(await client.request(`/session?${legacyQuery}`), "legacy");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function nextSessionPage(
|
|
179
|
+
client: OpenCodeClient,
|
|
180
|
+
options: SavedHistoryDiscoverOptions,
|
|
181
|
+
endpoint: SessionListEndpoint,
|
|
182
|
+
cursor: string,
|
|
183
|
+
): Promise<SessionPage> {
|
|
184
|
+
const query = sessionQuery(options, SESSION_PAGE_SIZE, endpoint, cursor, true);
|
|
185
|
+
return parseSessionPage(
|
|
186
|
+
await client.request(`/${endpoint === "experimental" ? "experimental/" : ""}session?${query}`),
|
|
187
|
+
endpoint,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function findUserText(
|
|
192
|
+
client: OpenCodeClient,
|
|
193
|
+
sessionId: string,
|
|
194
|
+
budget: InspectionBudget,
|
|
195
|
+
): Promise<string | null> {
|
|
196
|
+
let before: string | null = null;
|
|
197
|
+
let sessionBytes = 0;
|
|
198
|
+
for (let pageIndex = 0; pageIndex < MAX_MESSAGE_SCAN_PAGES; pageIndex += 1) {
|
|
199
|
+
const query = new URLSearchParams({ limit: String(MESSAGE_SCAN_PAGE_SIZE) });
|
|
200
|
+
if (before) query.set("before", before);
|
|
201
|
+
const response = await client.request(
|
|
202
|
+
`/session/${encodeURIComponent(sessionId)}/message?${query}`,
|
|
203
|
+
);
|
|
204
|
+
if (response.status === 404) return null;
|
|
205
|
+
if (!response.ok) {
|
|
206
|
+
throw new Error(`OpenCode saved-session message request failed (${response.status})`);
|
|
207
|
+
}
|
|
208
|
+
const read = await readInspectionText(response, sessionBytes, budget);
|
|
209
|
+
const text = read.text;
|
|
210
|
+
sessionBytes = read.sessionBytes;
|
|
211
|
+
const parsed = parseResponseArray(text, "saved-session message");
|
|
212
|
+
const messages = parsed
|
|
213
|
+
.map((value) => rec(value) as MessageEnvelope)
|
|
214
|
+
.filter(Boolean);
|
|
215
|
+
budget.messages += messages.length;
|
|
216
|
+
if (budget.messages > MAX_DISCOVERY_INSPECTION_MESSAGES) {
|
|
217
|
+
throw new OpenCodeSavedHistoryDiscoveryLimitError();
|
|
218
|
+
}
|
|
219
|
+
for (const message of messages) {
|
|
220
|
+
const user = historicalEvents(message, sessionId, { includeUserMessages: true })
|
|
221
|
+
.find((item) => item.event.type === "user");
|
|
222
|
+
if (user?.event.type === "user") return user.event.text;
|
|
223
|
+
}
|
|
224
|
+
if (messages.length < MESSAGE_SCAN_PAGE_SIZE) return null;
|
|
225
|
+
const next = response.headers.get("x-next-cursor");
|
|
226
|
+
if (!next || next === before) return null;
|
|
227
|
+
before = next;
|
|
228
|
+
}
|
|
229
|
+
throw new OpenCodeSavedHistoryDiscoveryLimitError();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function readInspectionText(
|
|
233
|
+
response: Response,
|
|
234
|
+
priorSessionBytes: number,
|
|
235
|
+
budget: InspectionBudget,
|
|
236
|
+
): Promise<{ text: string; sessionBytes: number }> {
|
|
237
|
+
const reader = response.body?.getReader();
|
|
238
|
+
if (!reader) {
|
|
239
|
+
const text = await response.text();
|
|
240
|
+
const size = Buffer.byteLength(text);
|
|
241
|
+
const sessionBytes = priorSessionBytes + size;
|
|
242
|
+
budget.bytes += size;
|
|
243
|
+
if (
|
|
244
|
+
sessionBytes > MAX_MESSAGE_SCAN_BYTES
|
|
245
|
+
|| budget.bytes > MAX_DISCOVERY_INSPECTION_BYTES
|
|
246
|
+
) {
|
|
247
|
+
throw new OpenCodeSavedHistoryDiscoveryLimitError();
|
|
248
|
+
}
|
|
249
|
+
return { text, sessionBytes };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const chunks: Uint8Array[] = [];
|
|
253
|
+
let total = 0;
|
|
254
|
+
let sessionBytes = priorSessionBytes;
|
|
255
|
+
while (true) {
|
|
256
|
+
const { done, value } = await reader.read();
|
|
257
|
+
if (done) break;
|
|
258
|
+
const size = value.byteLength;
|
|
259
|
+
total += size;
|
|
260
|
+
sessionBytes += size;
|
|
261
|
+
budget.bytes += size;
|
|
262
|
+
if (
|
|
263
|
+
sessionBytes > MAX_MESSAGE_SCAN_BYTES
|
|
264
|
+
|| budget.bytes > MAX_DISCOVERY_INSPECTION_BYTES
|
|
265
|
+
) {
|
|
266
|
+
await reader.cancel().catch(() => undefined);
|
|
267
|
+
throw new OpenCodeSavedHistoryDiscoveryLimitError();
|
|
268
|
+
}
|
|
269
|
+
chunks.push(value);
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
text: Buffer.concat(chunks, total).toString("utf8"),
|
|
273
|
+
sessionBytes,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function inspectSession(
|
|
278
|
+
client: OpenCodeClient,
|
|
279
|
+
raw: OpenCodeSessionEnvelope,
|
|
280
|
+
options: SavedHistoryDiscoverOptions,
|
|
281
|
+
budget: InspectionBudget,
|
|
282
|
+
): Promise<SavedHistorySession | null> {
|
|
283
|
+
const externalSessionId = string(raw.id);
|
|
284
|
+
const time = rec(raw.time);
|
|
285
|
+
const updatedAt = isoMillis(time?.["updated"]);
|
|
286
|
+
if (!externalSessionId || !updatedAt) return null;
|
|
287
|
+
if ((options.mainSessionsOnly ?? true) && string(raw.parentID)) return null;
|
|
288
|
+
|
|
289
|
+
const userText = await findUserText(client, externalSessionId, budget);
|
|
290
|
+
const hasUserMessage = userText !== null;
|
|
291
|
+
if ((options.requireUserMessage ?? true) && !hasUserMessage) return null;
|
|
292
|
+
const archived = millis(time?.["archived"]);
|
|
293
|
+
return {
|
|
294
|
+
version: 1,
|
|
295
|
+
providerType: "opencode",
|
|
296
|
+
externalSessionId,
|
|
297
|
+
cwd: string(raw.directory),
|
|
298
|
+
title: cleanTitle(string(raw.title)) ?? cleanTitle(userText),
|
|
299
|
+
startedAt: isoMillis(time?.["created"]),
|
|
300
|
+
updatedAt,
|
|
301
|
+
branch: null,
|
|
302
|
+
gitOriginUrl: null,
|
|
303
|
+
archiveState: archived === null ? "active" : "archived",
|
|
304
|
+
hasUserMessage,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function inspectBatch(
|
|
309
|
+
client: OpenCodeClient,
|
|
310
|
+
sessions: OpenCodeSessionEnvelope[],
|
|
311
|
+
options: SavedHistoryDiscoverOptions,
|
|
312
|
+
budget: InspectionBudget,
|
|
313
|
+
): Promise<Array<SavedHistorySession | null>> {
|
|
314
|
+
const output = new Array<SavedHistorySession | null>(sessions.length).fill(null);
|
|
315
|
+
const failures: unknown[] = [];
|
|
316
|
+
let next = 0;
|
|
317
|
+
const workers = Array.from(
|
|
318
|
+
{ length: Math.min(INSPECTION_CONCURRENCY, sessions.length) },
|
|
319
|
+
async () => {
|
|
320
|
+
while (true) {
|
|
321
|
+
if (failures.length > 0) return;
|
|
322
|
+
const index = next;
|
|
323
|
+
next += 1;
|
|
324
|
+
if (index >= sessions.length) return;
|
|
325
|
+
const session = sessions[index];
|
|
326
|
+
if (!session) continue;
|
|
327
|
+
try {
|
|
328
|
+
output[index] = await inspectSession(client, session, options, budget);
|
|
329
|
+
} catch (error) {
|
|
330
|
+
// A 404 is converted to null inside findUserText because that one
|
|
331
|
+
// candidate disappeared. Any thrown error is systemic or makes the
|
|
332
|
+
// scan incomplete, so discovery must not report a partial catalog
|
|
333
|
+
// as authoritative to a synchronizing host.
|
|
334
|
+
failures.push(error);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
);
|
|
340
|
+
await Promise.all(workers);
|
|
341
|
+
if (failures.length > 0) throw failures[0];
|
|
342
|
+
return output;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** @internal Exported for deterministic provider contract tests. */
|
|
346
|
+
export async function* discoverOpenCodeSavedSessions(
|
|
347
|
+
client: OpenCodeClient,
|
|
348
|
+
options: SavedHistoryDiscoverOptions = {},
|
|
349
|
+
): AsyncIterable<SavedHistorySession> {
|
|
350
|
+
const requestedLimit = normalizedLimit(options.limit);
|
|
351
|
+
if (requestedLimit === 0) return;
|
|
352
|
+
|
|
353
|
+
let page = await firstSessionPage(client, options, SESSION_PAGE_SIZE, true);
|
|
354
|
+
let totalSessions = 0;
|
|
355
|
+
let totalBytes = 0;
|
|
356
|
+
let yielded = 0;
|
|
357
|
+
let pageCount = 0;
|
|
358
|
+
let previousCursor: string | null = null;
|
|
359
|
+
const inspectionBudget: InspectionBudget = { bytes: 0, messages: 0 };
|
|
360
|
+
while (true) {
|
|
361
|
+
pageCount += 1;
|
|
362
|
+
totalSessions += page.sessions.length;
|
|
363
|
+
totalBytes += page.bytes;
|
|
364
|
+
if (
|
|
365
|
+
totalSessions > MAX_SESSIONS
|
|
366
|
+
|| totalBytes > MAX_SESSION_LIST_BYTES
|
|
367
|
+
|| pageCount > MAX_SESSION_PAGES
|
|
368
|
+
) {
|
|
369
|
+
throw new OpenCodeSavedHistoryDiscoveryLimitError();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const inspected = await inspectBatch(client, page.sessions, options, inspectionBudget);
|
|
373
|
+
for (const session of inspected) {
|
|
374
|
+
if (!session) continue;
|
|
375
|
+
yield session;
|
|
376
|
+
yielded += 1;
|
|
377
|
+
if (requestedLimit !== null && yielded >= requestedLimit) return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (page.endpoint === "legacy") {
|
|
381
|
+
if (
|
|
382
|
+
page.sessions.length >= MAX_SESSIONS
|
|
383
|
+
&& (requestedLimit === null || yielded < requestedLimit)
|
|
384
|
+
) {
|
|
385
|
+
throw new OpenCodeSavedHistoryDiscoveryLimitError();
|
|
386
|
+
}
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (!page.nextCursor) return;
|
|
390
|
+
if (page.nextCursor === previousCursor) throw new OpenCodeSavedHistoryDiscoveryLimitError();
|
|
391
|
+
previousCursor = page.nextCursor;
|
|
392
|
+
page = await nextSessionPage(client, options, page.endpoint, page.nextCursor);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** @internal Exported for deterministic provider contract tests. */
|
|
397
|
+
export async function* readOpenCodeSavedSession(
|
|
398
|
+
client: OpenCodeClient,
|
|
399
|
+
session: SavedHistorySession,
|
|
400
|
+
options: SavedHistoryReadOptions = {},
|
|
401
|
+
): AsyncIterable<SavedHistoryYield> {
|
|
402
|
+
if (session.providerType !== "opencode" || !session.externalSessionId) {
|
|
403
|
+
throw new OpenCodeSavedHistoryInvalidSessionError();
|
|
404
|
+
}
|
|
405
|
+
const collected = await collectHistory(
|
|
406
|
+
client,
|
|
407
|
+
session.externalSessionId,
|
|
408
|
+
{ after: options.after, mode: options.mode },
|
|
409
|
+
{ includeUserMessages: true, missingSession: "error" },
|
|
410
|
+
);
|
|
411
|
+
for (const item of collected) {
|
|
412
|
+
const eventId = item.eventId;
|
|
413
|
+
if (!eventId) throw new OpenCodeSavedHistoryInvalidSessionError("OpenCode history event has no stable id");
|
|
414
|
+
yield {
|
|
415
|
+
event: { ...item.event, eventId },
|
|
416
|
+
checkpoint: item.checkpoint,
|
|
417
|
+
eventId,
|
|
418
|
+
partIndex: item.partIndex,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export const openCodeSavedHistory: SavedHistoryOps = {
|
|
424
|
+
async probe(options: SavedHistoryProbeOptions = {}) {
|
|
425
|
+
let runtime: Awaited<ReturnType<typeof acquireOpenCodeRuntime>> | null = null;
|
|
426
|
+
try {
|
|
427
|
+
runtime = await acquireOpenCodeRuntime(options);
|
|
428
|
+
const limit = Math.max(1, normalizedLimit(options.limit) ?? SESSION_PAGE_SIZE);
|
|
429
|
+
const page = await firstSessionPage(runtime.server.client, options, limit);
|
|
430
|
+
return {
|
|
431
|
+
providerType: "opencode",
|
|
432
|
+
sourceAvailable: true,
|
|
433
|
+
historyAvailable: page.sessions.length > 0,
|
|
434
|
+
approximateCount: page.sessions.length,
|
|
435
|
+
};
|
|
436
|
+
} catch {
|
|
437
|
+
return {
|
|
438
|
+
providerType: "opencode",
|
|
439
|
+
sourceAvailable: false,
|
|
440
|
+
historyAvailable: false,
|
|
441
|
+
};
|
|
442
|
+
} finally {
|
|
443
|
+
runtime?.server.release();
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
|
|
447
|
+
async *discover(options: SavedHistoryDiscoverOptions = {}) {
|
|
448
|
+
const runtime = await acquireOpenCodeRuntime(options);
|
|
449
|
+
try {
|
|
450
|
+
yield* discoverOpenCodeSavedSessions(runtime.server.client, options);
|
|
451
|
+
} finally {
|
|
452
|
+
runtime.server.release();
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
|
|
456
|
+
async *read(session, options: SavedHistoryReadOptions = {}) {
|
|
457
|
+
const runtime = await acquireOpenCodeRuntime({
|
|
458
|
+
...options,
|
|
459
|
+
cwd: session.cwd ?? options.cwd,
|
|
460
|
+
});
|
|
461
|
+
try {
|
|
462
|
+
yield* readOpenCodeSavedSession(runtime.server.client, session, options);
|
|
463
|
+
} finally {
|
|
464
|
+
runtime.server.release();
|
|
465
|
+
}
|
|
466
|
+
},
|
|
467
|
+
};
|
package/src/types.ts
CHANGED
|
@@ -97,6 +97,8 @@ export interface ProviderCapabilities {
|
|
|
97
97
|
durableHistory?: boolean;
|
|
98
98
|
/** Read-only discovery and import of locally persisted sessions. */
|
|
99
99
|
localHistory?: boolean;
|
|
100
|
+
/** Provider-neutral discovery and synchronization of saved sessions. */
|
|
101
|
+
savedHistory?: boolean;
|
|
100
102
|
/** A persisted provider session can be resumed. */
|
|
101
103
|
resume?: boolean;
|
|
102
104
|
/** Models expose provider-native variants independently from effort. */
|
|
@@ -372,6 +374,8 @@ export interface ProviderModule {
|
|
|
372
374
|
attachHistory?(record: SessionRecord, opts?: AttachOptions): Promise<HistoryAttachment>;
|
|
373
375
|
/** Discover and read local sessions when their ids are not already known. */
|
|
374
376
|
localHistory?: import("./history/types.js").LocalHistoryOps;
|
|
377
|
+
/** Discover and read provider-owned saved sessions without storage coupling. */
|
|
378
|
+
savedHistory?: import("./history/types.js").SavedHistoryOps;
|
|
375
379
|
}
|
|
376
380
|
|
|
377
381
|
// Result of looking up a transcript for a given session.
|