antechamber 1.0.40 → 2.0.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 +126 -0
- package/README.md +88 -141
- package/bin/opencode-antechamber +14 -4
- 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/cli.d.ts +1 -1
- package/dist/cli.js +1 -1
- package/dist/commands/context.d.ts +5 -2
- package/dist/commands/context.js +8 -4
- package/dist/commands/doctor.js +4 -3
- package/dist/commands/setup.js +9 -28
- package/dist/config.d.ts +6 -2
- package/dist/config.js +18 -4
- package/dist/file-watch.d.ts +35 -0
- package/dist/file-watch.js +163 -0
- package/dist/latency.d.ts +10 -0
- package/dist/latency.js +115 -0
- package/dist/misc-routes.js +14 -12
- package/dist/pi-event-translation.js +44 -2
- package/dist/route-services.d.ts +8 -3
- package/dist/routes-v1.d.ts +1 -1
- package/dist/routes-v1.js +29 -418
- package/dist/routes-v2.js +252 -34
- package/dist/self-update.d.ts +2 -1
- package/dist/self-update.js +5 -1
- package/dist/server.d.ts +17 -3
- package/dist/server.js +191 -18
- 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 +19 -10
- package/dist/session-store.js +266 -128
- package/dist/session-tree.d.ts +15 -1
- package/dist/session-tree.js +2 -2
- package/dist/sessions.d.ts +85 -1
- package/dist/sessions.js +238 -10
- package/dist/sse.d.ts +0 -2
- package/dist/sse.js +13 -46
- package/dist/text-generation.d.ts +20 -0
- package/dist/text-generation.js +93 -0
- package/dist/tool-map.js +1 -3
- package/dist/types.d.ts +60 -0
- package/dist/v2.d.ts +8 -1
- package/dist/v2.js +24 -4
- package/dist/work-status.js +1 -1
- package/extensions/antechamber/README.md +100 -0
- package/extensions/antechamber/bin/opencode-antechamber +42 -0
- package/extensions/{omo-dag/docs/DESIGN.md → antechamber/docs/GRAPH.md} +5 -5
- package/extensions/{omo-work/docs/DESIGN.md → antechamber/docs/WORK.md} +3 -3
- package/extensions/antechamber/package.json +60 -0
- package/extensions/{omo-dag → antechamber}/panel/index.html +8 -1
- package/extensions/antechamber/panel/main.js +261 -0
- package/extensions/antechamber/runtime/launcher.js +201 -0
- package/extensions/antechamber/service/main.js +884 -0
- package/package.json +75 -26
- package/extensions/omo-compat/README.md +0 -48
- package/extensions/omo-compat/icon.svg +0 -1
- package/extensions/omo-compat/package.json +0 -43
- package/extensions/omo-dag/README.md +0 -167
- package/extensions/omo-dag/package.json +0 -33
- package/extensions/omo-dag/panel/main.js +0 -176
- package/extensions/omo-dag/service/main.js +0 -457
- package/extensions/omo-work/README.md +0 -88
- package/extensions/omo-work/package.json +0 -30
- package/extensions/omo-work/service/main.js +0 -149
- /package/extensions/{omo-dag → antechamber}/icon.svg +0 -0
- /package/extensions/{omo-work → antechamber}/status/index.html +0 -0
- /package/extensions/{omo-work → antechamber}/status/main.js +0 -0
package/dist/sessions.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { SubagentBridge } from "./subagents.js";
|
|
|
12
12
|
import { MonitorSessions } from "./monitor-sessions.js";
|
|
13
13
|
import { WorkStatusBridge } from "./work-status.js";
|
|
14
14
|
import { OmoShells } from "./omo-shells.js";
|
|
15
|
-
import { SessionStore, type TitleSource } from "./session-store.js";
|
|
15
|
+
import { SessionStore, type InstructionEntryState, type TitleSource } from "./session-store.js";
|
|
16
16
|
import type { OCAssistantMessage, OCEvent, OCMessageWithParts, OCModelRef, OCPart, OCPromptPart, OCSession, OCSessionStatus, OCToolPart } from "./types.js";
|
|
17
17
|
export { ADAPTER_VERSION } from "./version.js";
|
|
18
18
|
interface StreamingState {
|
|
@@ -33,6 +33,9 @@ export interface ManagedSession extends ThinkingState {
|
|
|
33
33
|
accepted: boolean;
|
|
34
34
|
delivered: boolean;
|
|
35
35
|
}>;
|
|
36
|
+
instructions?: Record<string, InstructionEntryState>;
|
|
37
|
+
/** Suppress model-selection events until the session creation record has its final model. */
|
|
38
|
+
initializing?: boolean;
|
|
36
39
|
tree: SessionTreeState;
|
|
37
40
|
info: OCSession;
|
|
38
41
|
/** undefined while dormant (restored from disk, no pi process spawned yet). */
|
|
@@ -51,6 +54,8 @@ export interface ManagedSession extends ThinkingState {
|
|
|
51
54
|
/** A manual compaction started while idle; its end returns the session to idle. */
|
|
52
55
|
compactingFromIdle?: boolean;
|
|
53
56
|
compactionID?: string;
|
|
57
|
+
/** The native reason of the running compaction, for its failure event. */
|
|
58
|
+
compactionReason?: string;
|
|
54
59
|
/** Retries sessionRpc still owes a `compact` command that OmO cancels on its own. */
|
|
55
60
|
compactRetry?: {
|
|
56
61
|
left: number;
|
|
@@ -142,6 +147,8 @@ export declare class SessionManager {
|
|
|
142
147
|
* global/recent view; `list(directory)` stays the per-project source of truth.
|
|
143
148
|
*/
|
|
144
149
|
listAll(): OCSession[];
|
|
150
|
+
/** Reuse an already-running agent for directory-scoped catalog reads. Never revives a dormant session. */
|
|
151
|
+
liveClient(directory: string): PiRpcClient | undefined;
|
|
145
152
|
private sessionInfo;
|
|
146
153
|
get(sessionId: string): ManagedSession | undefined;
|
|
147
154
|
remove(sessionId: string): Promise<boolean>;
|
|
@@ -158,7 +165,77 @@ export declare class SessionManager {
|
|
|
158
165
|
metadata?: Record<string, unknown> | undefined;
|
|
159
166
|
agent?: string | undefined;
|
|
160
167
|
}): Promise<OCSession | undefined>;
|
|
168
|
+
/** Mark only the idle boundary that the caller actually observed. */
|
|
169
|
+
view(sessionId: string, idle: number): void;
|
|
170
|
+
putInstruction(sessionId: string, key: string, value: unknown): void;
|
|
171
|
+
removeInstruction(sessionId: string, key: string): void;
|
|
161
172
|
messages(sessionId: string): OCMessageWithParts[] | undefined;
|
|
173
|
+
compaction(sessionId: string, messageID: string): {
|
|
174
|
+
id: string;
|
|
175
|
+
type: "compaction";
|
|
176
|
+
time: {
|
|
177
|
+
created: number;
|
|
178
|
+
};
|
|
179
|
+
status: "completed";
|
|
180
|
+
reason: "auto" | "manual";
|
|
181
|
+
summary: string;
|
|
182
|
+
} | {
|
|
183
|
+
id: string;
|
|
184
|
+
type: "compaction";
|
|
185
|
+
time: {
|
|
186
|
+
created: number;
|
|
187
|
+
};
|
|
188
|
+
status: "running";
|
|
189
|
+
reason: "manual";
|
|
190
|
+
summary: string;
|
|
191
|
+
} | undefined;
|
|
192
|
+
/** Synchronous admission owns the ID before any native work can yield to another request. */
|
|
193
|
+
admitCompaction(sessionId: string, messageID: string): {
|
|
194
|
+
kind: "conflict";
|
|
195
|
+
item?: never;
|
|
196
|
+
} | {
|
|
197
|
+
kind: "existing";
|
|
198
|
+
item: {
|
|
199
|
+
id: string;
|
|
200
|
+
type: "compaction";
|
|
201
|
+
time: {
|
|
202
|
+
created: number;
|
|
203
|
+
};
|
|
204
|
+
status: "completed";
|
|
205
|
+
reason: "auto" | "manual";
|
|
206
|
+
summary: string;
|
|
207
|
+
} | {
|
|
208
|
+
id: string;
|
|
209
|
+
type: "compaction";
|
|
210
|
+
time: {
|
|
211
|
+
created: number;
|
|
212
|
+
};
|
|
213
|
+
status: "running";
|
|
214
|
+
reason: "manual";
|
|
215
|
+
summary: string;
|
|
216
|
+
};
|
|
217
|
+
} | {
|
|
218
|
+
kind: "admitted";
|
|
219
|
+
item: {
|
|
220
|
+
id: string;
|
|
221
|
+
type: "compaction";
|
|
222
|
+
time: {
|
|
223
|
+
created: number;
|
|
224
|
+
};
|
|
225
|
+
status: "completed";
|
|
226
|
+
reason: "auto" | "manual";
|
|
227
|
+
summary: string;
|
|
228
|
+
} | {
|
|
229
|
+
id: string;
|
|
230
|
+
type: "compaction";
|
|
231
|
+
time: {
|
|
232
|
+
created: number;
|
|
233
|
+
};
|
|
234
|
+
status: "running";
|
|
235
|
+
reason: "manual";
|
|
236
|
+
summary: string;
|
|
237
|
+
};
|
|
238
|
+
};
|
|
162
239
|
/** Append to canonical history, before the hidden redo branch while reverted. */
|
|
163
240
|
appendMessage(sessionId: string, message: OCMessageWithParts): void;
|
|
164
241
|
private operation;
|
|
@@ -212,6 +289,13 @@ export declare class SessionManager {
|
|
|
212
289
|
private preparePrompt;
|
|
213
290
|
private publishPrompt;
|
|
214
291
|
private promptLive;
|
|
292
|
+
/**
|
|
293
|
+
* What the model has is read from the active native branch, not stored beside it: each delivery carries the
|
|
294
|
+
* full delivered map in `details`, so after a revert, unrevert, fork or compaction the last delivery still in
|
|
295
|
+
* context says exactly what to send again. Deliveries before a compaction's kept range are no longer in context
|
|
296
|
+
* (OpenCode rewinds its instruction state the same way, core/session/projector.ts).
|
|
297
|
+
*/
|
|
298
|
+
private deliverInstructions;
|
|
215
299
|
/**
|
|
216
300
|
* No-reply context (v2 synthetic, v1 noReply) joins the native conversation without a turn:
|
|
217
301
|
* OmO appends it now when idle, or after the running turn, so the next prompt carries it.
|
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
|
+
}
|
package/dist/sse.d.ts
CHANGED
|
@@ -3,9 +3,7 @@ import type { ServerResponse } from "node:http";
|
|
|
3
3
|
import type { OCEvent } from "./types.js";
|
|
4
4
|
import type { TodoUpdated } from "./todo-bridge.js";
|
|
5
5
|
export type SSESubscriber = {
|
|
6
|
-
flavor: "v1" | "v2";
|
|
7
6
|
directory?: string;
|
|
8
|
-
global: boolean;
|
|
9
7
|
};
|
|
10
8
|
/** The session an event belongs to, when it names one. */
|
|
11
9
|
export declare function eventSessionID(event: OCEvent | TodoUpdated): string | undefined;
|
package/dist/sse.js
CHANGED
|
@@ -48,9 +48,7 @@ export class SSE {
|
|
|
48
48
|
: event.type === "todo.updated"
|
|
49
49
|
? [{ id: `evt_${randomUUID()}`, type: event.type, location: { directory }, data: event.properties }]
|
|
50
50
|
: this.v2events.translate(directory, event);
|
|
51
|
-
const records = v2Frames.
|
|
52
|
-
? [this.record(directory, { v1: event })]
|
|
53
|
-
: v2Frames.map((v2, index) => this.record(directory, index === 0 ? { v1: event, v2 } : { v2 }));
|
|
51
|
+
const records = v2Frames.map((v2) => this.record(directory, { v2 }));
|
|
54
52
|
for (const record of records)
|
|
55
53
|
this.retain(record);
|
|
56
54
|
for (const [res, subscriber] of this.clients) {
|
|
@@ -59,7 +57,7 @@ export class SSE {
|
|
|
59
57
|
continue;
|
|
60
58
|
}
|
|
61
59
|
for (const record of records) {
|
|
62
|
-
if (
|
|
60
|
+
if (subscriber.directory !== undefined && subscriber.directory !== directory)
|
|
63
61
|
continue;
|
|
64
62
|
if (!this.writeRecord(res, subscriber, record))
|
|
65
63
|
break;
|
|
@@ -102,22 +100,14 @@ export class SSE {
|
|
|
102
100
|
this.clients.delete(res);
|
|
103
101
|
});
|
|
104
102
|
// OpenCode emits server.connected on subscribe; OpenChamber waits for it.
|
|
105
|
-
const connected =
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
: { type: "server.connected", properties: {} };
|
|
114
|
-
const initial = subscriber.flavor === "v1" && subscriber.global
|
|
115
|
-
? {
|
|
116
|
-
directory: subscriber.directory ?? this.options.defaultDirectory,
|
|
117
|
-
payload: { id: `evt_${randomUUID()}`, ...connected },
|
|
118
|
-
}
|
|
119
|
-
: connected;
|
|
120
|
-
write(res, `data: ${JSON.stringify(initial)}\n\n`);
|
|
103
|
+
const connected = {
|
|
104
|
+
id: `evt_${Date.now()}`,
|
|
105
|
+
created: Date.now(),
|
|
106
|
+
type: "server.connected",
|
|
107
|
+
location: { directory: subscriber.directory ?? this.options.defaultDirectory },
|
|
108
|
+
data: {},
|
|
109
|
+
};
|
|
110
|
+
write(res, `data: ${JSON.stringify(connected)}\n\n`);
|
|
121
111
|
const cursor = Number(lastEventID);
|
|
122
112
|
if (!Number.isSafeInteger(cursor) || cursor < 0)
|
|
123
113
|
return;
|
|
@@ -135,35 +125,12 @@ export class SSE {
|
|
|
135
125
|
this.replay.shift();
|
|
136
126
|
}
|
|
137
127
|
writeRecord(res, subscriber, record) {
|
|
138
|
-
if (
|
|
128
|
+
if (subscriber.directory !== undefined && subscriber.directory !== record.directory)
|
|
139
129
|
return true;
|
|
140
|
-
const data =
|
|
141
|
-
? record.v2
|
|
142
|
-
: record.v1 === undefined
|
|
143
|
-
? undefined
|
|
144
|
-
: subscriber.global
|
|
145
|
-
? {
|
|
146
|
-
directory: record.directory,
|
|
147
|
-
project: record.directory,
|
|
148
|
-
payload: {
|
|
149
|
-
id: `evt_${record.id}`,
|
|
150
|
-
...record.v1,
|
|
151
|
-
properties: {
|
|
152
|
-
...record.v1.properties,
|
|
153
|
-
...(record.v1.type === "session.created" ||
|
|
154
|
-
record.v1.type === "session.updated" ||
|
|
155
|
-
record.v1.type === "session.deleted"
|
|
156
|
-
? { sessionID: record.v1.properties.info.id }
|
|
157
|
-
: {}),
|
|
158
|
-
},
|
|
159
|
-
},
|
|
160
|
-
}
|
|
161
|
-
: record.v1;
|
|
130
|
+
const data = record.v2;
|
|
162
131
|
if (data === undefined)
|
|
163
132
|
return true;
|
|
164
|
-
const frame =
|
|
165
|
-
? `data: ${JSON.stringify(data)}\n\n`
|
|
166
|
-
: `id: ${record.id}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
133
|
+
const frame = `id: ${record.id}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
167
134
|
return this.writeSSE(res, frame);
|
|
168
135
|
}
|
|
169
136
|
}
|
|
@@ -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 {};
|