opencode-codex-memory 0.6.5 → 0.7.1
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/README.md +26 -28
- package/dist/opencode.json +1 -1
- package/dist/src/citation.d.ts +9 -0
- package/dist/src/citation.js +68 -11
- package/dist/src/db.js +10 -0
- package/dist/src/host-client.d.ts +1 -0
- package/dist/src/host-client.js +1 -0
- package/dist/src/index.d.ts +12 -2
- package/dist/src/index.js +32 -5
- package/dist/src/llm.d.ts +6 -0
- package/dist/src/llm.js +21 -10
- package/dist/src/phase2.d.ts +2 -0
- package/dist/src/phase2.js +1 -1
- package/dist/src/rollout-input.d.ts +6 -0
- package/dist/src/rollout-input.js +111 -0
- package/dist/src/store.d.ts +17 -1
- package/dist/src/store.js +90 -4
- package/dist/src/v2/agents.d.ts +53 -0
- package/dist/src/v2/agents.js +204 -0
- package/dist/src/v2/citation-overlay.d.ts +7 -0
- package/dist/src/v2/citation-overlay.js +52 -0
- package/dist/src/v2/index.d.ts +7 -0
- package/dist/src/v2/index.js +10 -0
- package/dist/src/v2/injection.d.ts +14 -0
- package/dist/src/v2/injection.js +19 -0
- package/dist/src/v2/plugin.d.ts +7 -0
- package/dist/src/v2/plugin.js +482 -0
- package/dist/src/v2/service.d.ts +78 -0
- package/dist/src/v2/service.js +195 -0
- package/dist/src/v2/shim.d.ts +47 -0
- package/dist/src/v2/shim.js +591 -0
- package/dist/src/v2/status-rpc.d.ts +197 -0
- package/dist/src/v2/status-rpc.js +159 -0
- package/dist/src/v2/status.d.ts +3 -0
- package/dist/src/v2/status.js +83 -0
- package/dist/src/v2/tools.d.ts +33 -0
- package/dist/src/v2/tools.js +57 -0
- package/dist/src/v2/tui.d.ts +3 -0
- package/dist/src/v2/tui.js +750 -0
- package/opencode.json +1 -1
- package/package.json +38 -2
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
import { memoryRoot } from "../paths.js";
|
|
2
|
+
import { invalidateOwnService, lastServiceFailure, ownServiceClient } from "./service.js";
|
|
3
|
+
let v2ctx = null;
|
|
4
|
+
export function setV2Context(ctx) {
|
|
5
|
+
v2ctx = ctx;
|
|
6
|
+
}
|
|
7
|
+
function ctx() {
|
|
8
|
+
if (!v2ctx)
|
|
9
|
+
throw new Error("v2 context not initialized");
|
|
10
|
+
return v2ctx;
|
|
11
|
+
}
|
|
12
|
+
/** Unwrap {data} vs direct payloads (client shape varies by call). */
|
|
13
|
+
function und(v) {
|
|
14
|
+
const r = v;
|
|
15
|
+
if (r && typeof r === "object" && "data" in r)
|
|
16
|
+
return r.data;
|
|
17
|
+
return v;
|
|
18
|
+
}
|
|
19
|
+
function isNotFoundError(e) {
|
|
20
|
+
if (!e || typeof e !== "object")
|
|
21
|
+
return String(e ?? "").includes("404");
|
|
22
|
+
const r = e;
|
|
23
|
+
if (r.status === 404)
|
|
24
|
+
return true;
|
|
25
|
+
for (const f of [r._tag, r.name, r.message]) {
|
|
26
|
+
if (typeof f === "string" && /notfound|404/i.test(f))
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
async function sessionGoneOnService(client, sessionID) {
|
|
32
|
+
if (typeof client.session.get !== "function")
|
|
33
|
+
return false;
|
|
34
|
+
try {
|
|
35
|
+
const info = await client.session.get({ sessionID });
|
|
36
|
+
if (info?.error && isNotFoundError(info.error)) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
const data = und(info);
|
|
40
|
+
return !data || typeof data !== "object";
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
return isNotFoundError(e);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Sub-session ids released via delete(): report 404 from get(). */
|
|
47
|
+
const releasedSubSessions = new Set();
|
|
48
|
+
const RELEASED_CAP = 500;
|
|
49
|
+
function markReleased(id) {
|
|
50
|
+
releasedSubSessions.add(id);
|
|
51
|
+
if (releasedSubSessions.size > RELEASED_CAP) {
|
|
52
|
+
const oldest = releasedSubSessions.values().next().value;
|
|
53
|
+
if (oldest !== undefined)
|
|
54
|
+
releasedSubSessions.delete(oldest);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function isReleasedSubSession(id) {
|
|
58
|
+
return releasedSubSessions.has(id);
|
|
59
|
+
}
|
|
60
|
+
/** Stable synthetic id for extraction helpers (see create below). */
|
|
61
|
+
export const EXTRACT_STUB_SESSION_ID = "codex-memory-extract-stub";
|
|
62
|
+
/** Test seam. */
|
|
63
|
+
export function resetV2ShimStateForTest() {
|
|
64
|
+
releasedSubSessions.clear();
|
|
65
|
+
invalidateOwnService();
|
|
66
|
+
}
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Shape adapters
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
function joinTextParts(content) {
|
|
71
|
+
if (!Array.isArray(content))
|
|
72
|
+
return "";
|
|
73
|
+
const out = [];
|
|
74
|
+
for (const p of content) {
|
|
75
|
+
if (p && typeof p === "object" && p.type === "text" && typeof p.text === "string") {
|
|
76
|
+
out.push(p.text);
|
|
77
|
+
}
|
|
78
|
+
else if (typeof p === "string") {
|
|
79
|
+
out.push(p);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return out.join("\n");
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* V2 public transcript messages → V1 session.messages rows
|
|
86
|
+
* ({info:{role}, parts:[...]}) consumed by capture.ts extractText.
|
|
87
|
+
*/
|
|
88
|
+
export function adaptV2Messages(msgs) {
|
|
89
|
+
if (!Array.isArray(msgs))
|
|
90
|
+
return [];
|
|
91
|
+
const rows = [];
|
|
92
|
+
for (const m of msgs) {
|
|
93
|
+
if (!m || typeof m !== "object")
|
|
94
|
+
continue;
|
|
95
|
+
if (m.type === "user") {
|
|
96
|
+
rows.push({ info: { role: "user" }, parts: [{ type: "text", text: m.text }] });
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (m.type === "system") {
|
|
100
|
+
rows.push({ info: { role: "system" }, parts: [{ type: "system", text: m.text }] });
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (m.type === "assistant") {
|
|
104
|
+
const parts = [];
|
|
105
|
+
for (const p of m.content ?? []) {
|
|
106
|
+
if (!p || typeof p !== "object")
|
|
107
|
+
continue;
|
|
108
|
+
if (p.type === "text")
|
|
109
|
+
parts.push({ type: "text", text: p.text });
|
|
110
|
+
else if (p.type === "reasoning")
|
|
111
|
+
parts.push({ type: "reasoning" });
|
|
112
|
+
else if (p.type === "tool") {
|
|
113
|
+
// The codemode `execute` wrapper runs code that calls the real
|
|
114
|
+
// tools; expand its toolCalls so the transcript keeps V1's
|
|
115
|
+
// per-tool granularity ([tool: name] input/output).
|
|
116
|
+
const calls = p.state?.metadata?.toolCalls ?? p?.metadata?.toolCalls;
|
|
117
|
+
if (p.name === "execute" && Array.isArray(calls) && calls.length > 0) {
|
|
118
|
+
const outputText = joinTextParts(p.state?.content);
|
|
119
|
+
for (const c of calls) {
|
|
120
|
+
parts.push({
|
|
121
|
+
type: "tool",
|
|
122
|
+
tool: c.tool ?? "execute",
|
|
123
|
+
state: {
|
|
124
|
+
input: c.input ?? p.state?.input,
|
|
125
|
+
...(outputText ? { output: outputText } : {}),
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
const st = p.state ?? {};
|
|
132
|
+
const outputText = joinTextParts(st.content);
|
|
133
|
+
parts.push({
|
|
134
|
+
type: "tool",
|
|
135
|
+
tool: p.name ?? "unknown",
|
|
136
|
+
state: {
|
|
137
|
+
...(st.input !== undefined ? { input: st.input } : {}),
|
|
138
|
+
...(outputText ? { output: outputText } : {}),
|
|
139
|
+
...(typeof st.error === "string" ? { error: st.error } : {}),
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
rows.push({ info: { role: "assistant" }, parts });
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
// synthetic/skill/shell/compaction/…: keep visible text, if any.
|
|
149
|
+
if (typeof m.text === "string") {
|
|
150
|
+
rows.push({ info: { role: m.type }, parts: [{ type: "text", text: m.text }] });
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
rows.push({ info: { role: m.type }, parts: [] });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return rows;
|
|
157
|
+
}
|
|
158
|
+
/** V2 catalog.model.list → V1 provider-list shape for catalogVariantKeys. */
|
|
159
|
+
export function adaptProviderCatalog(v2) {
|
|
160
|
+
const items = und(v2);
|
|
161
|
+
const list = Array.isArray(items) ? items : items?.data ?? [];
|
|
162
|
+
const providers = new Map();
|
|
163
|
+
for (const m of list) {
|
|
164
|
+
if (!m || typeof m !== "object")
|
|
165
|
+
continue;
|
|
166
|
+
const providerID = m.providerID;
|
|
167
|
+
const modelID = m.modelID ?? m.id;
|
|
168
|
+
if (typeof providerID !== "string" || typeof modelID !== "string")
|
|
169
|
+
continue;
|
|
170
|
+
if (!providers.has(providerID))
|
|
171
|
+
providers.set(providerID, {});
|
|
172
|
+
const variants = {};
|
|
173
|
+
for (const v of m.variants ?? []) {
|
|
174
|
+
if (v && typeof v === "object" && typeof v.id === "string") {
|
|
175
|
+
variants[v.id] = { ...(typeof v.disabled === "boolean" ? { disabled: v.disabled } : {}) };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
;
|
|
179
|
+
providers.get(providerID)[modelID] = { variants };
|
|
180
|
+
}
|
|
181
|
+
return { all: [...providers.entries()].map(([id, models]) => ({ id, models })) };
|
|
182
|
+
}
|
|
183
|
+
/** V2 mcp.list → V1 mcp.status map shape. */
|
|
184
|
+
export function adaptMcpStatus(v2) {
|
|
185
|
+
const items = und(v2);
|
|
186
|
+
const list = Array.isArray(items) ? items : [];
|
|
187
|
+
const out = {};
|
|
188
|
+
for (const s of list) {
|
|
189
|
+
if (!s || typeof s !== "object" || typeof s.name !== "string")
|
|
190
|
+
continue;
|
|
191
|
+
const st = s.status;
|
|
192
|
+
out[s.name] = { status: typeof st === "string" ? st : (st?.status ?? "unknown") };
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
function parseModelRef(ref) {
|
|
197
|
+
const slash = ref.indexOf("/");
|
|
198
|
+
if (slash <= 0 || slash === ref.length - 1)
|
|
199
|
+
return null;
|
|
200
|
+
return { providerID: ref.slice(0, slash), modelID: ref.slice(slash + 1) };
|
|
201
|
+
}
|
|
202
|
+
function responseRows(response) {
|
|
203
|
+
const payload = und(response);
|
|
204
|
+
if (Array.isArray(payload))
|
|
205
|
+
return payload;
|
|
206
|
+
if (payload && typeof payload === "object" && Array.isArray(payload.data)) {
|
|
207
|
+
return payload.data;
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
function responseNextCursor(response) {
|
|
212
|
+
const payload = und(response);
|
|
213
|
+
const direct = response?.cursor?.next;
|
|
214
|
+
const next = direct ?? payload?.cursor?.next;
|
|
215
|
+
return typeof next === "string" && next.length > 0 ? next : undefined;
|
|
216
|
+
}
|
|
217
|
+
function adaptV2SessionRow(row) {
|
|
218
|
+
if (!row || typeof row !== "object")
|
|
219
|
+
return row;
|
|
220
|
+
const record = row;
|
|
221
|
+
const location = record.location;
|
|
222
|
+
const directory = typeof record.directory === "string"
|
|
223
|
+
? record.directory
|
|
224
|
+
: location && typeof location === "object" && typeof location.directory === "string"
|
|
225
|
+
? location.directory
|
|
226
|
+
: undefined;
|
|
227
|
+
return directory === undefined ? row : { ...record, directory };
|
|
228
|
+
}
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// The façade: V1-shaped client over the V2 context
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
async function v2promptWithWait(sessionID, body, signal) {
|
|
233
|
+
const c = ctx();
|
|
234
|
+
const text = body.parts.map((p) => p.text ?? "").join("\n");
|
|
235
|
+
// Structured-output extraction: V2 prompts carry text only, so run the
|
|
236
|
+
// turn through generate.text (inherently tool-less, like the V1
|
|
237
|
+
// memorize-extract sandbox) with the system prompt prepended. The caller
|
|
238
|
+
// falls back to JSON text parsing (hostStructuredOutput finds nothing).
|
|
239
|
+
if (body.format) {
|
|
240
|
+
const prompt = body.system ? `${body.system}\n\n---\n\n${text}` : text;
|
|
241
|
+
const parsed = body.model ? parseModelRef(`${body.model.providerID}/${body.model.modelID}`) : null;
|
|
242
|
+
const payload = {
|
|
243
|
+
prompt,
|
|
244
|
+
...(parsed || body.variant
|
|
245
|
+
? {
|
|
246
|
+
model: {
|
|
247
|
+
...(parsed ? { providerID: parsed.providerID, id: parsed.modelID } : {}),
|
|
248
|
+
...(body.variant ? { variant: body.variant } : {}),
|
|
249
|
+
},
|
|
250
|
+
}
|
|
251
|
+
: {}),
|
|
252
|
+
};
|
|
253
|
+
if (signal?.aborted)
|
|
254
|
+
throw new Error("sub-agent prompt cancelled");
|
|
255
|
+
const publicClient = await ownServiceClient();
|
|
256
|
+
if (typeof publicClient?.generate?.text === "function") {
|
|
257
|
+
const gen = await publicClient.generate.text(payload, signal ? { signal } : undefined);
|
|
258
|
+
const outText = typeof gen?.text === "string" ? gen.text : JSON.stringify(gen);
|
|
259
|
+
return { data: { parts: [{ type: "text", text: outText }] } };
|
|
260
|
+
}
|
|
261
|
+
// ctx.generate.text ignores request-option signals. Race AbortSignal.
|
|
262
|
+
const genP = c.generate.text(payload);
|
|
263
|
+
const gen = signal
|
|
264
|
+
? await Promise.race([
|
|
265
|
+
genP,
|
|
266
|
+
new Promise((_, reject) => {
|
|
267
|
+
signal.addEventListener("abort", () => reject(new Error("sub-agent prompt cancelled")), { once: true });
|
|
268
|
+
}),
|
|
269
|
+
])
|
|
270
|
+
: await genP;
|
|
271
|
+
const outText = typeof gen?.text === "string" ? gen.text : JSON.stringify(gen);
|
|
272
|
+
return { data: { parts: [{ type: "text", text: outText }] } };
|
|
273
|
+
}
|
|
274
|
+
// Agentic turn (consolidation): the agent/model must be set at CREATE time
|
|
275
|
+
// in V2, so switch the fresh helper session first, then prompt + wait to
|
|
276
|
+
// preserve V1's "prompt resolves after the turn" semantics.
|
|
277
|
+
if (body.agent) {
|
|
278
|
+
await c.session.switchAgent({ sessionID, agent: body.agent });
|
|
279
|
+
}
|
|
280
|
+
if (body.model) {
|
|
281
|
+
await c.session.switchModel({
|
|
282
|
+
sessionID,
|
|
283
|
+
model: {
|
|
284
|
+
providerID: body.model.providerID,
|
|
285
|
+
id: body.model.modelID,
|
|
286
|
+
...(body.variant ? { variant: body.variant } : {}),
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
const posted = await c.session.prompt({ sessionID, text });
|
|
291
|
+
const waitP = c.session.wait({ sessionID });
|
|
292
|
+
if (signal) {
|
|
293
|
+
if (signal.aborted) {
|
|
294
|
+
await c.session.interrupt({ sessionID }).catch(() => { });
|
|
295
|
+
await waitP.catch(() => { });
|
|
296
|
+
throw new Error("sub-agent prompt cancelled");
|
|
297
|
+
}
|
|
298
|
+
await Promise.race([
|
|
299
|
+
waitP,
|
|
300
|
+
new Promise((_, reject) => {
|
|
301
|
+
signal.addEventListener("abort", () => reject(new Error("sub-agent prompt cancelled")), { once: true });
|
|
302
|
+
}),
|
|
303
|
+
]).catch(async (e) => {
|
|
304
|
+
await c.session.interrupt({ sessionID }).catch(() => { });
|
|
305
|
+
await waitP.catch(() => { });
|
|
306
|
+
throw e;
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
await waitP;
|
|
311
|
+
}
|
|
312
|
+
return { data: { posted } };
|
|
313
|
+
}
|
|
314
|
+
/** Build the V1-shaped client. Passed to setPluginInput() by V2 setup(). */
|
|
315
|
+
export function buildV1ClientShim() {
|
|
316
|
+
async function serviceOrThrow() {
|
|
317
|
+
const client = await ownServiceClient();
|
|
318
|
+
if (!client) {
|
|
319
|
+
throw new Error(lastServiceFailure() ?? "no healthy registered OpenCode 2 service for global memory operations");
|
|
320
|
+
}
|
|
321
|
+
return client;
|
|
322
|
+
}
|
|
323
|
+
async function paginateSessionList(listFn, limit, cursor, search) {
|
|
324
|
+
const pageSize = Math.min(Math.max(limit, 1), 5000);
|
|
325
|
+
const out = [];
|
|
326
|
+
const seenCursors = new Set();
|
|
327
|
+
const timestampCursor = typeof cursor === "number" ? cursor : undefined;
|
|
328
|
+
let next = typeof cursor === "string" ? cursor : undefined;
|
|
329
|
+
while (out.length < limit) {
|
|
330
|
+
const response = (await listFn({
|
|
331
|
+
limit: pageSize,
|
|
332
|
+
order: "desc",
|
|
333
|
+
parentID: null,
|
|
334
|
+
...(search ? { search } : {}),
|
|
335
|
+
...(next ? { cursor: next } : {}),
|
|
336
|
+
}));
|
|
337
|
+
const rawRows = responseRows(response);
|
|
338
|
+
if (!rawRows)
|
|
339
|
+
throw new Error("registered service returned an invalid session list");
|
|
340
|
+
const rows = rawRows
|
|
341
|
+
.map(adaptV2SessionRow)
|
|
342
|
+
.filter((row) => {
|
|
343
|
+
if (!row || typeof row !== "object")
|
|
344
|
+
return false;
|
|
345
|
+
const record = row;
|
|
346
|
+
const time = record.time;
|
|
347
|
+
const updated = time && typeof time === "object" ? time.updated : undefined;
|
|
348
|
+
if (timestampCursor !== undefined && (typeof updated !== "number" || updated >= timestampCursor))
|
|
349
|
+
return false;
|
|
350
|
+
if (!search)
|
|
351
|
+
return true;
|
|
352
|
+
const title = typeof record.title === "string" ? record.title : "";
|
|
353
|
+
const id = typeof record.id === "string" ? record.id : "";
|
|
354
|
+
return `${id}\n${title}`.toLowerCase().includes(search.toLowerCase());
|
|
355
|
+
});
|
|
356
|
+
out.push(...rows);
|
|
357
|
+
const candidate = responseNextCursor(response);
|
|
358
|
+
if (!candidate || seenCursors.has(candidate) || rawRows.length === 0)
|
|
359
|
+
break;
|
|
360
|
+
seenCursors.add(candidate);
|
|
361
|
+
next = candidate;
|
|
362
|
+
}
|
|
363
|
+
return { data: out.slice(0, limit) };
|
|
364
|
+
}
|
|
365
|
+
async function listGlobalSessions(limit, cursor, search) {
|
|
366
|
+
const localList = v2ctx ? v2ctx.session.list : undefined;
|
|
367
|
+
if (typeof localList === "function") {
|
|
368
|
+
return paginateSessionList((input) => localList(input), limit, cursor, search);
|
|
369
|
+
}
|
|
370
|
+
const client = await serviceOrThrow();
|
|
371
|
+
if (typeof client.session.list !== "function")
|
|
372
|
+
throw new Error("registered service does not support session.list");
|
|
373
|
+
return paginateSessionList((input) => client.session.list(input), limit, cursor, search);
|
|
374
|
+
}
|
|
375
|
+
const session = {
|
|
376
|
+
create: async (opts) => {
|
|
377
|
+
try {
|
|
378
|
+
// Extraction turns run through generate.text (see prompt below) and
|
|
379
|
+
// never touch a session, so hand out a stable synthetic id instead
|
|
380
|
+
// of creating a server row per extraction (without this each
|
|
381
|
+
// extraction would litter one dead
|
|
382
|
+
// `codex-memory-extract-*` session). Skip/tracking logic keys off
|
|
383
|
+
// the id string only, so behavior is unchanged.
|
|
384
|
+
if (opts?.body?.title?.startsWith("codex-memory-extract-")) {
|
|
385
|
+
releasedSubSessions.delete(EXTRACT_STUB_SESSION_ID);
|
|
386
|
+
return { data: { id: EXTRACT_STUB_SESSION_ID } };
|
|
387
|
+
}
|
|
388
|
+
const res = await ctx().session.create({
|
|
389
|
+
...(opts?.body?.title ? { title: opts.body.title } : {}),
|
|
390
|
+
...(opts?.body?.metadata ? { metadata: opts.body.metadata } : {}),
|
|
391
|
+
location: { directory: opts?.query?.directory ?? memoryRoot() },
|
|
392
|
+
});
|
|
393
|
+
return { data: { id: und(res)?.id } };
|
|
394
|
+
}
|
|
395
|
+
catch (e) {
|
|
396
|
+
return { error: e };
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
prompt: async (opts) => {
|
|
400
|
+
try {
|
|
401
|
+
return await v2promptWithWait(opts.path.id, opts.body, opts.signal);
|
|
402
|
+
}
|
|
403
|
+
catch (e) {
|
|
404
|
+
return { error: e };
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
messages: async (opts) => {
|
|
408
|
+
try {
|
|
409
|
+
if (isReleasedSubSession(opts.path.id))
|
|
410
|
+
throw Object.assign(new Error("SessionNotFound"), { _tag: "SessionNotFoundError" });
|
|
411
|
+
const client = await serviceOrThrow();
|
|
412
|
+
if (typeof client.message?.list !== "function")
|
|
413
|
+
throw new Error("registered service does not support message.list");
|
|
414
|
+
const messages = [];
|
|
415
|
+
const seenCursors = new Set();
|
|
416
|
+
let cursor;
|
|
417
|
+
while (true) {
|
|
418
|
+
const response = await client.message.list(cursor ? { sessionID: opts.path.id, cursor } : { sessionID: opts.path.id, order: "asc" });
|
|
419
|
+
const rows = responseRows(response);
|
|
420
|
+
if (!rows)
|
|
421
|
+
throw new Error("registered service returned an invalid message list");
|
|
422
|
+
messages.push(...rows);
|
|
423
|
+
const next = responseNextCursor(response);
|
|
424
|
+
if (!next || seenCursors.has(next))
|
|
425
|
+
break;
|
|
426
|
+
seenCursors.add(next);
|
|
427
|
+
cursor = next;
|
|
428
|
+
}
|
|
429
|
+
return { data: adaptV2Messages(messages) };
|
|
430
|
+
}
|
|
431
|
+
catch (e) {
|
|
432
|
+
return { error: e };
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
delete: async (opts) => {
|
|
436
|
+
let shutdownError;
|
|
437
|
+
try {
|
|
438
|
+
const client = await serviceOrThrow();
|
|
439
|
+
if (typeof client.session.remove !== "function")
|
|
440
|
+
throw new Error("registered service does not support session.remove");
|
|
441
|
+
const result = await client.session.remove({ sessionID: opts.path.id }, opts.signal ? { signal: opts.signal } : undefined);
|
|
442
|
+
if (result?.error)
|
|
443
|
+
throw result.error;
|
|
444
|
+
if (await sessionGoneOnService(client, opts.path.id)) {
|
|
445
|
+
markReleased(opts.path.id);
|
|
446
|
+
return {};
|
|
447
|
+
}
|
|
448
|
+
throw new Error("session still exists after remove");
|
|
449
|
+
}
|
|
450
|
+
catch (error) {
|
|
451
|
+
shutdownError = error;
|
|
452
|
+
}
|
|
453
|
+
try {
|
|
454
|
+
const session = ctx().session;
|
|
455
|
+
const interrupt = await session.interrupt({ sessionID: opts.path.id });
|
|
456
|
+
if (interrupt?.error)
|
|
457
|
+
throw interrupt.error;
|
|
458
|
+
if (typeof session.wait === "function")
|
|
459
|
+
await session.wait({ sessionID: opts.path.id });
|
|
460
|
+
}
|
|
461
|
+
catch (interruptError) {
|
|
462
|
+
return { error: shutdownError ?? interruptError };
|
|
463
|
+
}
|
|
464
|
+
try {
|
|
465
|
+
const client = await ownServiceClient();
|
|
466
|
+
if (!client?.session?.get)
|
|
467
|
+
return { error: shutdownError ?? new Error("session still exists after interrupt") };
|
|
468
|
+
const info = await client.session.get({ sessionID: opts.path.id });
|
|
469
|
+
if (info?.error && isNotFoundError(info.error)) {
|
|
470
|
+
markReleased(opts.path.id);
|
|
471
|
+
return {};
|
|
472
|
+
}
|
|
473
|
+
const data = und(info);
|
|
474
|
+
if (data && typeof data === "object") {
|
|
475
|
+
return { error: shutdownError ?? new Error("session still exists after interrupt") };
|
|
476
|
+
}
|
|
477
|
+
return { error: shutdownError ?? new Error("session still exists after interrupt") };
|
|
478
|
+
}
|
|
479
|
+
catch (e) {
|
|
480
|
+
if (isNotFoundError(e)) {
|
|
481
|
+
markReleased(opts.path.id);
|
|
482
|
+
return {};
|
|
483
|
+
}
|
|
484
|
+
return { error: shutdownError ?? e };
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
get: async (opts) => {
|
|
488
|
+
try {
|
|
489
|
+
if (isReleasedSubSession(opts.path.id)) {
|
|
490
|
+
return { response: { status: 404 }, error: { _tag: "SessionNotFoundError" } };
|
|
491
|
+
}
|
|
492
|
+
const client = await serviceOrThrow();
|
|
493
|
+
const info = und(await client.session.get?.({ sessionID: opts.path.id }));
|
|
494
|
+
return { data: info };
|
|
495
|
+
}
|
|
496
|
+
catch (e) {
|
|
497
|
+
if (isNotFoundError(e))
|
|
498
|
+
return { response: { status: 404 }, error: e };
|
|
499
|
+
return { error: e };
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
abort: async (opts) => {
|
|
503
|
+
try {
|
|
504
|
+
const client = await serviceOrThrow();
|
|
505
|
+
if (typeof client.session.interrupt !== "function")
|
|
506
|
+
throw new Error("registered service does not support session.interrupt");
|
|
507
|
+
const result = await client.session.interrupt({ sessionID: opts.path.id });
|
|
508
|
+
if (!result?.error)
|
|
509
|
+
return {};
|
|
510
|
+
}
|
|
511
|
+
catch {
|
|
512
|
+
// Fall through to the context-local interrupt below.
|
|
513
|
+
}
|
|
514
|
+
try {
|
|
515
|
+
await ctx().session.interrupt({ sessionID: opts.path.id });
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
// Best-effort, mirrors V1.
|
|
519
|
+
}
|
|
520
|
+
return {};
|
|
521
|
+
},
|
|
522
|
+
};
|
|
523
|
+
const config = {
|
|
524
|
+
get: async () => {
|
|
525
|
+
try {
|
|
526
|
+
const client = await serviceOrThrow();
|
|
527
|
+
const response = await client.config?.get({ location: { directory: ctx().location.directory } });
|
|
528
|
+
const documents = und(response);
|
|
529
|
+
const candidates = Array.isArray(documents) ? documents : documents?.type === "document" ? [documents] : [];
|
|
530
|
+
let info = {};
|
|
531
|
+
let found = false;
|
|
532
|
+
for (const entry of candidates) {
|
|
533
|
+
if (!entry || typeof entry !== "object" || entry.type !== "document")
|
|
534
|
+
continue;
|
|
535
|
+
if (!entry.info || typeof entry.info !== "object" || Array.isArray(entry.info))
|
|
536
|
+
continue;
|
|
537
|
+
info = { ...info, ...entry.info };
|
|
538
|
+
found = true;
|
|
539
|
+
}
|
|
540
|
+
if (!found)
|
|
541
|
+
return { data: {} };
|
|
542
|
+
const model = info.model;
|
|
543
|
+
let normalizedModel = model;
|
|
544
|
+
const modelRecord = model && typeof model === "object" ? model : null;
|
|
545
|
+
if (typeof modelRecord?.providerID === "string" && typeof modelRecord.model === "string") {
|
|
546
|
+
normalizedModel = `${modelRecord.providerID}/${modelRecord.model}`;
|
|
547
|
+
}
|
|
548
|
+
return { data: { ...info, ...(normalizedModel !== undefined ? { model: normalizedModel } : {}) } };
|
|
549
|
+
}
|
|
550
|
+
catch (e) {
|
|
551
|
+
return { error: e };
|
|
552
|
+
}
|
|
553
|
+
},
|
|
554
|
+
};
|
|
555
|
+
const provider = {
|
|
556
|
+
list: async () => {
|
|
557
|
+
try {
|
|
558
|
+
const res = await ctx().catalog.model.list();
|
|
559
|
+
return { data: adaptProviderCatalog(res) };
|
|
560
|
+
}
|
|
561
|
+
catch (e) {
|
|
562
|
+
return { error: e };
|
|
563
|
+
}
|
|
564
|
+
},
|
|
565
|
+
};
|
|
566
|
+
const mcp = {
|
|
567
|
+
status: async () => {
|
|
568
|
+
try {
|
|
569
|
+
const res = await ctx().mcp.list();
|
|
570
|
+
return { data: adaptMcpStatus(res) };
|
|
571
|
+
}
|
|
572
|
+
catch (e) {
|
|
573
|
+
return { error: e };
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
};
|
|
577
|
+
const _client = {
|
|
578
|
+
get: async (opts) => {
|
|
579
|
+
if (opts.url === "/experimental/session") {
|
|
580
|
+
const q = opts.query ?? {};
|
|
581
|
+
const cursor = typeof q.cursor === "number" || typeof q.cursor === "string" ? q.cursor : undefined;
|
|
582
|
+
return listGlobalSessions(typeof q.limit === "number" ? q.limit : 5000, cursor, typeof q.search === "string" ? q.search : undefined);
|
|
583
|
+
}
|
|
584
|
+
if (opts.url === "/provider") {
|
|
585
|
+
return provider.list();
|
|
586
|
+
}
|
|
587
|
+
return { error: { message: `unsupported shim route ${opts.url}` } };
|
|
588
|
+
},
|
|
589
|
+
};
|
|
590
|
+
return { session, config, provider, mcp, _client };
|
|
591
|
+
}
|