antechamber 1.0.39 → 1.0.41
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 +37 -0
- package/dist/approvals.d.ts +9 -1
- package/dist/approvals.js +10 -2
- package/dist/catalog.d.ts +13 -2
- package/dist/catalog.js +88 -52
- package/dist/file-watch.d.ts +35 -0
- package/dist/file-watch.js +163 -0
- package/dist/misc-routes.js +12 -2
- package/dist/pi-event-translation.js +44 -2
- package/dist/route-services.d.ts +8 -1
- package/dist/routes-v2.js +251 -33
- package/dist/server.d.ts +16 -0
- package/dist/server.js +182 -6
- package/dist/session-commands.d.ts +3 -1
- package/dist/session-commands.js +13 -7
- package/dist/session-history.js +3 -0
- package/dist/session-store.d.ts +7 -0
- package/dist/session-store.js +28 -0
- package/dist/session-tree.d.ts +15 -1
- package/dist/sessions.d.ts +85 -1
- package/dist/sessions.js +238 -10
- package/dist/text-generation.d.ts +20 -0
- package/dist/text-generation.js +93 -0
- package/dist/types.d.ts +60 -0
- package/dist/v2.d.ts +8 -1
- package/dist/v2.js +34 -16
- package/extensions/omo-compat/package.json +1 -1
- package/extensions/omo-dag/package.json +1 -1
- package/extensions/omo-work/package.json +1 -1
- package/package.json +1 -1
package/dist/sessions.js
CHANGED
|
@@ -87,7 +87,9 @@ export class SessionManager {
|
|
|
87
87
|
this.workStatus = new WorkStatusBridge(this.monitors);
|
|
88
88
|
this.shells = new OmoShells(this.sessions, this.monitors, this.workStatus, this.emit);
|
|
89
89
|
this.events = new PiEventTranslator({
|
|
90
|
-
emit:
|
|
90
|
+
emit: (directory, event) => {
|
|
91
|
+
this.emit(directory, event);
|
|
92
|
+
},
|
|
91
93
|
subagents: this.subagents,
|
|
92
94
|
monitors: this.monitors,
|
|
93
95
|
workStatus: this.workStatus,
|
|
@@ -161,6 +163,7 @@ export class SessionManager {
|
|
|
161
163
|
messages: [],
|
|
162
164
|
status: { type: "idle" },
|
|
163
165
|
model: undefined,
|
|
166
|
+
initializing: true,
|
|
164
167
|
streaming: undefined,
|
|
165
168
|
toolByCallId: new Map(),
|
|
166
169
|
hasPendingInteraction: () => false,
|
|
@@ -179,13 +182,11 @@ export class SessionManager {
|
|
|
179
182
|
this.wireDormant(managed);
|
|
180
183
|
this.lifecycle.touch(managed);
|
|
181
184
|
this.wirePi(managed, pi);
|
|
185
|
+
this.applyPiModel(managed, state.data.model?.provider, state.data.model?.id);
|
|
182
186
|
if (options.model !== undefined)
|
|
183
187
|
await this.setModel(info.id, options.model);
|
|
184
188
|
this.emit(directory, { type: "session.created", properties: { info } });
|
|
185
|
-
|
|
186
|
-
// knows the session when the switch marker lands in its transcript.
|
|
187
|
-
if (options.model === undefined)
|
|
188
|
-
this.applyPiModel(managed, state.data.model?.provider, state.data.model?.id);
|
|
189
|
+
delete managed.initializing;
|
|
189
190
|
// Bind pi's session file so the session survives adapter restarts. pi
|
|
190
191
|
// reports the path directly; fall back to a background scan for older pi
|
|
191
192
|
// versions, and re-bind after the first prompt (pi persists lazily).
|
|
@@ -250,6 +251,7 @@ export class SessionManager {
|
|
|
250
251
|
s.info.time.updated = Date.now();
|
|
251
252
|
this.persistMeta(s);
|
|
252
253
|
this.emit(s.info.directory, { type: "session.updated", properties: { info: s.info } });
|
|
254
|
+
this.emit(s.info.directory, { type: "session.renamed", properties: { sessionID: s.info.id, title } });
|
|
253
255
|
}
|
|
254
256
|
refreshNativeTitles() {
|
|
255
257
|
for (const s of this.sessions.values())
|
|
@@ -394,6 +396,14 @@ export class SessionManager {
|
|
|
394
396
|
return [...this.sessions.values()].map((s) => this.sessionInfo(s));
|
|
395
397
|
});
|
|
396
398
|
}
|
|
399
|
+
/** Reuse an already-running agent for directory-scoped catalog reads. Never revives a dormant session. */
|
|
400
|
+
liveClient(directory) {
|
|
401
|
+
for (const session of this.sessions.values()) {
|
|
402
|
+
if (session.info.directory === directory && session.pi !== undefined)
|
|
403
|
+
return session.pi;
|
|
404
|
+
}
|
|
405
|
+
return undefined;
|
|
406
|
+
}
|
|
397
407
|
sessionInfo(s) {
|
|
398
408
|
if (s.restored !== undefined)
|
|
399
409
|
this.ensureMessages(s);
|
|
@@ -458,6 +468,9 @@ export class SessionManager {
|
|
|
458
468
|
return undefined;
|
|
459
469
|
if (patch.agent !== undefined && !getExposedAgentNames().includes(patch.agent))
|
|
460
470
|
throw new RequestBodyError(400, `unknown agent: ${patch.agent}`);
|
|
471
|
+
const previousTitle = s.info.title;
|
|
472
|
+
const previousAgent = s.info.agent ?? getAgentName();
|
|
473
|
+
const wasArchived = s.info.time.archived !== undefined;
|
|
461
474
|
if (typeof patch.title === "string" && patch.title.trim() !== "") {
|
|
462
475
|
const title = patch.title.trim().replace(/\s+/g, " ");
|
|
463
476
|
if (s.pi !== undefined) {
|
|
@@ -483,8 +496,58 @@ export class SessionManager {
|
|
|
483
496
|
if (!this.subagents.save(s.info.id))
|
|
484
497
|
this.persistMeta(s);
|
|
485
498
|
this.emit(s.info.directory, { type: "session.updated", properties: { info: s.info } });
|
|
499
|
+
if (s.info.title !== previousTitle)
|
|
500
|
+
this.emit(s.info.directory, {
|
|
501
|
+
type: "session.renamed",
|
|
502
|
+
properties: { sessionID: sessionId, title: s.info.title },
|
|
503
|
+
});
|
|
504
|
+
if ((s.info.agent ?? getAgentName()) !== previousAgent)
|
|
505
|
+
this.emit(s.info.directory, {
|
|
506
|
+
type: "session.agent.selected",
|
|
507
|
+
properties: { sessionID: sessionId, agent: s.info.agent ?? getAgentName(), previous: previousAgent },
|
|
508
|
+
});
|
|
509
|
+
if ((s.info.time.archived !== undefined) !== wasArchived)
|
|
510
|
+
this.emit(s.info.directory, {
|
|
511
|
+
type: s.info.time.archived !== undefined ? "session.archived" : "session.unarchived",
|
|
512
|
+
properties: { sessionID: sessionId },
|
|
513
|
+
});
|
|
486
514
|
return s.info;
|
|
487
515
|
}
|
|
516
|
+
/** Mark only the idle boundary that the caller actually observed. */
|
|
517
|
+
view(sessionId, idle) {
|
|
518
|
+
const s = this.get(sessionId);
|
|
519
|
+
if (s === undefined ||
|
|
520
|
+
s.info.time.idle === undefined ||
|
|
521
|
+
idle > s.info.time.idle ||
|
|
522
|
+
(s.info.time.viewed !== undefined && s.info.time.viewed >= idle))
|
|
523
|
+
return;
|
|
524
|
+
s.info.time.viewed = idle;
|
|
525
|
+
this.persistMeta(s);
|
|
526
|
+
this.emit(s.info.directory, { type: "session.viewed", properties: { sessionID: sessionId, idle } });
|
|
527
|
+
}
|
|
528
|
+
putInstruction(sessionId, key, value) {
|
|
529
|
+
const s = this.get(sessionId);
|
|
530
|
+
if (s === undefined)
|
|
531
|
+
throw new Error(`unknown session: ${sessionId}`);
|
|
532
|
+
const previous = s.instructions?.[key];
|
|
533
|
+
if (previous?.removed !== true &&
|
|
534
|
+
previous !== undefined &&
|
|
535
|
+
JSON.stringify(previous.value) === JSON.stringify(value))
|
|
536
|
+
return;
|
|
537
|
+
(s.instructions ??= {})[key] = { value };
|
|
538
|
+
this.persistMeta(s);
|
|
539
|
+
}
|
|
540
|
+
removeInstruction(sessionId, key) {
|
|
541
|
+
const s = this.get(sessionId);
|
|
542
|
+
if (s === undefined)
|
|
543
|
+
throw new Error(`unknown session: ${sessionId}`);
|
|
544
|
+
const instructions = s.instructions;
|
|
545
|
+
const previous = instructions?.[key];
|
|
546
|
+
if (instructions === undefined || previous === undefined || previous.removed)
|
|
547
|
+
return;
|
|
548
|
+
instructions[key] = { value: null, removed: true };
|
|
549
|
+
this.persistMeta(s);
|
|
550
|
+
}
|
|
488
551
|
messages(sessionId) {
|
|
489
552
|
const s = this.get(sessionId);
|
|
490
553
|
if (!s)
|
|
@@ -499,6 +562,61 @@ export class SessionManager {
|
|
|
499
562
|
}
|
|
500
563
|
return messages;
|
|
501
564
|
}
|
|
565
|
+
compaction(sessionId, messageID) {
|
|
566
|
+
const s = this.get(sessionId);
|
|
567
|
+
if (s === undefined)
|
|
568
|
+
return undefined;
|
|
569
|
+
const history = this.ensureMessages(s);
|
|
570
|
+
const index = history.findIndex((message) => message.parts.some((part) => part.type === "compaction") &&
|
|
571
|
+
(message.info.id === messageID ||
|
|
572
|
+
s.tree.messageEntries[messageID] === message.info.id.replace(/^msg_compaction_/, "")));
|
|
573
|
+
const message = history[index];
|
|
574
|
+
const summary = history[index + 1];
|
|
575
|
+
if (message !== undefined && summary?.info.role === "assistant" && summary.info.summary) {
|
|
576
|
+
const entryID = message.info.id.replace(/^msg_compaction_/, "");
|
|
577
|
+
const admittedID = Object.keys(s.tree.messageEntries).find((key) => key !== message.info.id && s.tree.messageEntries[key] === entryID) ?? message.info.id;
|
|
578
|
+
const content = summary.parts.find((part) => part.type === "text");
|
|
579
|
+
return {
|
|
580
|
+
id: admittedID,
|
|
581
|
+
type: "compaction",
|
|
582
|
+
time: { created: s.tree.compactionAdmissions?.[admittedID] ?? message.info.time.created },
|
|
583
|
+
status: "completed",
|
|
584
|
+
reason: s.tree.compactionReasons?.[entryID] ?? "manual",
|
|
585
|
+
summary: content?.type === "text" ? content.text : "",
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
if (s.compactionID !== messageID)
|
|
589
|
+
return undefined;
|
|
590
|
+
return {
|
|
591
|
+
id: messageID,
|
|
592
|
+
type: "compaction",
|
|
593
|
+
time: { created: s.tree.compactionAdmissions?.[messageID] ?? s.info.time.compacting ?? Date.now() },
|
|
594
|
+
status: "running",
|
|
595
|
+
reason: "manual",
|
|
596
|
+
summary: "",
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
/** Synchronous admission owns the ID before any native work can yield to another request. */
|
|
600
|
+
admitCompaction(sessionId, messageID) {
|
|
601
|
+
const s = this.get(sessionId);
|
|
602
|
+
if (s === undefined)
|
|
603
|
+
throw new SessionTreeError(`unknown session: ${sessionId}`);
|
|
604
|
+
const history = this.ensureMessages(s);
|
|
605
|
+
const owner = history.find((message) => message.info.id === messageID);
|
|
606
|
+
if ((owner !== undefined && !owner.parts.some((part) => part.type === "compaction")) ||
|
|
607
|
+
Object.keys(s.tree.idleBoundaries ?? {}).some((key) => `msg_idle_${key.replace(/^msg_/, "")}` === messageID))
|
|
608
|
+
return { kind: "conflict" };
|
|
609
|
+
const exact = this.compaction(sessionId, messageID);
|
|
610
|
+
if (exact !== undefined)
|
|
611
|
+
return { kind: "existing", item: exact };
|
|
612
|
+
if (s.tree.messageEntries[messageID] !== undefined)
|
|
613
|
+
return { kind: "conflict" };
|
|
614
|
+
if (s.compactionID !== undefined)
|
|
615
|
+
return { kind: "existing", item: this.compaction(sessionId, s.compactionID) };
|
|
616
|
+
s.compactionID = messageID;
|
|
617
|
+
(s.tree.compactionAdmissions ??= {})[messageID] = Date.now();
|
|
618
|
+
return { kind: "admitted", item: this.compaction(sessionId, messageID) };
|
|
619
|
+
}
|
|
502
620
|
/** Append to canonical history, before the hidden redo branch while reverted. */
|
|
503
621
|
appendMessage(sessionId, message) {
|
|
504
622
|
const s = this.get(sessionId);
|
|
@@ -626,6 +744,8 @@ export class SessionManager {
|
|
|
626
744
|
child.info.agent = s.info.agent ?? getAgentName();
|
|
627
745
|
if (s.info.metadata !== undefined)
|
|
628
746
|
child.info.metadata = structuredClone(s.info.metadata);
|
|
747
|
+
if (s.instructions !== undefined)
|
|
748
|
+
child.instructions = structuredClone(s.instructions);
|
|
629
749
|
const childTree = await getEntries(pi);
|
|
630
750
|
child.tree.leafId = childTree.leafId;
|
|
631
751
|
this.ensureMessages(child);
|
|
@@ -979,10 +1099,11 @@ export class SessionManager {
|
|
|
979
1099
|
s.model = model;
|
|
980
1100
|
s.info.model = { id: modelId, providerID: provider, variant: s.thinkingLevel ?? "default" };
|
|
981
1101
|
this.persistMeta(s);
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1102
|
+
if (!s.initializing)
|
|
1103
|
+
this.emit(s.info.directory, {
|
|
1104
|
+
type: "model.changed",
|
|
1105
|
+
properties: { sessionID: s.info.id, model, ...(previous !== undefined ? { previous } : {}) },
|
|
1106
|
+
});
|
|
986
1107
|
return true;
|
|
987
1108
|
}
|
|
988
1109
|
async prompt(sessionId, text, model, noReply = false, messageID, delivery = "steer", parts, metadata) {
|
|
@@ -1035,6 +1156,9 @@ export class SessionManager {
|
|
|
1035
1156
|
agent: getAgentName(),
|
|
1036
1157
|
model: userMessageModel(s.model ?? model ?? { providerID: "unknown", modelID: "unknown" }, s.thinkingLevel),
|
|
1037
1158
|
...(context ? { synthetic: true } : {}),
|
|
1159
|
+
...(context && s.tree.promptDescriptions?.[messageID ?? ""] !== undefined
|
|
1160
|
+
? { description: s.tree.promptDescriptions[messageID ?? ""] }
|
|
1161
|
+
: {}),
|
|
1038
1162
|
...(metadata !== undefined ? { metadata } : {}),
|
|
1039
1163
|
};
|
|
1040
1164
|
if (metadata !== undefined)
|
|
@@ -1052,12 +1176,18 @@ export class SessionManager {
|
|
|
1052
1176
|
(s.tree.promptParts ??= {})[userMsg.id] = [...inputParts];
|
|
1053
1177
|
s.messages?.push({ info: userMsg, parts });
|
|
1054
1178
|
s.info.time.updated = Date.now();
|
|
1179
|
+
const previousTitle = s.info.title;
|
|
1055
1180
|
if (!context && s.info.title === "New session" && s.titleSource !== "user" && s.titleSource !== "native") {
|
|
1056
1181
|
s.info.title = text.trim().replace(/\s+/g, " ").slice(0, SESSION_TITLE_MAX_CHARS) || "New session";
|
|
1057
1182
|
s.titleSource = "fallback";
|
|
1058
1183
|
}
|
|
1059
1184
|
this.persistMeta(s);
|
|
1060
1185
|
this.emit(s.info.directory, { type: "session.updated", properties: { info: s.info } });
|
|
1186
|
+
if (s.info.title !== previousTitle)
|
|
1187
|
+
this.emit(s.info.directory, {
|
|
1188
|
+
type: "session.renamed",
|
|
1189
|
+
properties: { sessionID: s.info.id, title: s.info.title },
|
|
1190
|
+
});
|
|
1061
1191
|
this.emit(s.info.directory, { type: "message.updated", properties: { info: userMsg } });
|
|
1062
1192
|
// Echo the user text part: the reducer replaces the client's sessionID-less
|
|
1063
1193
|
// optimistic part (same type, new id) in place — without this echo a page
|
|
@@ -1069,6 +1199,10 @@ export class SessionManager {
|
|
|
1069
1199
|
async promptLive(s, text, model, noReply = false, messageID, delivery = "steer", parts, metadata) {
|
|
1070
1200
|
const pi = await this.preparePrompt(s, model);
|
|
1071
1201
|
const native = nativePromptInput(text, parts ?? []);
|
|
1202
|
+
// OpenChamber treats instruction writes as best effort, so a rejected delivery must not fail the prompt;
|
|
1203
|
+
// the entries stay undelivered and are offered again before the next prompt.
|
|
1204
|
+
if (!noReply)
|
|
1205
|
+
await this.deliverInstructions(s, pi).catch((error) => console.error(`[instructions] delivery to ${s.info.id} failed:`, error));
|
|
1072
1206
|
const userMsg = this.publishPrompt(s, text, model, messageID, parts, metadata, noReply);
|
|
1073
1207
|
if (noReply) {
|
|
1074
1208
|
await this.admitContext(s, pi, userMsg, native.message);
|
|
@@ -1078,6 +1212,53 @@ export class SessionManager {
|
|
|
1078
1212
|
return;
|
|
1079
1213
|
await this.bindPromptState(s);
|
|
1080
1214
|
}
|
|
1215
|
+
/**
|
|
1216
|
+
* What the model has is read from the active native branch, not stored beside it: each delivery carries the
|
|
1217
|
+
* full delivered map in `details`, so after a revert, unrevert, fork or compaction the last delivery still in
|
|
1218
|
+
* context says exactly what to send again. Deliveries before a compaction's kept range are no longer in context
|
|
1219
|
+
* (OpenCode rewinds its instruction state the same way, core/session/projector.ts).
|
|
1220
|
+
*/
|
|
1221
|
+
async deliverInstructions(s, pi) {
|
|
1222
|
+
const instructions = Object.entries(s.instructions ?? {});
|
|
1223
|
+
if (instructions.length === 0)
|
|
1224
|
+
return;
|
|
1225
|
+
const delivered = deliveredInstructions(branchEntries(await getEntries(pi)));
|
|
1226
|
+
const pending = instructions
|
|
1227
|
+
.filter(([key, entry]) => entry.removed
|
|
1228
|
+
? key in delivered
|
|
1229
|
+
: !(key in delivered) || JSON.stringify(entry.value) !== JSON.stringify(delivered[key]))
|
|
1230
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
1231
|
+
if (pending.length === 0)
|
|
1232
|
+
return;
|
|
1233
|
+
const text = pending
|
|
1234
|
+
.map(([key, entry]) => {
|
|
1235
|
+
if (entry.removed)
|
|
1236
|
+
return `The context under "${key}" no longer applies. Disregard it.`;
|
|
1237
|
+
const block = `<context key="${key}">\n${typeof entry.value === "string" ? entry.value : JSON.stringify(entry.value, null, 2)}\n</context>`;
|
|
1238
|
+
return key in delivered
|
|
1239
|
+
? `The context under "${key}" changed and supersedes the previous value:\n${block}`
|
|
1240
|
+
: block;
|
|
1241
|
+
})
|
|
1242
|
+
.join("\n\n");
|
|
1243
|
+
const snapshot = { ...delivered };
|
|
1244
|
+
for (const [key, entry] of pending) {
|
|
1245
|
+
if (entry.removed)
|
|
1246
|
+
delete snapshot[key];
|
|
1247
|
+
else
|
|
1248
|
+
snapshot[key] = entry.value;
|
|
1249
|
+
}
|
|
1250
|
+
const result = await pi.command({
|
|
1251
|
+
type: "send_custom_message",
|
|
1252
|
+
customType: INSTRUCTIONS_CUSTOM_TYPE,
|
|
1253
|
+
content: [{ type: "text", text }],
|
|
1254
|
+
display: false,
|
|
1255
|
+
details: { instructions: snapshot },
|
|
1256
|
+
triggerTurn: false,
|
|
1257
|
+
});
|
|
1258
|
+
if (!result.success)
|
|
1259
|
+
throw new Error(result.error ?? "instruction delivery rejected");
|
|
1260
|
+
await this.bindPromptState(s);
|
|
1261
|
+
}
|
|
1081
1262
|
/**
|
|
1082
1263
|
* No-reply context (v2 synthetic, v1 noReply) joins the native conversation without a turn:
|
|
1083
1264
|
* OmO appends it now when idle, or after the running turn, so the next prompt carries it.
|
|
@@ -1494,6 +1675,7 @@ export class SessionManager {
|
|
|
1494
1675
|
(reg?.title && reg.title !== "New session" && reg.title !== sc.fallbackTitle ? "user" : "fallback");
|
|
1495
1676
|
const useRegistry = titleSource === "user" || (sc.titleSource !== "native" && titleSource === "generated");
|
|
1496
1677
|
const resumed = dormantModel(sc, reg?.model);
|
|
1678
|
+
const tree = durableShellTree(reg?.tree ?? { messageEntries: {} });
|
|
1497
1679
|
const info = {
|
|
1498
1680
|
id: ocId,
|
|
1499
1681
|
projectID: sc.directory,
|
|
@@ -1518,10 +1700,12 @@ export class SessionManager {
|
|
|
1518
1700
|
created: sc.created,
|
|
1519
1701
|
updated: Math.max(sc.updated, reg?.updated ?? 0),
|
|
1520
1702
|
...(reg?.archived ? { archived: reg.archived } : {}),
|
|
1703
|
+
...(reg?.viewed !== undefined ? { viewed: reg.viewed } : {}),
|
|
1704
|
+
...(tree.lastIdle !== undefined ? { idle: tree.lastIdle } : {}),
|
|
1521
1705
|
},
|
|
1522
1706
|
};
|
|
1523
1707
|
const managed = {
|
|
1524
|
-
tree
|
|
1708
|
+
tree,
|
|
1525
1709
|
titleSource: useRegistry ? titleSource : (sc.titleSource ?? "fallback"),
|
|
1526
1710
|
info,
|
|
1527
1711
|
pi: undefined,
|
|
@@ -1534,6 +1718,7 @@ export class SessionManager {
|
|
|
1534
1718
|
hasPendingInteraction: () => false,
|
|
1535
1719
|
hasRunningChildren: () => false,
|
|
1536
1720
|
...(reg?.lastStop !== undefined ? { lastStop: reg.lastStop } : {}),
|
|
1721
|
+
...(reg?.instructions !== undefined ? { instructions: structuredClone(reg.instructions) } : {}),
|
|
1537
1722
|
};
|
|
1538
1723
|
this.sessions.set(ocId, managed);
|
|
1539
1724
|
if (reg?.connectorOwned === true)
|
|
@@ -1608,7 +1793,9 @@ export class SessionManager {
|
|
|
1608
1793
|
...(s.info.title !== "New session" ? { title: s.info.title } : {}),
|
|
1609
1794
|
...(s.titleSource !== undefined ? { titleSource: s.titleSource } : {}),
|
|
1610
1795
|
...(s.info.time.archived !== undefined ? { archived: s.info.time.archived } : {}),
|
|
1796
|
+
...(s.info.time.viewed !== undefined ? { viewed: s.info.time.viewed } : {}),
|
|
1611
1797
|
...(s.info.metadata !== undefined ? { metadata: s.info.metadata } : {}),
|
|
1798
|
+
...(s.instructions !== undefined ? { instructions: s.instructions } : {}),
|
|
1612
1799
|
...(s.info.agent !== undefined ? { agent: s.info.agent } : {}),
|
|
1613
1800
|
...(s.info.parentID !== undefined ? { parentID: s.info.parentID } : {}),
|
|
1614
1801
|
...(s.info.parentMessageID !== undefined ? { parentMessageID: s.info.parentMessageID } : {}),
|
|
@@ -1629,6 +1816,47 @@ export class SessionManager {
|
|
|
1629
1816
|
}
|
|
1630
1817
|
/** Keep the existing event entry point for RPC wiring and test harnesses. */
|
|
1631
1818
|
onPiEvent(s, ev) {
|
|
1819
|
+
const interrupted = s.interruptRequested;
|
|
1632
1820
|
this.events.onPiEvent(s, ev);
|
|
1821
|
+
if (ev.type === "agent_idle" && s.info.time.idle !== undefined) {
|
|
1822
|
+
recordTurn(s);
|
|
1823
|
+
const last = s.messages?.findLast((message) => message.info.role === "assistant" &&
|
|
1824
|
+
!message.info.summary &&
|
|
1825
|
+
!message.info.id.startsWith("msg_background_") &&
|
|
1826
|
+
!s.localMessageIds?.has(message.info.id));
|
|
1827
|
+
if (last?.info.role === "assistant") {
|
|
1828
|
+
const error = interrupted ? { name: "MessageAbortedError", data: { message: "Cancelled" } } : last.info.error;
|
|
1829
|
+
const outcome = interrupted ? "interrupted" : (s.info.outcome ?? "succeeded");
|
|
1830
|
+
s.info.outcome = outcome;
|
|
1831
|
+
(s.tree.idleBoundaries ??= {})[last.info.id] = {
|
|
1832
|
+
at: s.info.time.idle,
|
|
1833
|
+
outcome,
|
|
1834
|
+
...(error !== undefined ? { error } : {}),
|
|
1835
|
+
};
|
|
1836
|
+
}
|
|
1837
|
+
this.persistMeta(s);
|
|
1838
|
+
}
|
|
1633
1839
|
}
|
|
1634
1840
|
}
|
|
1841
|
+
const INSTRUCTIONS_CUSTOM_TYPE = "antechamber.instructions";
|
|
1842
|
+
/** The instruction map the model holds on this branch: the last delivery inside the newest compaction's kept range. */
|
|
1843
|
+
function deliveredInstructions(branch) {
|
|
1844
|
+
let start = 0;
|
|
1845
|
+
for (let index = branch.length - 1; index >= 0; index--) {
|
|
1846
|
+
const entry = branch[index];
|
|
1847
|
+
if (entry.type !== "compaction")
|
|
1848
|
+
continue;
|
|
1849
|
+
const kept = branch.findIndex((candidate) => candidate.id === entry.firstKeptEntryId);
|
|
1850
|
+
start = kept >= 0 ? kept : index + 1;
|
|
1851
|
+
break;
|
|
1852
|
+
}
|
|
1853
|
+
for (let index = branch.length - 1; index >= start; index--) {
|
|
1854
|
+
const entry = branch[index];
|
|
1855
|
+
if (entry?.type !== "custom_message" || entry.customType !== INSTRUCTIONS_CUSTOM_TYPE)
|
|
1856
|
+
continue;
|
|
1857
|
+
const details = entry.details;
|
|
1858
|
+
if (details?.instructions !== null && typeof details?.instructions === "object")
|
|
1859
|
+
return { ...details.instructions };
|
|
1860
|
+
}
|
|
1861
|
+
return {};
|
|
1862
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
type Model = {
|
|
2
|
+
providerID: string;
|
|
3
|
+
id: string;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* A hang guard, not a budget: OpenChamber sizes its own small-model deadline by prompt length up to 900 s
|
|
7
|
+
* (server/lib/small-model/index.js generationBudgetMs) and its disconnect cancels the run here, and OpenCode's
|
|
8
|
+
* generate.text has no server deadline. This only has to outlast the longest budget a caller may give.
|
|
9
|
+
*/
|
|
10
|
+
export declare const GENERATION_DEADLINE_MS: number;
|
|
11
|
+
/** Own the auxiliary agents separately from user sessions, with no admission queue. */
|
|
12
|
+
export declare class TextGeneration {
|
|
13
|
+
private readonly active;
|
|
14
|
+
private stopped;
|
|
15
|
+
generate(directory: string, prompt: string, model?: Model, signal?: AbortSignal): Promise<string>;
|
|
16
|
+
dispose(): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
/** Run a model turn in an agent with no persistent session, never in a user's chat. */
|
|
19
|
+
export declare function generateText(directory: string, prompt: string, model?: Model, caller?: AbortSignal): Promise<string>;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { PiRpcClient } from "./pi-rpc.js";
|
|
2
|
+
/**
|
|
3
|
+
* A hang guard, not a budget: OpenChamber sizes its own small-model deadline by prompt length up to 900 s
|
|
4
|
+
* (server/lib/small-model/index.js generationBudgetMs) and its disconnect cancels the run here, and OpenCode's
|
|
5
|
+
* generate.text has no server deadline. This only has to outlast the longest budget a caller may give.
|
|
6
|
+
*/
|
|
7
|
+
export const GENERATION_DEADLINE_MS = 15 * 60 * 1000;
|
|
8
|
+
/** Own the auxiliary agents separately from user sessions, with no admission queue. */
|
|
9
|
+
export class TextGeneration {
|
|
10
|
+
active = new Map();
|
|
11
|
+
stopped = false;
|
|
12
|
+
async generate(directory, prompt, model, signal) {
|
|
13
|
+
if (this.stopped)
|
|
14
|
+
throw new Error("Text generation is stopping");
|
|
15
|
+
if (this.active.size >= 2)
|
|
16
|
+
throw new Error("Text generation is at capacity");
|
|
17
|
+
const cancellation = new AbortController();
|
|
18
|
+
const result = generateText(directory, prompt, model, signal ? AbortSignal.any([signal, cancellation.signal]) : cancellation.signal);
|
|
19
|
+
this.active.set(cancellation, result);
|
|
20
|
+
try {
|
|
21
|
+
return await result;
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
this.active.delete(cancellation);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async dispose() {
|
|
28
|
+
this.stopped = true;
|
|
29
|
+
for (const cancellation of this.active.keys())
|
|
30
|
+
cancellation.abort(new Error("Text generation is stopping"));
|
|
31
|
+
await Promise.allSettled(this.active.values());
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Run a model turn in an agent with no persistent session, never in a user's chat. */
|
|
35
|
+
export async function generateText(directory, prompt, model, caller) {
|
|
36
|
+
caller?.throwIfAborted();
|
|
37
|
+
// Startup overrides initialize agent state without persisting the default, unlike set_model.
|
|
38
|
+
// senpi dist/cli/args.js:93-104 -> main.js:561-576,843 -> core/sdk.js:125,292-298.
|
|
39
|
+
const pi = new PiRpcClient({
|
|
40
|
+
cwd: directory,
|
|
41
|
+
args: ["--no-session", "--no-tools", ...(model ? ["--provider", model.providerID, "--model", model.id] : [])],
|
|
42
|
+
});
|
|
43
|
+
const cancellation = new AbortController();
|
|
44
|
+
const signal = caller ? AbortSignal.any([caller, cancellation.signal]) : cancellation.signal;
|
|
45
|
+
// This budget covers startup/model resolution, admission, the turn, and the result read.
|
|
46
|
+
const timer = setTimeout(() => cancellation.abort(new Error("Text generation timed out")), GENERATION_DEADLINE_MS);
|
|
47
|
+
try {
|
|
48
|
+
const finished = new Promise((resolve, reject) => {
|
|
49
|
+
const cleanup = () => {
|
|
50
|
+
pi.off("event", onEvent);
|
|
51
|
+
signal.removeEventListener("abort", onAbort);
|
|
52
|
+
};
|
|
53
|
+
const onAbort = () => {
|
|
54
|
+
cleanup();
|
|
55
|
+
reject(signal.reason);
|
|
56
|
+
};
|
|
57
|
+
const onEvent = (event) => {
|
|
58
|
+
if (event.type !== "agent_end")
|
|
59
|
+
return;
|
|
60
|
+
cleanup();
|
|
61
|
+
resolve();
|
|
62
|
+
};
|
|
63
|
+
pi.onEvent(onEvent);
|
|
64
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
65
|
+
});
|
|
66
|
+
// A prompt RPC may acknowledge admission before the agent finishes; agent_end
|
|
67
|
+
// is the completion boundary before reading the resulting entries.
|
|
68
|
+
const admitted = pi.command({ type: "prompt", message: prompt }, undefined, signal).then((response) => {
|
|
69
|
+
if (!response.success)
|
|
70
|
+
throw new Error(response.error ?? "Text generation failed");
|
|
71
|
+
});
|
|
72
|
+
await Promise.all([admitted, finished]);
|
|
73
|
+
const history = await pi.command({ type: "get_entries" }, undefined, signal);
|
|
74
|
+
if (!history.success || !Array.isArray(history.data?.entries))
|
|
75
|
+
throw new Error("Text generation produced no readable history");
|
|
76
|
+
const assistant = history.data.entries.findLast((entry) => entry.type === "message" && entry.message?.role === "assistant");
|
|
77
|
+
const content = assistant?.message?.content;
|
|
78
|
+
const text = typeof content === "string"
|
|
79
|
+
? content
|
|
80
|
+
: content
|
|
81
|
+
?.filter((part) => part.type === "text")
|
|
82
|
+
.map((part) => part.text ?? "")
|
|
83
|
+
.join("");
|
|
84
|
+
if (!text)
|
|
85
|
+
throw new Error("Text generation produced no text");
|
|
86
|
+
return text;
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
cancellation.abort();
|
|
91
|
+
await pi.dispose();
|
|
92
|
+
}
|
|
93
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -48,6 +48,7 @@ export interface OCSession {
|
|
|
48
48
|
created: number;
|
|
49
49
|
updated: number;
|
|
50
50
|
idle?: number;
|
|
51
|
+
viewed?: number;
|
|
51
52
|
compacting?: number;
|
|
52
53
|
/** Epoch ms — presence marks the session archived (SDK v2 session.update). */
|
|
53
54
|
archived?: number;
|
|
@@ -67,6 +68,8 @@ export interface OCUserMessage {
|
|
|
67
68
|
};
|
|
68
69
|
/** No-reply context admitted without a turn (v2 lists it as `type: "synthetic"`). */
|
|
69
70
|
synthetic?: boolean;
|
|
71
|
+
/** Human-readable label for a v2 synthetic notice. */
|
|
72
|
+
description?: string;
|
|
70
73
|
/** Client metadata sent with the prompt (OpenChamber 2.x). */
|
|
71
74
|
metadata?: Record<string, unknown>;
|
|
72
75
|
}
|
|
@@ -291,6 +294,33 @@ export type OCEvent = {
|
|
|
291
294
|
properties: {
|
|
292
295
|
server: string;
|
|
293
296
|
};
|
|
297
|
+
} | {
|
|
298
|
+
type: "filesystem.changed";
|
|
299
|
+
properties: {
|
|
300
|
+
file: string;
|
|
301
|
+
event: "change" | "unlink";
|
|
302
|
+
};
|
|
303
|
+
} | {
|
|
304
|
+
type: "project.updated";
|
|
305
|
+
properties: {
|
|
306
|
+
id: string;
|
|
307
|
+
canonical: string;
|
|
308
|
+
time: {
|
|
309
|
+
created: number;
|
|
310
|
+
updated: number;
|
|
311
|
+
active: number;
|
|
312
|
+
};
|
|
313
|
+
sandboxes: string[];
|
|
314
|
+
vcs: string;
|
|
315
|
+
};
|
|
316
|
+
} | {
|
|
317
|
+
type: "provider.updated" | "model.updated" | "command.updated";
|
|
318
|
+
properties: Record<string, never>;
|
|
319
|
+
} | {
|
|
320
|
+
type: "vcs.branch.updated";
|
|
321
|
+
properties: {
|
|
322
|
+
branch?: string;
|
|
323
|
+
};
|
|
294
324
|
} | {
|
|
295
325
|
type: "session.created";
|
|
296
326
|
properties: {
|
|
@@ -301,6 +331,30 @@ export type OCEvent = {
|
|
|
301
331
|
properties: {
|
|
302
332
|
info: OCSession;
|
|
303
333
|
};
|
|
334
|
+
} | {
|
|
335
|
+
type: "session.renamed";
|
|
336
|
+
properties: {
|
|
337
|
+
sessionID: string;
|
|
338
|
+
title: string;
|
|
339
|
+
};
|
|
340
|
+
} | {
|
|
341
|
+
type: "session.agent.selected";
|
|
342
|
+
properties: {
|
|
343
|
+
sessionID: string;
|
|
344
|
+
agent: string;
|
|
345
|
+
previous?: string;
|
|
346
|
+
};
|
|
347
|
+
} | {
|
|
348
|
+
type: "session.archived" | "session.unarchived";
|
|
349
|
+
properties: {
|
|
350
|
+
sessionID: string;
|
|
351
|
+
};
|
|
352
|
+
} | {
|
|
353
|
+
type: "session.viewed";
|
|
354
|
+
properties: {
|
|
355
|
+
sessionID: string;
|
|
356
|
+
idle: number;
|
|
357
|
+
};
|
|
304
358
|
} | {
|
|
305
359
|
type: "session.revert.staged";
|
|
306
360
|
properties: {
|
|
@@ -327,6 +381,12 @@ export type OCEvent = {
|
|
|
327
381
|
reason: string;
|
|
328
382
|
inputID: string;
|
|
329
383
|
};
|
|
384
|
+
} | {
|
|
385
|
+
type: "session.compaction.delta";
|
|
386
|
+
properties: {
|
|
387
|
+
sessionID: string;
|
|
388
|
+
text: string;
|
|
389
|
+
};
|
|
330
390
|
} | {
|
|
331
391
|
type: "session.compaction.ended";
|
|
332
392
|
properties: {
|
package/dist/v2.d.ts
CHANGED
|
@@ -123,6 +123,8 @@ export interface V2Session {
|
|
|
123
123
|
time: {
|
|
124
124
|
created: number;
|
|
125
125
|
updated: number;
|
|
126
|
+
idle?: number;
|
|
127
|
+
viewed?: number;
|
|
126
128
|
archived?: number;
|
|
127
129
|
};
|
|
128
130
|
title: string;
|
|
@@ -148,7 +150,12 @@ export interface V2Message {
|
|
|
148
150
|
completed?: number;
|
|
149
151
|
streamed?: number;
|
|
150
152
|
};
|
|
151
|
-
type: "user" | "assistant" | "synthetic";
|
|
153
|
+
type: "user" | "assistant" | "synthetic" | "idle" | "compaction";
|
|
154
|
+
status?: "running" | "completed" | "failed";
|
|
155
|
+
reason?: string;
|
|
156
|
+
summary?: string;
|
|
157
|
+
outcome?: "succeeded" | "failed" | "interrupted";
|
|
158
|
+
description?: string;
|
|
152
159
|
text?: string;
|
|
153
160
|
files?: Array<Record<string, unknown>>;
|
|
154
161
|
agents?: Array<Record<string, unknown>>;
|