antechamber 0.1.1 → 0.2.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/CHANGELOG.md +45 -0
- package/README.md +24 -14
- package/bin/opencode-antechamber +14 -1
- package/bin/opencode-pi +11 -1
- package/dist/approvals.d.ts +4 -1
- package/dist/approvals.js +51 -33
- package/dist/catalog.d.ts +1 -1
- package/dist/catalog.js +1 -1
- package/dist/commands/context.d.ts +0 -1
- package/dist/commands/context.js +1 -1
- package/dist/commands/doctor.d.ts +3 -2
- package/dist/commands/serve.d.ts +2 -1
- package/dist/commands/setup.d.ts +2 -3
- package/dist/commands/setup.js +16 -4
- package/dist/http.d.ts +8 -1
- package/dist/http.js +42 -19
- package/dist/pi-event-translation.d.ts +10 -6
- package/dist/pi-event-translation.js +237 -80
- package/dist/pi-rpc.d.ts +20 -2
- package/dist/pi-rpc.js +96 -29
- package/dist/prompt-parts.d.ts +40 -0
- package/dist/prompt-parts.js +157 -0
- package/dist/route-services.d.ts +45 -2
- package/dist/route-services.js +70 -3
- package/dist/routes-v1.js +113 -65
- package/dist/routes-v2.js +407 -95
- package/dist/server.d.ts +9 -1
- package/dist/server.js +109 -9
- package/dist/session-commands.d.ts +12 -15
- package/dist/session-commands.js +54 -13
- package/dist/session-diff.d.ts +1 -1
- package/dist/session-diff.js +10 -2
- package/dist/session-history.d.ts +1 -1
- package/dist/session-history.js +110 -44
- package/dist/session-hosts.js +2 -2
- package/dist/session-store.d.ts +8 -4
- package/dist/session-store.js +191 -38
- package/dist/session-tree.d.ts +10 -2
- package/dist/session-tree.js +55 -26
- package/dist/sessions.d.ts +32 -5
- package/dist/sessions.js +331 -120
- package/dist/sse.d.ts +16 -6
- package/dist/sse.js +77 -21
- package/dist/subagents.d.ts +5 -0
- package/dist/subagents.js +84 -71
- package/dist/tool-map.d.ts +3 -2
- package/dist/types.d.ts +152 -1
- package/dist/v2.d.ts +18 -4
- package/dist/v2.js +128 -44
- package/dist/vcs.d.ts +5 -0
- package/dist/vcs.js +199 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/extensions/omo-dag/README.md +2 -0
- package/extensions/omo-dag/docs/DESIGN.md +10 -1
- package/extensions/omo-dag/panel/index.html +1 -1
- package/extensions/omo-dag/panel/main.js +60 -60
- package/extensions/omo-dag/service/main.js +72 -8
- package/package.json +1 -1
package/dist/sessions.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { id, PiEventTranslator, syncSessionCost } from "./pi-event-translation.js";
|
|
6
6
|
import { parsePiSessionFile, parsePiSessionText } from "./session-history.js";
|
|
7
|
+
import { CONTEXT_MESSAGE_TYPE, nativePromptInput, v2PromptAttachments } from "./prompt-parts.js";
|
|
7
8
|
import { existsSync, readdirSync, statSync, unlinkSync } from "node:fs";
|
|
8
9
|
import { join } from "node:path";
|
|
9
10
|
import { COMPACT_RPC_TIMEOUT_MS, PiRpcClient, RPC_COMMAND_TIMEOUT_MS, } from "./pi-rpc.js";
|
|
@@ -11,15 +12,19 @@ import { stopSessionHosts } from "./session-hosts.js";
|
|
|
11
12
|
import { setThinkingLevel } from "./thinking-levels.js";
|
|
12
13
|
import { SessionLifecycle } from "./lifecycle.js";
|
|
13
14
|
import { ApprovalBridge, extensionUiRequestFrom } from "./approvals.js";
|
|
14
|
-
import { getAgentName } from "./agent-name.js";
|
|
15
|
+
import { getAgentName, getExposedAgentNames } from "./agent-name.js";
|
|
16
|
+
import { RequestBodyError } from "./http.js";
|
|
15
17
|
import { bindMessageEntries, durableShellTree, forkConversation, getEntries, navigate, readSessionTree, revertConversation, SessionTreeError, unrevertConversation, } from "./session-tree.js";
|
|
16
18
|
import { SubagentBridge } from "./subagents.js";
|
|
17
19
|
import { sessionDiff } from "./session-diff.js";
|
|
18
20
|
import { announceSessionNotice, slashTitlePrompt } from "./session-commands.js";
|
|
19
|
-
import { ocIdFor, piSessionDir, piSessionsRootMtime, scanAllPiSessions, scanPiSessions,
|
|
21
|
+
import { ocIdFor, piSessionDir, piSessionsRootMtime, scanAllPiSessions, scanPiSessions, SessionStore, readSessionTitle, } from "./session-store.js";
|
|
20
22
|
import { ADAPTER_VERSION } from "./version.js";
|
|
21
23
|
export { ADAPTER_VERSION } from "./version.js";
|
|
22
24
|
const SESSION_SCAN_CACHE_TTL_MS = 5_000;
|
|
25
|
+
const PI_FILE_RESOLVE_ATTEMPTS = 10;
|
|
26
|
+
const PI_FILE_RESOLVE_RETRY_MS = 200;
|
|
27
|
+
const SESSION_TITLE_MAX_CHARS = 60;
|
|
23
28
|
export class SessionManager {
|
|
24
29
|
emit;
|
|
25
30
|
defaultDirectory;
|
|
@@ -57,10 +62,14 @@ export class SessionManager {
|
|
|
57
62
|
persistMeta: (s) => this.persistMeta(s),
|
|
58
63
|
});
|
|
59
64
|
}
|
|
60
|
-
async create(directory, title, parentID) {
|
|
61
|
-
|
|
65
|
+
async create(directory, title, parentID, options = {}) {
|
|
66
|
+
if (options.agent !== undefined && !getExposedAgentNames().includes(options.agent))
|
|
67
|
+
throw new RequestBodyError(400, `unknown agent: ${options.agent}`);
|
|
68
|
+
return this.lifecycle.run(() => this.createLive(directory, title, parentID, options));
|
|
62
69
|
}
|
|
63
|
-
async createLive(directory, title, parentID) {
|
|
70
|
+
async createLive(directory, title, parentID, options = {}) {
|
|
71
|
+
if (options.id !== undefined && (options.id === "" || this.get(options.id) !== undefined))
|
|
72
|
+
throw new RequestBodyError(400, "session id must be nonempty and unused");
|
|
64
73
|
await this.lifecycle.makeRoom();
|
|
65
74
|
const pi = new PiRpcClient({ cwd: directory, clock: this.lifecycle.clock });
|
|
66
75
|
const state = await pi.command({ type: "get_state" }).catch(async (error) => {
|
|
@@ -78,12 +87,18 @@ export class SessionManager {
|
|
|
78
87
|
if (piSessionId !== undefined)
|
|
79
88
|
this.createdSessions.set(piSessionId, directory);
|
|
80
89
|
const info = {
|
|
81
|
-
id: piSessionId !== undefined ? ocIdFor(piSessionId) : id("ses"),
|
|
90
|
+
id: options.id ?? (piSessionId !== undefined ? ocIdFor(piSessionId) : id("ses")),
|
|
82
91
|
projectID: directory,
|
|
83
92
|
directory,
|
|
84
93
|
title: title?.trim() || "New session",
|
|
85
94
|
version: ADAPTER_VERSION,
|
|
86
95
|
time: { created: now, updated: now },
|
|
96
|
+
agent: options.agent ?? getAgentName(),
|
|
97
|
+
slug: piSessionId ?? id("session"),
|
|
98
|
+
path: "",
|
|
99
|
+
summary: { additions: 0, deletions: 0, files: 0 },
|
|
100
|
+
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
101
|
+
...(options.metadata !== undefined ? { metadata: options.metadata } : {}),
|
|
87
102
|
...(parentID !== undefined ? { parentID } : {}),
|
|
88
103
|
};
|
|
89
104
|
const managed = {
|
|
@@ -101,44 +116,73 @@ export class SessionManager {
|
|
|
101
116
|
...(piSessionId !== undefined ? { piSessionId } : {}),
|
|
102
117
|
...(state.data.thinkingLevel !== undefined ? { thinkingLevel: state.data.thinkingLevel } : {}),
|
|
103
118
|
};
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
119
|
+
let published = false;
|
|
120
|
+
try {
|
|
121
|
+
managed.approvals = this.approvalsFor(managed);
|
|
122
|
+
this.sessions.set(info.id, managed);
|
|
123
|
+
published = true;
|
|
124
|
+
this.subagents.wire(managed);
|
|
125
|
+
this.lifecycle.touch(managed);
|
|
126
|
+
this.wirePi(managed, pi);
|
|
127
|
+
if (options.model !== undefined)
|
|
128
|
+
await this.setModel(info.id, options.model);
|
|
129
|
+
this.emit(directory, { type: "session.created", properties: { info } });
|
|
130
|
+
// Announce pi's real model *after* session.created, so the client already
|
|
131
|
+
// knows the session when the switch marker lands in its transcript.
|
|
132
|
+
if (options.model === undefined)
|
|
133
|
+
this.applyPiModel(managed, state.data.model?.provider, state.data.model?.id);
|
|
134
|
+
// Bind pi's session file so the session survives adapter restarts. pi
|
|
135
|
+
// reports the path directly; fall back to a background scan for older pi
|
|
136
|
+
// versions, and re-bind after the first prompt (pi persists lazily).
|
|
137
|
+
if (typeof state.data.sessionFile === "string" && state.data.sessionFile !== "") {
|
|
138
|
+
managed.piPath = state.data.sessionFile;
|
|
139
|
+
this.persistMeta(managed);
|
|
140
|
+
}
|
|
141
|
+
else if (piSessionId !== undefined) {
|
|
142
|
+
void this.resolvePiFile(directory, piSessionId)
|
|
143
|
+
.then((piPath) => {
|
|
144
|
+
if (this.sessions.get(info.id) !== managed || piPath === undefined || managed.piPath !== undefined)
|
|
145
|
+
return;
|
|
124
146
|
managed.piPath = piPath;
|
|
125
147
|
this.persistMeta(managed);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (
|
|
133
|
-
|
|
148
|
+
})
|
|
149
|
+
.catch((error) => {
|
|
150
|
+
if (this.sessions.get(info.id) === managed)
|
|
151
|
+
this.reportPersistenceError(managed, error);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if (managed.titleSource === "user") {
|
|
155
|
+
const renamed = await pi.command({ type: "set_session_name", name: info.title });
|
|
156
|
+
if (!renamed.success)
|
|
157
|
+
throw new Error(renamed.error ?? "session rename rejected");
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
this.applyNativeTitle(managed, state.data.sessionName);
|
|
161
|
+
}
|
|
162
|
+
const reconciled = await pi.command({ type: "get_state" });
|
|
163
|
+
if (reconciled.success)
|
|
164
|
+
this.applyNativeTitle(managed, reconciled.data?.sessionName);
|
|
165
|
+
return info;
|
|
134
166
|
}
|
|
135
|
-
|
|
136
|
-
|
|
167
|
+
catch (error) {
|
|
168
|
+
if (published)
|
|
169
|
+
await this.rollbackCreate(managed, piSessionId);
|
|
170
|
+
else
|
|
171
|
+
await pi.dispose();
|
|
172
|
+
throw error;
|
|
137
173
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
174
|
+
}
|
|
175
|
+
async rollbackCreate(managed, piSessionId) {
|
|
176
|
+
this.sessions.delete(managed.info.id);
|
|
177
|
+
if (piSessionId !== undefined)
|
|
178
|
+
this.createdSessions.delete(piSessionId);
|
|
179
|
+
managed.unwirePi?.();
|
|
180
|
+
const pi = managed.pi;
|
|
181
|
+
managed.pi = undefined;
|
|
182
|
+
if (pi !== undefined)
|
|
183
|
+
await pi.dispose();
|
|
184
|
+
this.store.remove(managed.info.id);
|
|
185
|
+
this.emit(managed.info.directory, { type: "session.deleted", properties: { info: managed.info } });
|
|
142
186
|
}
|
|
143
187
|
applyNativeTitle(s, value) {
|
|
144
188
|
if (s.titleSource === "user" || typeof value !== "string")
|
|
@@ -186,12 +230,15 @@ export class SessionManager {
|
|
|
186
230
|
const onExit = (code) => {
|
|
187
231
|
if (s.pi !== pi)
|
|
188
232
|
return;
|
|
189
|
-
|
|
233
|
+
this.events.finishInterruptedRun(s, {
|
|
234
|
+
name: "UnknownError",
|
|
235
|
+
data: { message: `pi process exited (code=${code})` },
|
|
236
|
+
});
|
|
237
|
+
delete s.interruptRequested;
|
|
190
238
|
this.subagents.stopParent(s.info.id);
|
|
191
239
|
void this.lifecycle
|
|
192
240
|
.dormant(s)
|
|
193
241
|
.catch((error) => console.error("[lifecycle] exit cleanup failed:", error));
|
|
194
|
-
this.events.setStatus(s, { type: "idle" });
|
|
195
242
|
this.emit(s.info.directory, {
|
|
196
243
|
type: "session.error",
|
|
197
244
|
properties: {
|
|
@@ -199,6 +246,7 @@ export class SessionManager {
|
|
|
199
246
|
error: { name: "UnknownError", data: { message: `pi process exited (code=${code})` } },
|
|
200
247
|
},
|
|
201
248
|
});
|
|
249
|
+
this.events.setStatus(s, { type: "idle" });
|
|
202
250
|
};
|
|
203
251
|
pi.onEvent(onEvent);
|
|
204
252
|
pi.onExtensionUI(onExtensionUI);
|
|
@@ -214,7 +262,7 @@ export class SessionManager {
|
|
|
214
262
|
async resolvePiFile(directory, piSessionId) {
|
|
215
263
|
const dir = piSessionDir(directory);
|
|
216
264
|
const suffix = `_${piSessionId}.jsonl`;
|
|
217
|
-
for (let attempt = 0; attempt <
|
|
265
|
+
for (let attempt = 0; attempt < PI_FILE_RESOLVE_ATTEMPTS; attempt++) {
|
|
218
266
|
try {
|
|
219
267
|
for (const name of readdirSync(dir)) {
|
|
220
268
|
if (name.endsWith(suffix))
|
|
@@ -224,7 +272,7 @@ export class SessionManager {
|
|
|
224
272
|
catch {
|
|
225
273
|
/* session dir may not exist until pi writes the first entry */
|
|
226
274
|
}
|
|
227
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
275
|
+
await new Promise((resolve) => setTimeout(resolve, PI_FILE_RESOLVE_RETRY_MS));
|
|
228
276
|
}
|
|
229
277
|
return undefined;
|
|
230
278
|
}
|
|
@@ -277,8 +325,9 @@ export class SessionManager {
|
|
|
277
325
|
try {
|
|
278
326
|
unlinkSync(piPath);
|
|
279
327
|
}
|
|
280
|
-
catch {
|
|
281
|
-
|
|
328
|
+
catch (error) {
|
|
329
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
330
|
+
throw error;
|
|
282
331
|
}
|
|
283
332
|
}
|
|
284
333
|
this.store.remove(sessionId);
|
|
@@ -290,14 +339,26 @@ export class SessionManager {
|
|
|
290
339
|
* (SDK v2 session.update); `title` is only touched when actually sent,
|
|
291
340
|
* otherwise an archive-only PATCH would blank the title.
|
|
292
341
|
*/
|
|
293
|
-
update(sessionId, patch) {
|
|
342
|
+
async update(sessionId, patch) {
|
|
294
343
|
const s = this.get(sessionId);
|
|
295
344
|
if (!s)
|
|
296
345
|
return undefined;
|
|
346
|
+
if (patch.agent !== undefined && !getExposedAgentNames().includes(patch.agent))
|
|
347
|
+
throw new RequestBodyError(400, `unknown agent: ${patch.agent}`);
|
|
297
348
|
if (typeof patch.title === "string" && patch.title.trim() !== "") {
|
|
298
|
-
|
|
349
|
+
const title = patch.title.trim().replace(/\s+/g, " ");
|
|
350
|
+
if (s.pi !== undefined) {
|
|
351
|
+
const renamed = await s.pi.command({ type: "set_session_name", name: title });
|
|
352
|
+
if (!renamed.success)
|
|
353
|
+
throw new Error(renamed.error ?? "session rename rejected");
|
|
354
|
+
}
|
|
355
|
+
s.info.title = title;
|
|
299
356
|
s.titleSource = "user";
|
|
300
357
|
}
|
|
358
|
+
if (patch.metadata !== undefined)
|
|
359
|
+
s.info.metadata = patch.metadata;
|
|
360
|
+
if (patch.agent !== undefined)
|
|
361
|
+
s.info.agent = patch.agent;
|
|
301
362
|
if (patch.archived !== undefined) {
|
|
302
363
|
if (patch.archived > 0)
|
|
303
364
|
s.info.time.archived = patch.archived;
|
|
@@ -306,9 +367,6 @@ export class SessionManager {
|
|
|
306
367
|
}
|
|
307
368
|
s.info.time.updated = Date.now();
|
|
308
369
|
this.persistMeta(s);
|
|
309
|
-
if (typeof patch.title === "string" && patch.title !== "" && s.pi !== undefined) {
|
|
310
|
-
void s.pi.command({ type: "set_session_name", name: s.info.title }).catch(() => undefined);
|
|
311
|
-
}
|
|
312
370
|
this.emit(s.info.directory, { type: "session.updated", properties: { info: s.info } });
|
|
313
371
|
return s.info;
|
|
314
372
|
}
|
|
@@ -349,6 +407,25 @@ export class SessionManager {
|
|
|
349
407
|
this.operations.delete(s.info.id);
|
|
350
408
|
}
|
|
351
409
|
}
|
|
410
|
+
/** Cancellation releases only the caller; the native action keeps its queue lease. */
|
|
411
|
+
async waitForOperation(pending, signal) {
|
|
412
|
+
if (!signal)
|
|
413
|
+
return pending;
|
|
414
|
+
let onAbort;
|
|
415
|
+
const aborted = new Promise((_, reject) => {
|
|
416
|
+
onAbort = () => reject(signal.reason);
|
|
417
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
418
|
+
if (signal.aborted)
|
|
419
|
+
onAbort();
|
|
420
|
+
});
|
|
421
|
+
try {
|
|
422
|
+
return await Promise.race([pending, aborted]);
|
|
423
|
+
}
|
|
424
|
+
finally {
|
|
425
|
+
if (onAbort)
|
|
426
|
+
signal.removeEventListener("abort", onAbort);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
352
429
|
async syncTree(s, treeOperation = false) {
|
|
353
430
|
const pi = s.pi;
|
|
354
431
|
if (!pi)
|
|
@@ -405,7 +482,8 @@ export class SessionManager {
|
|
|
405
482
|
});
|
|
406
483
|
}
|
|
407
484
|
async fork(sessionId, messageID) {
|
|
408
|
-
|
|
485
|
+
// Admission must see either the parent or child owner throughout the transfer.
|
|
486
|
+
return this.treeSession(sessionId, (s, pi) => this.lifecycle.run(async () => {
|
|
409
487
|
const tree = await this.syncTree(s, true);
|
|
410
488
|
const state = await forkConversation(s, pi, tree, messageID);
|
|
411
489
|
this.createdSessions.set(state.sessionId, s.info.directory);
|
|
@@ -423,6 +501,12 @@ export class SessionManager {
|
|
|
423
501
|
child.piSessionId = state.sessionId;
|
|
424
502
|
delete child.restored;
|
|
425
503
|
child.model = s.model;
|
|
504
|
+
child.info.parentID = s.info.id;
|
|
505
|
+
if (messageID !== undefined)
|
|
506
|
+
child.info.parentMessageID = messageID;
|
|
507
|
+
child.info.agent = s.info.agent ?? getAgentName();
|
|
508
|
+
if (s.info.metadata !== undefined)
|
|
509
|
+
child.info.metadata = structuredClone(s.info.metadata);
|
|
426
510
|
const childTree = await getEntries(pi);
|
|
427
511
|
child.tree.leafId = childTree.leafId;
|
|
428
512
|
this.ensureMessages(child);
|
|
@@ -431,7 +515,7 @@ export class SessionManager {
|
|
|
431
515
|
this.persistMeta(child);
|
|
432
516
|
this.emit(child.info.directory, { type: "session.created", properties: { info: child.info } });
|
|
433
517
|
return child.info;
|
|
434
|
-
});
|
|
518
|
+
}));
|
|
435
519
|
}
|
|
436
520
|
async revert(sessionId, messageID) {
|
|
437
521
|
return this.treeSession(sessionId, async (s, pi) => {
|
|
@@ -441,6 +525,10 @@ export class SessionManager {
|
|
|
441
525
|
return s.info;
|
|
442
526
|
this.persistMeta(s);
|
|
443
527
|
this.emit(s.info.directory, { type: "session.updated", properties: { info: s.info } });
|
|
528
|
+
this.emit(s.info.directory, {
|
|
529
|
+
type: "session.revert.staged",
|
|
530
|
+
properties: { sessionID: s.info.id, revert: { messageID } },
|
|
531
|
+
});
|
|
444
532
|
return s.info;
|
|
445
533
|
});
|
|
446
534
|
}
|
|
@@ -451,6 +539,7 @@ export class SessionManager {
|
|
|
451
539
|
return s.info;
|
|
452
540
|
this.persistMeta(s);
|
|
453
541
|
this.emit(s.info.directory, { type: "session.updated", properties: { info: s.info } });
|
|
542
|
+
this.emit(s.info.directory, { type: "session.revert.cleared", properties: { sessionID: s.info.id } });
|
|
454
543
|
return s.info;
|
|
455
544
|
});
|
|
456
545
|
}
|
|
@@ -458,7 +547,7 @@ export class SessionManager {
|
|
|
458
547
|
const s = this.get(sessionId);
|
|
459
548
|
if (!s)
|
|
460
549
|
return undefined;
|
|
461
|
-
return this.operation(s, () => this.reloadMessagesLive(s, signal));
|
|
550
|
+
return this.waitForOperation(this.operation(s, () => this.reloadMessagesLive(s, signal)), signal);
|
|
462
551
|
}
|
|
463
552
|
/** Called only within the conversation operation queue, including RPC refreshes. */
|
|
464
553
|
async reloadMessagesLive(s, signal) {
|
|
@@ -469,13 +558,13 @@ export class SessionManager {
|
|
|
469
558
|
const pi = s.pi;
|
|
470
559
|
if (!pi)
|
|
471
560
|
throw new SessionTreeError("Session cannot be resumed");
|
|
472
|
-
const tree = await getEntries(pi
|
|
561
|
+
const tree = await getEntries(pi);
|
|
473
562
|
signal?.throwIfAborted();
|
|
474
563
|
bindMessageEntries(s, tree);
|
|
475
564
|
// Compact/bash and extension slash commands can append without agent_end.
|
|
476
565
|
// Binding follows new descendants, never a stale ancestor of the selection.
|
|
477
566
|
if (s.tree.revert && s.tree.leafId !== undefined && tree.leafId !== s.tree.leafId)
|
|
478
|
-
await navigate(pi, s.tree.leafId
|
|
567
|
+
await navigate(pi, s.tree.leafId);
|
|
479
568
|
signal?.throwIfAborted();
|
|
480
569
|
const previous = s.messages ?? [];
|
|
481
570
|
const messages = this.refreshMessageCache(s);
|
|
@@ -493,7 +582,7 @@ export class SessionManager {
|
|
|
493
582
|
for (const part of message.parts)
|
|
494
583
|
this.events.emitPart(s, part);
|
|
495
584
|
}
|
|
496
|
-
this.update(sessionId, {});
|
|
585
|
+
await this.update(sessionId, {});
|
|
497
586
|
return messages;
|
|
498
587
|
}
|
|
499
588
|
statuses() {
|
|
@@ -509,11 +598,45 @@ export class SessionManager {
|
|
|
509
598
|
this.ensureMessages(s);
|
|
510
599
|
return s.todoBridge?.todos ?? [];
|
|
511
600
|
}
|
|
512
|
-
async
|
|
601
|
+
async commitRevert(sessionId) {
|
|
602
|
+
await this.treeSession(sessionId, async (s, pi) => {
|
|
603
|
+
await this.commitRevertLive(s, pi);
|
|
604
|
+
return s.info;
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
async commitRevertLive(s, pi) {
|
|
608
|
+
const revert = s.tree.revert;
|
|
609
|
+
if (revert === undefined)
|
|
610
|
+
return;
|
|
611
|
+
const boundary = s.messages?.findIndex((message) => message.info.id === revert.messageID) ?? -1;
|
|
612
|
+
if (boundary >= 0) {
|
|
613
|
+
for (const message of s.messages?.splice(boundary) ?? []) {
|
|
614
|
+
this.emit(s.info.directory, {
|
|
615
|
+
type: "message.removed",
|
|
616
|
+
properties: { sessionID: s.info.id, messageID: message.info.id },
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
delete s.tree.revert;
|
|
621
|
+
delete s.info.revert;
|
|
622
|
+
bindMessageEntries(s, await getEntries(pi));
|
|
623
|
+
this.persistMeta(s);
|
|
624
|
+
this.emit(s.info.directory, { type: "session.updated", properties: { info: s.info } });
|
|
625
|
+
this.emit(s.info.directory, {
|
|
626
|
+
type: "session.revert.committed",
|
|
627
|
+
properties: { sessionID: s.info.id, to: revert.messageID },
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
async diff(sessionId, options = {}) {
|
|
513
631
|
const s = this.get(sessionId);
|
|
514
632
|
if (s === undefined)
|
|
515
633
|
throw new Error(`unknown session: ${sessionId}`);
|
|
516
|
-
|
|
634
|
+
const messages = this.ensureMessages(s);
|
|
635
|
+
const start = options.from === undefined ? 0 : messages.findIndex((message) => message.info.id === options.from);
|
|
636
|
+
const end = options.to === undefined ? messages.length : messages.findIndex((message) => message.info.id === options.to);
|
|
637
|
+
if (start < 0 || end < 0 || start > end)
|
|
638
|
+
throw new RequestBodyError(400, "invalid diff message range");
|
|
639
|
+
return sessionDiff(s.info.directory, messages.slice(start, end), options.context);
|
|
517
640
|
}
|
|
518
641
|
emitDiff(s) {
|
|
519
642
|
if (!this.dirtyDiffs.delete(s.info.id))
|
|
@@ -544,9 +667,17 @@ export class SessionManager {
|
|
|
544
667
|
if (!s)
|
|
545
668
|
return false;
|
|
546
669
|
if (s.pi !== undefined) {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
670
|
+
if (s.status.type !== "idle")
|
|
671
|
+
s.interruptRequested = true;
|
|
672
|
+
try {
|
|
673
|
+
const response = await s.pi.command({ type: "abort" });
|
|
674
|
+
if (!response.success)
|
|
675
|
+
throw new Error(response.error ?? "pi abort failed");
|
|
676
|
+
}
|
|
677
|
+
catch (error) {
|
|
678
|
+
delete s.interruptRequested;
|
|
679
|
+
throw error;
|
|
680
|
+
}
|
|
550
681
|
}
|
|
551
682
|
s.cancelShell?.();
|
|
552
683
|
s.approvals?.cancelAll();
|
|
@@ -580,7 +711,7 @@ export class SessionManager {
|
|
|
580
711
|
}
|
|
581
712
|
approvalsFor(s) {
|
|
582
713
|
s.hasPendingInteraction = () => s.approvals?.hasPending(s.info.id) ?? false;
|
|
583
|
-
return new ApprovalBridge(s.info.id, (event) => this.emit(s.info.directory, event), (response) => s.pi?.respondExtensionUI(response), () => s.streaming?.message.id ??
|
|
714
|
+
return new ApprovalBridge(s.info.id, (event) => this.emit(s.info.directory, event), (response) => s.pi?.respondExtensionUI(response), () => s.streaming?.message.id ?? s.messages?.findLast((m) => m.info.role === "assistant")?.info.id, async (response) => {
|
|
584
715
|
if (!s.pi)
|
|
585
716
|
return false;
|
|
586
717
|
const reply = await s.pi.command({ ...response, type: "extension_ui_response" });
|
|
@@ -601,27 +732,13 @@ export class SessionManager {
|
|
|
601
732
|
throw new Error(`session cannot be resumed: ${sessionId}`);
|
|
602
733
|
this.lifecycle.touch(s);
|
|
603
734
|
const timeout = command.type === "compact" ? COMPACT_RPC_TIMEOUT_MS : RPC_COMMAND_TIMEOUT_MS;
|
|
604
|
-
|
|
735
|
+
signal?.throwIfAborted();
|
|
736
|
+
const response = await pi.command(command, timeout, undefined, onDispatch);
|
|
605
737
|
if (!signal?.aborted && response.success && (command.type === "compact" || command.type === "bash"))
|
|
606
738
|
await this.reloadMessagesLive(s, signal);
|
|
607
739
|
return response;
|
|
608
740
|
});
|
|
609
|
-
|
|
610
|
-
return pending;
|
|
611
|
-
// Cancel the caller's wait without releasing the conversation queue. The
|
|
612
|
-
// queued action checks the signal before dispatch when its predecessor ends.
|
|
613
|
-
let onAbort;
|
|
614
|
-
const aborted = new Promise((_, reject) => {
|
|
615
|
-
onAbort = () => reject(signal.reason);
|
|
616
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
617
|
-
});
|
|
618
|
-
try {
|
|
619
|
-
return await Promise.race([pending, aborted]);
|
|
620
|
-
}
|
|
621
|
-
finally {
|
|
622
|
-
if (onAbort)
|
|
623
|
-
signal.removeEventListener("abort", onAbort);
|
|
624
|
-
}
|
|
741
|
+
return this.waitForOperation(pending, signal);
|
|
625
742
|
}
|
|
626
743
|
/**
|
|
627
744
|
* POST /api/session/{id}/model — OpenChamber awaits this before every prompt
|
|
@@ -693,6 +810,7 @@ export class SessionManager {
|
|
|
693
810
|
return false;
|
|
694
811
|
const model = { providerID: provider, modelID: modelId };
|
|
695
812
|
s.model = model;
|
|
813
|
+
s.info.model = { id: modelId, providerID: provider, variant: s.thinkingLevel ?? "default" };
|
|
696
814
|
this.persistMeta(s);
|
|
697
815
|
this.emit(s.info.directory, {
|
|
698
816
|
type: "model.changed",
|
|
@@ -700,13 +818,13 @@ export class SessionManager {
|
|
|
700
818
|
});
|
|
701
819
|
return true;
|
|
702
820
|
}
|
|
703
|
-
async prompt(sessionId, text, model, noReply = false, messageID, delivery = "steer") {
|
|
821
|
+
async prompt(sessionId, text, model, noReply = false, messageID, delivery = "steer", parts, metadata) {
|
|
704
822
|
const s = this.get(sessionId);
|
|
705
823
|
if (!s)
|
|
706
824
|
throw new Error(`unknown session: ${sessionId}`);
|
|
707
|
-
return this.operation(s, () => this.promptLive(s, text, model, noReply, messageID, delivery));
|
|
825
|
+
return this.operation(s, () => this.promptLive(s, text, model, noReply, messageID, delivery, parts, metadata));
|
|
708
826
|
}
|
|
709
|
-
async
|
|
827
|
+
async preparePrompt(s, model) {
|
|
710
828
|
const sessionId = s.info.id;
|
|
711
829
|
if (s.pi === undefined)
|
|
712
830
|
await this.revive(s);
|
|
@@ -715,41 +833,29 @@ export class SessionManager {
|
|
|
715
833
|
throw new Error(`session cannot be resumed: ${sessionId}`);
|
|
716
834
|
this.lifecycle.touch(s);
|
|
717
835
|
this.ensureMessages(s);
|
|
718
|
-
|
|
719
|
-
const boundary = s.messages?.findIndex((message) => message.info.id === s.tree.revert?.messageID) ?? -1;
|
|
720
|
-
if (boundary >= 0) {
|
|
721
|
-
for (const message of s.messages?.splice(boundary) ?? []) {
|
|
722
|
-
this.emit(s.info.directory, {
|
|
723
|
-
type: "message.removed",
|
|
724
|
-
properties: { sessionID: s.info.id, messageID: message.info.id },
|
|
725
|
-
});
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
delete s.tree.revert;
|
|
729
|
-
delete s.info.revert;
|
|
730
|
-
bindMessageEntries(s, await getEntries(pi));
|
|
731
|
-
this.persistMeta(s);
|
|
732
|
-
this.emit(s.info.directory, { type: "session.updated", properties: { info: s.info } });
|
|
733
|
-
}
|
|
734
|
-
// Same funnel as POST /api/session/{id}/model. Failing loudly matters: the
|
|
735
|
-
// previous code swallowed pi's refusal and then reported the *requested*
|
|
736
|
-
// model as though the switch had happened.
|
|
836
|
+
await this.commitRevertLive(s, pi);
|
|
737
837
|
if (model !== undefined) {
|
|
738
838
|
try {
|
|
739
839
|
// This prompt already owns the operation queue; do not enqueue recursively.
|
|
740
840
|
await this.setModelLive(s, model);
|
|
741
841
|
}
|
|
742
|
-
catch (
|
|
842
|
+
catch (error) {
|
|
743
843
|
this.emit(s.info.directory, {
|
|
744
844
|
type: "session.error",
|
|
745
845
|
properties: {
|
|
746
846
|
sessionID: s.info.id,
|
|
747
|
-
error: {
|
|
847
|
+
error: {
|
|
848
|
+
name: "UnknownError",
|
|
849
|
+
data: { message: error instanceof Error ? error.message : String(error) },
|
|
850
|
+
},
|
|
748
851
|
},
|
|
749
852
|
});
|
|
750
|
-
throw
|
|
853
|
+
throw error;
|
|
751
854
|
}
|
|
752
855
|
}
|
|
856
|
+
return pi;
|
|
857
|
+
}
|
|
858
|
+
publishPrompt(s, text, model, messageID, inputParts, metadata, context = false) {
|
|
753
859
|
// OpenChamber sends a client-generated messageID with its optimistic
|
|
754
860
|
// insert; the server MUST reuse it so the echoed message.part.updated /
|
|
755
861
|
// message.updated events reconcile the optimistic entry in place instead
|
|
@@ -761,12 +867,23 @@ export class SessionManager {
|
|
|
761
867
|
time: { created: Date.now() },
|
|
762
868
|
agent: getAgentName(),
|
|
763
869
|
model: s.model ?? model ?? { providerID: "unknown", modelID: "unknown" },
|
|
870
|
+
...(context ? { synthetic: true } : {}),
|
|
871
|
+
...(metadata !== undefined ? { metadata } : {}),
|
|
764
872
|
};
|
|
765
|
-
|
|
873
|
+
if (metadata !== undefined)
|
|
874
|
+
(s.tree.promptMetadata ??= {})[userMsg.id] = metadata;
|
|
875
|
+
const parts = (inputParts ?? [{ type: "text", text }]).map((part) => ({
|
|
876
|
+
...part,
|
|
877
|
+
id: part.id ?? id("prt"),
|
|
878
|
+
sessionID: s.info.id,
|
|
879
|
+
messageID: userMsg.id,
|
|
880
|
+
}));
|
|
881
|
+
if (inputParts?.some((part) => part.type !== "text"))
|
|
882
|
+
(s.tree.promptParts ??= {})[userMsg.id] = [...inputParts];
|
|
766
883
|
s.messages?.push({ info: userMsg, parts });
|
|
767
884
|
s.info.time.updated = Date.now();
|
|
768
|
-
if (s.info.title === "New session" && s.titleSource !== "user" && s.titleSource !== "native") {
|
|
769
|
-
s.info.title = text.trim().replace(/\s+/g, " ").slice(0,
|
|
885
|
+
if (!context && s.info.title === "New session" && s.titleSource !== "user" && s.titleSource !== "native") {
|
|
886
|
+
s.info.title = text.trim().replace(/\s+/g, " ").slice(0, SESSION_TITLE_MAX_CHARS) || "New session";
|
|
770
887
|
s.titleSource = "fallback";
|
|
771
888
|
}
|
|
772
889
|
this.persistMeta(s);
|
|
@@ -775,19 +892,74 @@ export class SessionManager {
|
|
|
775
892
|
// Echo the user text part: the reducer replaces the client's sessionID-less
|
|
776
893
|
// optimistic part (same type, new id) in place — without this echo a page
|
|
777
894
|
// fetch merges the optimistic part back in and the text shows twice.
|
|
778
|
-
for (const
|
|
779
|
-
this.events.emitPart(s,
|
|
780
|
-
|
|
781
|
-
|
|
895
|
+
for (const part of parts)
|
|
896
|
+
this.events.emitPart(s, part);
|
|
897
|
+
return userMsg;
|
|
898
|
+
}
|
|
899
|
+
async promptLive(s, text, model, noReply = false, messageID, delivery = "steer", parts, metadata) {
|
|
900
|
+
const pi = await this.preparePrompt(s, model);
|
|
901
|
+
const native = nativePromptInput(text, parts ?? []);
|
|
902
|
+
const userMsg = this.publishPrompt(s, text, model, messageID, parts, metadata, noReply);
|
|
903
|
+
if (noReply) {
|
|
904
|
+
await this.admitContext(s, pi, userMsg, native.message);
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
if (!(await this.dispatchPrompt(s, pi, userMsg, text, delivery, native, parts ?? [])))
|
|
908
|
+
return;
|
|
909
|
+
await this.bindPromptState(s);
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* No-reply context (v2 synthetic, v1 noReply) joins the native conversation without a turn:
|
|
913
|
+
* OmO appends it now when idle, or after the running turn, so the next prompt carries it.
|
|
914
|
+
*/
|
|
915
|
+
async admitContext(s, pi, userMsg, text) {
|
|
916
|
+
const res = await pi.command({
|
|
917
|
+
type: "send_custom_message",
|
|
918
|
+
customType: CONTEXT_MESSAGE_TYPE,
|
|
919
|
+
content: [{ type: "text", text }],
|
|
920
|
+
display: true,
|
|
921
|
+
details: { messageID: userMsg.id },
|
|
922
|
+
triggerTurn: false,
|
|
923
|
+
});
|
|
924
|
+
if (!res.success) {
|
|
925
|
+
if (s.tree.promptParts)
|
|
926
|
+
delete s.tree.promptParts[userMsg.id];
|
|
927
|
+
if (s.tree.promptMetadata)
|
|
928
|
+
delete s.tree.promptMetadata[userMsg.id];
|
|
929
|
+
s.messages = s.messages?.filter((message) => message.info.id !== userMsg.id);
|
|
930
|
+
this.emit(s.info.directory, {
|
|
931
|
+
type: "message.removed",
|
|
932
|
+
properties: { sessionID: s.info.id, messageID: userMsg.id },
|
|
933
|
+
});
|
|
934
|
+
throw new Error(res.error ?? "context admission rejected");
|
|
935
|
+
}
|
|
936
|
+
// Bind the appended entry and persist the new leaf before announcing delivery, or a restart
|
|
937
|
+
// restores the old branch without the context.
|
|
938
|
+
await this.bindPromptState(s);
|
|
939
|
+
this.emit(s.info.directory, {
|
|
940
|
+
type: "session.inbox.enqueued",
|
|
941
|
+
properties: {
|
|
942
|
+
sessionID: s.info.id,
|
|
943
|
+
inboxID: userMsg.id,
|
|
944
|
+
item: { type: "synthetic", payload: { text, ...(userMsg.metadata ? { metadata: userMsg.metadata } : {}) } },
|
|
945
|
+
},
|
|
946
|
+
});
|
|
947
|
+
this.events.deliverInput(s, userMsg.id);
|
|
948
|
+
}
|
|
949
|
+
async dispatchPrompt(s, pi, userMsg, text, delivery, native, parts) {
|
|
782
950
|
const wasIdle = s.status.type === "idle";
|
|
783
951
|
if (wasIdle) {
|
|
784
952
|
s.toolByCallId = new Map();
|
|
785
953
|
this.events.setStatus(s, { type: "busy" });
|
|
786
954
|
}
|
|
787
|
-
const input = { text, delivery, accepted: false, delivered: false };
|
|
955
|
+
const input = { text: native.message, delivery, accepted: false, delivered: false };
|
|
788
956
|
(s.inputs ??= new Map()).set(userMsg.id, input);
|
|
789
957
|
const rejectInput = () => {
|
|
790
958
|
s.inputs?.delete(userMsg.id);
|
|
959
|
+
if (s.tree.promptParts)
|
|
960
|
+
delete s.tree.promptParts[userMsg.id];
|
|
961
|
+
if (s.tree.promptMetadata)
|
|
962
|
+
delete s.tree.promptMetadata[userMsg.id];
|
|
791
963
|
s.messages = s.messages?.filter((message) => message.info.id !== userMsg.id);
|
|
792
964
|
this.emit(s.info.directory, {
|
|
793
965
|
type: "message.removed",
|
|
@@ -806,7 +978,7 @@ export class SessionManager {
|
|
|
806
978
|
const res = await pi
|
|
807
979
|
.command({
|
|
808
980
|
type: "prompt",
|
|
809
|
-
|
|
981
|
+
...native,
|
|
810
982
|
streamingBehavior: delivery === "queue" ? "followUp" : "steer",
|
|
811
983
|
clientMessageId: userMsg.id,
|
|
812
984
|
...slashTitlePrompt(text),
|
|
@@ -827,12 +999,24 @@ export class SessionManager {
|
|
|
827
999
|
error: { name: "UnknownError", data: { message: res.error ?? "prompt rejected" } },
|
|
828
1000
|
},
|
|
829
1001
|
});
|
|
830
|
-
return;
|
|
1002
|
+
return false;
|
|
831
1003
|
}
|
|
832
1004
|
input.accepted = true;
|
|
833
1005
|
this.emit(s.info.directory, {
|
|
834
1006
|
type: "session.inbox.enqueued",
|
|
835
|
-
properties: {
|
|
1007
|
+
properties: {
|
|
1008
|
+
sessionID: s.info.id,
|
|
1009
|
+
inboxID: userMsg.id,
|
|
1010
|
+
item: {
|
|
1011
|
+
type: "user",
|
|
1012
|
+
delivery,
|
|
1013
|
+
payload: {
|
|
1014
|
+
text,
|
|
1015
|
+
...v2PromptAttachments(parts),
|
|
1016
|
+
...(userMsg.metadata ? { metadata: userMsg.metadata } : {}),
|
|
1017
|
+
},
|
|
1018
|
+
},
|
|
1019
|
+
},
|
|
836
1020
|
});
|
|
837
1021
|
if (input.delivered)
|
|
838
1022
|
this.events.deliverInput(s, userMsg.id);
|
|
@@ -841,9 +1025,14 @@ export class SessionManager {
|
|
|
841
1025
|
typeof res.data === "object" &&
|
|
842
1026
|
"disposition" in res.data &&
|
|
843
1027
|
res.data.disposition === "handled") {
|
|
1028
|
+
if (!input.delivered)
|
|
1029
|
+
this.events.deliverInput(s, userMsg.id);
|
|
844
1030
|
if (wasIdle)
|
|
845
1031
|
this.events.setStatus(s, { type: "idle" });
|
|
846
1032
|
}
|
|
1033
|
+
return true;
|
|
1034
|
+
}
|
|
1035
|
+
async bindPromptState(s) {
|
|
847
1036
|
// First message makes pi write the session file — bind it now so the
|
|
848
1037
|
// session (and its current title) survives an adapter restart.
|
|
849
1038
|
if (s.piPath === undefined && s.piSessionId !== undefined) {
|
|
@@ -1022,7 +1211,7 @@ export class SessionManager {
|
|
|
1022
1211
|
}
|
|
1023
1212
|
}
|
|
1024
1213
|
adopt(sc) {
|
|
1025
|
-
const ocId = ocIdFor(sc.piSessionId);
|
|
1214
|
+
const ocId = this.store.idFor(sc.piSessionId) ?? ocIdFor(sc.piSessionId);
|
|
1026
1215
|
const reg = this.store.get(ocId);
|
|
1027
1216
|
const titleSource = reg?.titleSource ??
|
|
1028
1217
|
(reg?.title && reg.title !== "New session" && reg.title !== sc.fallbackTitle ? "user" : "fallback");
|
|
@@ -1032,6 +1221,19 @@ export class SessionManager {
|
|
|
1032
1221
|
projectID: sc.directory,
|
|
1033
1222
|
directory: sc.directory,
|
|
1034
1223
|
title: useRegistry ? (reg?.title ?? sc.title) : sc.title,
|
|
1224
|
+
agent: reg?.agent ?? getAgentName(),
|
|
1225
|
+
slug: sc.piSessionId,
|
|
1226
|
+
path: "",
|
|
1227
|
+
summary: { additions: 0, deletions: 0, files: 0 },
|
|
1228
|
+
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
1229
|
+
...(reg?.metadata !== undefined ? { metadata: reg.metadata } : {}),
|
|
1230
|
+
...(reg?.parentID !== undefined ? { parentID: reg.parentID } : {}),
|
|
1231
|
+
...(reg?.parentMessageID !== undefined ? { parentMessageID: reg.parentMessageID } : {}),
|
|
1232
|
+
...(reg?.model !== undefined
|
|
1233
|
+
? {
|
|
1234
|
+
model: { id: reg.model.modelID, providerID: reg.model.providerID, variant: reg.model.variant ?? "default" },
|
|
1235
|
+
}
|
|
1236
|
+
: {}),
|
|
1035
1237
|
...(reg?.tree?.revert ? { revert: { messageID: reg.tree.revert.messageID } } : {}),
|
|
1036
1238
|
version: ADAPTER_VERSION,
|
|
1037
1239
|
time: {
|
|
@@ -1089,6 +1291,11 @@ export class SessionManager {
|
|
|
1089
1291
|
}
|
|
1090
1292
|
/** Registry write for adapter-owned metadata (title override, pi path). */
|
|
1091
1293
|
persistMeta(s) {
|
|
1294
|
+
for (const message of s.messages ?? []) {
|
|
1295
|
+
if (s.localMessageIds?.has(message.info.id))
|
|
1296
|
+
continue;
|
|
1297
|
+
(s.tree.messageParts ??= {})[message.info.id] = message.parts.map((part) => part.id);
|
|
1298
|
+
}
|
|
1092
1299
|
const piPath = s.piPath ?? s.restored?.piPath;
|
|
1093
1300
|
const piSessionId = s.piSessionId ?? s.restored?.piSessionId;
|
|
1094
1301
|
if (piPath === undefined || piSessionId === undefined)
|
|
@@ -1103,6 +1310,10 @@ export class SessionManager {
|
|
|
1103
1310
|
...(s.info.title !== "New session" ? { title: s.info.title } : {}),
|
|
1104
1311
|
...(s.titleSource !== undefined ? { titleSource: s.titleSource } : {}),
|
|
1105
1312
|
...(s.info.time.archived !== undefined ? { archived: s.info.time.archived } : {}),
|
|
1313
|
+
...(s.info.metadata !== undefined ? { metadata: s.info.metadata } : {}),
|
|
1314
|
+
...(s.info.agent !== undefined ? { agent: s.info.agent } : {}),
|
|
1315
|
+
...(s.info.parentID !== undefined ? { parentID: s.info.parentID } : {}),
|
|
1316
|
+
...(s.info.parentMessageID !== undefined ? { parentMessageID: s.info.parentMessageID } : {}),
|
|
1106
1317
|
...(s.model !== undefined ? { model: { providerID: s.model.providerID, modelID: s.model.modelID } } : {}),
|
|
1107
1318
|
created: s.info.time.created,
|
|
1108
1319
|
updated: s.info.time.updated,
|
|
@@ -1110,11 +1321,11 @@ export class SessionManager {
|
|
|
1110
1321
|
}
|
|
1111
1322
|
/** Async event callbacks cannot reject an HTTP operation; report save failures through SSE. */
|
|
1112
1323
|
reportPersistenceError(s, error) {
|
|
1113
|
-
|
|
1114
|
-
|
|
1324
|
+
const name = error instanceof Error ? error.name : "UnknownError";
|
|
1325
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1115
1326
|
this.emit(s.info.directory, {
|
|
1116
1327
|
type: "session.error",
|
|
1117
|
-
properties: { sessionID: s.info.id, error: { name
|
|
1328
|
+
properties: { sessionID: s.info.id, error: { name, data: { message } } },
|
|
1118
1329
|
});
|
|
1119
1330
|
}
|
|
1120
1331
|
/** Keep the existing event entry point for RPC wiring and test harnesses. */
|