antechamber 0.2.34 → 0.2.36
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 +18 -0
- package/dist/pi-event-translation.js +1 -0
- package/dist/pi-rpc.d.ts +2 -0
- package/dist/pi-rpc.js +11 -0
- package/dist/session-tree.d.ts +2 -0
- package/dist/session-tree.js +21 -4
- package/dist/sessions.js +10 -4
- package/dist/subagents.d.ts +24 -0
- package/dist/subagents.js +86 -14
- package/extensions/omo-compat/package.json +1 -1
- package/extensions/omo-dag/package.json +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,24 @@ and this project uses [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.36] - 2026-10-20
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Keep large sessions usable after their persisted history exceeds the 64 MiB RPC frame limit.
|
|
15
|
+
History synchronization now reads the persisted prefix locally and requests only the native tail,
|
|
16
|
+
preserving the selected branch when sending messages or reopening a session.
|
|
17
|
+
|
|
18
|
+
## [0.2.35] - 2026-10-19
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Keep OmO background subagents running in OpenChamber after the parent turn is stopped or its agent
|
|
23
|
+
process exits. Child status now follows OmO's `omo.task.updated` snapshots and task records, and a
|
|
24
|
+
child an older version wrongly marked cancelled recovers from its running record. Stopping the turn
|
|
25
|
+
no longer unblocks fork and revert while a background subagent runs; the error now names the
|
|
26
|
+
running subagent instead of asking to stop the turn.
|
|
27
|
+
|
|
10
28
|
## [0.2.34] - 2026-10-19
|
|
11
29
|
|
|
12
30
|
### Fixed
|
package/dist/pi-rpc.d.ts
CHANGED
|
@@ -71,8 +71,10 @@ export declare class PiRpcClient extends EventEmitter {
|
|
|
71
71
|
private disposing;
|
|
72
72
|
private agentActive;
|
|
73
73
|
private compacting;
|
|
74
|
+
private sessionPath;
|
|
74
75
|
get pid(): number | undefined;
|
|
75
76
|
get isBusy(): boolean;
|
|
77
|
+
get sessionFile(): string | undefined;
|
|
76
78
|
onEvent(listener: (payload: Record<string, unknown>) => void): this;
|
|
77
79
|
onExit(listener: (code: number | null, signal: string | null) => void): this;
|
|
78
80
|
onExtensionUI(listener: (request: Record<string, unknown>) => void): this;
|
package/dist/pi-rpc.js
CHANGED
|
@@ -70,12 +70,16 @@ export class PiRpcClient extends EventEmitter {
|
|
|
70
70
|
disposing;
|
|
71
71
|
agentActive = false;
|
|
72
72
|
compacting = false;
|
|
73
|
+
sessionPath;
|
|
73
74
|
get pid() {
|
|
74
75
|
return this.proc.pid;
|
|
75
76
|
}
|
|
76
77
|
get isBusy() {
|
|
77
78
|
return this.pending.size > 0;
|
|
78
79
|
}
|
|
80
|
+
get sessionFile() {
|
|
81
|
+
return this.sessionPath;
|
|
82
|
+
}
|
|
79
83
|
onEvent(listener) {
|
|
80
84
|
return super.on("event", listener);
|
|
81
85
|
}
|
|
@@ -231,6 +235,13 @@ export class PiRpcClient extends EventEmitter {
|
|
|
231
235
|
...(typeof msg.errorCode === "string" ? { errorCode: msg.errorCode } : {}),
|
|
232
236
|
...(msg.errorData !== undefined ? { errorData: msg.errorData } : {}),
|
|
233
237
|
};
|
|
238
|
+
if (pending && response.success && msg.command === "get_state") {
|
|
239
|
+
const data = response.data;
|
|
240
|
+
this.sessionPath =
|
|
241
|
+
typeof data === "object" && data !== null && "sessionFile" in data && typeof data.sessionFile === "string"
|
|
242
|
+
? data.sessionFile
|
|
243
|
+
: undefined;
|
|
244
|
+
}
|
|
234
245
|
pending?.resolve(response);
|
|
235
246
|
return;
|
|
236
247
|
}
|
package/dist/session-tree.d.ts
CHANGED
|
@@ -49,6 +49,8 @@ export interface TreeEntry {
|
|
|
49
49
|
export interface Entries {
|
|
50
50
|
entries: TreeEntry[];
|
|
51
51
|
leafId: string | null;
|
|
52
|
+
/** A disk prefix is not proof that the live runtime has loaded every entry. */
|
|
53
|
+
unverifiedPrefix?: true;
|
|
52
54
|
}
|
|
53
55
|
export declare class SessionTreeError extends Error {
|
|
54
56
|
}
|
package/dist/session-tree.js
CHANGED
|
@@ -4,6 +4,7 @@ import { contextMessageID, nativePromptInput } from "./prompt-parts.js";
|
|
|
4
4
|
import { backgroundResult } from "./background-results.js";
|
|
5
5
|
export class SessionTreeError extends Error {
|
|
6
6
|
}
|
|
7
|
+
const INLINE_TREE_BYTES = 32 * 1024 * 1024;
|
|
7
8
|
export function durableShellTree(tree) {
|
|
8
9
|
return {
|
|
9
10
|
...tree,
|
|
@@ -12,10 +13,26 @@ export function durableShellTree(tree) {
|
|
|
12
13
|
};
|
|
13
14
|
}
|
|
14
15
|
export async function getEntries(pi, signal) {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
const path = pi.sessionFile;
|
|
17
|
+
const disk = path !== undefined && (statSync(path, { throwIfNoEntry: false })?.size ?? 0) >= INLINE_TREE_BYTES
|
|
18
|
+
? readSessionTree(path)
|
|
19
|
+
: undefined;
|
|
20
|
+
for (let index = (disk?.entries.length ?? 0) - 1;; index--) {
|
|
21
|
+
const since = disk?.entries[index]?.id;
|
|
22
|
+
const response = await pi.command({ type: "get_entries", ...(since === undefined ? {} : { since }) }, RPC_COMMAND_TIMEOUT_MS, signal);
|
|
23
|
+
// Another client may have appended entries the live runtime has not loaded.
|
|
24
|
+
if (!response.success && response.errorCode === "not_found" && since !== undefined)
|
|
25
|
+
continue;
|
|
26
|
+
if (!response.success || !response.data)
|
|
27
|
+
throw new SessionTreeError(response.error ?? "OmO returned no session entries");
|
|
28
|
+
return disk === undefined
|
|
29
|
+
? response.data
|
|
30
|
+
: {
|
|
31
|
+
entries: [...disk.entries.slice(0, index + 1), ...response.data.entries],
|
|
32
|
+
leafId: response.data.leafId,
|
|
33
|
+
unverifiedPrefix: true,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
19
36
|
}
|
|
20
37
|
export function readSessionTree(piPath) {
|
|
21
38
|
const lines = readFileSync(piPath, "utf8").trimEnd().split("\n");
|
package/dist/sessions.js
CHANGED
|
@@ -251,7 +251,8 @@ export class SessionManager {
|
|
|
251
251
|
}
|
|
252
252
|
delete s.interruptRequested;
|
|
253
253
|
this.events.settleBackground(s);
|
|
254
|
-
|
|
254
|
+
// Its task cards end with the run; the tasks themselves may still run in OmO's shared host.
|
|
255
|
+
this.subagents.finishCalls(s.info.id, true);
|
|
255
256
|
this.monitors.stopParent(s.info.id);
|
|
256
257
|
void this.lifecycle
|
|
257
258
|
.dormant(s)
|
|
@@ -469,7 +470,7 @@ export class SessionManager {
|
|
|
469
470
|
if (treeOperation && s.piPath !== undefined) {
|
|
470
471
|
const disk = readSessionTree(s.piPath);
|
|
471
472
|
const known = new Set(tree.entries.map((entry) => entry.id));
|
|
472
|
-
if (disk.leafId !== tree.leafId || disk.entries.some((entry) => !known.has(entry.id))) {
|
|
473
|
+
if (tree.unverifiedPrefix || disk.leafId !== tree.leafId || disk.entries.some((entry) => !known.has(entry.id))) {
|
|
473
474
|
// Only idle user tree operations can replace a stale native runtime.
|
|
474
475
|
const saved = s.tree.leafId;
|
|
475
476
|
const response = await pi.command({ type: "switch_session", sessionPath: s.piPath }, RPC_COMMAND_TIMEOUT_MS);
|
|
@@ -500,9 +501,13 @@ export class SessionManager {
|
|
|
500
501
|
if (!s)
|
|
501
502
|
throw new SessionTreeError(`unknown session: ${sessionId}`);
|
|
502
503
|
return this.operation(s, async () => {
|
|
503
|
-
if (s.status.type !== "idle" || s.hasPendingInteraction()
|
|
504
|
+
if (s.status.type !== "idle" || s.hasPendingInteraction()) {
|
|
504
505
|
throw new SessionTreeError("Session is busy; stop the turn before changing the conversation");
|
|
505
506
|
}
|
|
507
|
+
// Stopping the turn does not stop a background subagent; it ends on its own or when cancelled.
|
|
508
|
+
if (s.hasRunningChildren()) {
|
|
509
|
+
throw new SessionTreeError("A subagent of this session is still running; wait for it to finish before changing the conversation");
|
|
510
|
+
}
|
|
506
511
|
if (!s.pi)
|
|
507
512
|
await this.revive(s);
|
|
508
513
|
const pi = s.pi;
|
|
@@ -712,7 +717,8 @@ export class SessionManager {
|
|
|
712
717
|
}
|
|
713
718
|
s.cancelShell?.();
|
|
714
719
|
s.approvals?.cancelAll();
|
|
715
|
-
|
|
720
|
+
// Background tasks outlive an abort; OmO itself cancels the ones the turn was waiting on.
|
|
721
|
+
this.subagents.finishCalls(sessionId, true);
|
|
716
722
|
return true;
|
|
717
723
|
}
|
|
718
724
|
permissions(sessionId) {
|
package/dist/subagents.d.ts
CHANGED
|
@@ -57,6 +57,19 @@ export declare class SubagentBridge {
|
|
|
57
57
|
start(parent: ManagedSession, original: OCToolPart, args: Record<string, unknown>): void;
|
|
58
58
|
result(parent: ManagedSession, callID: string, result: ToolResult, ended: boolean): boolean;
|
|
59
59
|
wake(parent: ManagedSession, event: Record<string, unknown>): void;
|
|
60
|
+
/**
|
|
61
|
+
* OmO's task bridge publishes `omo.task.updated` over the RPC extension channel: a replacement
|
|
62
|
+
* snapshot of every task under the parent, sent on attach (after OmO reconciles detached workers)
|
|
63
|
+
* and on each task store change (omo-task.js `W6`). A task missing from it is unknown, not ended:
|
|
64
|
+
* the snapshot is capped at 256 tasks.
|
|
65
|
+
*/
|
|
66
|
+
observe(parent: ManagedSession, ev: Record<string, unknown>): void;
|
|
67
|
+
/**
|
|
68
|
+
* Merge task state reported by OmO (record, snapshot) unless it is older than what is known: an
|
|
69
|
+
* older update, or a running state that predates a terminal one OmO already reported. Terminal
|
|
70
|
+
* states are only ever taken from OmO, so nothing the adapter inferred can block a running task.
|
|
71
|
+
*/
|
|
72
|
+
private applyNative;
|
|
60
73
|
discover(parent: ManagedSession, messages: OCMessageWithParts[]): void;
|
|
61
74
|
private discoverParts;
|
|
62
75
|
private create;
|
|
@@ -90,7 +103,18 @@ export declare class SubagentBridge {
|
|
|
90
103
|
private persist;
|
|
91
104
|
private emitPart;
|
|
92
105
|
private closeWatchers;
|
|
106
|
+
/**
|
|
107
|
+
* End the parent's task cards that have no running child. A card whose child runs is left to OmO:
|
|
108
|
+
* an abort cancels only a task the turn was still waiting on, and OmO reports that cancellation
|
|
109
|
+
* like any other end (omo-task.js `UZ`, "parent turn aborted"); background tasks keep running.
|
|
110
|
+
* After an abort the call stays tracked, because its tool result can still arrive.
|
|
111
|
+
*/
|
|
93
112
|
finishCalls(parentID: string, cancelled?: boolean): void;
|
|
113
|
+
/**
|
|
114
|
+
* Stop showing a deleted parent's children as live. Their status is not touched: OmO tasks outlive
|
|
115
|
+
* their parent's turn and process (shared-host workers detach, omo-task.js `DK`/`jK`), so only OmO
|
|
116
|
+
* can say a task ended.
|
|
117
|
+
*/
|
|
94
118
|
stopParent(parentID: string): void;
|
|
95
119
|
/** Forget a deleted child and leave a registry tombstone; false when `id` is not a child. */
|
|
96
120
|
remove(id: string): boolean;
|
package/dist/subagents.js
CHANGED
|
@@ -29,6 +29,22 @@ function active(status) {
|
|
|
29
29
|
function revivable(details) {
|
|
30
30
|
return (details.residency_state === "resident" && ["completed", "error", "interrupted"].includes(String(details.status)));
|
|
31
31
|
}
|
|
32
|
+
function timestamp(value) {
|
|
33
|
+
return typeof value === "string" ? Date.parse(value) : Number.NaN;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The lifecycle fields of an `omo.task.updated` row. `live_progress` changes on every event and is
|
|
37
|
+
* not task state; a text field OmO cut to its 32 000-character limit (`*_truncated`) must not
|
|
38
|
+
* replace the full value the task record carries.
|
|
39
|
+
*/
|
|
40
|
+
function snapshotState(row) {
|
|
41
|
+
const { live_progress: _progress, final_response_truncated: responseCut, error_message_truncated: errorCut, description_truncated: _descriptionCut, ...data } = row;
|
|
42
|
+
if (responseCut === true)
|
|
43
|
+
delete data.final_response;
|
|
44
|
+
if (errorCut === true)
|
|
45
|
+
delete data.error_message;
|
|
46
|
+
return data;
|
|
47
|
+
}
|
|
32
48
|
function terminal(status) {
|
|
33
49
|
return ["completed", "failed", "error", "cancelled", "canceled", "killed", "lost", "denied", "plan_error"].includes(String(status));
|
|
34
50
|
}
|
|
@@ -371,6 +387,50 @@ export class SubagentBridge {
|
|
|
371
387
|
this.refresh(child, true, false);
|
|
372
388
|
}
|
|
373
389
|
}
|
|
390
|
+
/**
|
|
391
|
+
* OmO's task bridge publishes `omo.task.updated` over the RPC extension channel: a replacement
|
|
392
|
+
* snapshot of every task under the parent, sent on attach (after OmO reconciles detached workers)
|
|
393
|
+
* and on each task store change (omo-task.js `W6`). A task missing from it is unknown, not ended:
|
|
394
|
+
* the snapshot is capped at 256 tasks.
|
|
395
|
+
*/
|
|
396
|
+
observe(parent, ev) {
|
|
397
|
+
if (ev.name !== "omo.task.updated" || !record(ev.data) || !Array.isArray(ev.data.tasks))
|
|
398
|
+
return;
|
|
399
|
+
for (const row of ev.data.tasks) {
|
|
400
|
+
if (!record(row) || !taskId(row.task_id))
|
|
401
|
+
continue;
|
|
402
|
+
const id = ocIdFor(row.task_id);
|
|
403
|
+
if (!this.children.has(id))
|
|
404
|
+
this.restore(id);
|
|
405
|
+
const child = this.children.get(id);
|
|
406
|
+
if (!child)
|
|
407
|
+
continue;
|
|
408
|
+
const data = snapshotState(row);
|
|
409
|
+
// OmO re-sends every row ~150 ms after each progress event of any task; only changes matter.
|
|
410
|
+
if (Object.entries(data).every(([key, value]) => child.task.details[key] === value))
|
|
411
|
+
continue;
|
|
412
|
+
this.wire(parent);
|
|
413
|
+
if (!this.applyNative(child, data))
|
|
414
|
+
continue;
|
|
415
|
+
child.live = true;
|
|
416
|
+
this.refresh(child, true, false);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Merge task state reported by OmO (record, snapshot) unless it is older than what is known: an
|
|
421
|
+
* older update, or a running state that predates a terminal one OmO already reported. Terminal
|
|
422
|
+
* states are only ever taken from OmO, so nothing the adapter inferred can block a running task.
|
|
423
|
+
*/
|
|
424
|
+
applyNative(child, data) {
|
|
425
|
+
const updated = timestamp(data.updated_at);
|
|
426
|
+
const known = timestamp(child.task.details.updated_at);
|
|
427
|
+
if (updated < known)
|
|
428
|
+
return false;
|
|
429
|
+
if (active(data.status) && terminal(child.task.details.status) && updated <= (child.task.terminalAt ?? 0))
|
|
430
|
+
return false;
|
|
431
|
+
child.task.details = { ...child.task.details, ...data };
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
374
434
|
discover(parent, messages) {
|
|
375
435
|
this.discoverParts(parent, messages.flatMap((message) => message.parts), false);
|
|
376
436
|
}
|
|
@@ -582,8 +642,12 @@ export class SubagentBridge {
|
|
|
582
642
|
}
|
|
583
643
|
refresh(child, publish, useRecord = true) {
|
|
584
644
|
const archive = this.archive(child.task.id);
|
|
585
|
-
const found = this.reconcileTaskRecord(child,
|
|
586
|
-
|
|
645
|
+
const found = this.reconcileTaskRecord(child, useRecord);
|
|
646
|
+
// OmO stops in-process workers with their parent and leaves them `running` but `persisted_only`
|
|
647
|
+
// until the revived parent reconciles them (omo-task.js `DK`/`jK`); they are not running.
|
|
648
|
+
const running = active(child.task.details.status) &&
|
|
649
|
+
child.task.details.residency_state !== "persisted_only" &&
|
|
650
|
+
(found !== undefined || child.live);
|
|
587
651
|
const state = {
|
|
588
652
|
previousMessages: child.session.messages ?? [],
|
|
589
653
|
previousStatus: child.session.status.type,
|
|
@@ -599,16 +663,13 @@ export class SubagentBridge {
|
|
|
599
663
|
this.persist(child);
|
|
600
664
|
this.publishRefresh(child, state, publish);
|
|
601
665
|
}
|
|
602
|
-
reconcileTaskRecord(child,
|
|
666
|
+
reconcileTaskRecord(child, useRecord) {
|
|
603
667
|
const found = findTask(child.task.id, child.task.parentPiId);
|
|
604
668
|
if (!found)
|
|
605
669
|
return undefined;
|
|
606
670
|
child.recordPath = found.path;
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
const staleRunning = active(found.data.status) && terminal(child.task.details.status) && updated <= retainedTerminal;
|
|
610
|
-
if (useRecord && !staleRunning)
|
|
611
|
-
child.task.details = { ...child.task.details, ...found.data };
|
|
671
|
+
if (useRecord)
|
|
672
|
+
this.applyNative(child, found.data);
|
|
612
673
|
const host = found.data.host_session;
|
|
613
674
|
if (record(host) && typeof host.session_path === "string") {
|
|
614
675
|
child.livePath = host.session_path;
|
|
@@ -806,15 +867,24 @@ export class SubagentBridge {
|
|
|
806
867
|
child.watchers.delete(dir);
|
|
807
868
|
}
|
|
808
869
|
}
|
|
870
|
+
/**
|
|
871
|
+
* End the parent's task cards that have no running child. A card whose child runs is left to OmO:
|
|
872
|
+
* an abort cancels only a task the turn was still waiting on, and OmO reports that cancellation
|
|
873
|
+
* like any other end (omo-task.js `UZ`, "parent turn aborted"); background tasks keep running.
|
|
874
|
+
* After an abort the call stays tracked, because its tool result can still arrive.
|
|
875
|
+
*/
|
|
809
876
|
finishCalls(parentID, cancelled = false) {
|
|
810
877
|
for (const [key, parts] of this.calls) {
|
|
811
878
|
if (!key.startsWith(`${parentID}|`))
|
|
812
879
|
continue;
|
|
813
880
|
const parent = this.sessions.get(parentID);
|
|
881
|
+
let waiting = false;
|
|
814
882
|
for (const part of parts) {
|
|
815
883
|
const childID = part.state.status !== "pending" ? part.state.metadata?.sessionID : undefined;
|
|
816
|
-
if (
|
|
884
|
+
if (typeof childID === "string" && this.children.get(childID)?.session.status.type === "busy") {
|
|
885
|
+
waiting = true;
|
|
817
886
|
continue;
|
|
887
|
+
}
|
|
818
888
|
updateTaskParts([part], {
|
|
819
889
|
isError: true,
|
|
820
890
|
details: { status: cancelled ? "cancelled" : "failed" },
|
|
@@ -823,19 +893,21 @@ export class SubagentBridge {
|
|
|
823
893
|
if (parent)
|
|
824
894
|
this.emitPart(parent, part);
|
|
825
895
|
}
|
|
826
|
-
|
|
896
|
+
if (!cancelled || !waiting)
|
|
897
|
+
this.calls.delete(key);
|
|
827
898
|
}
|
|
828
899
|
}
|
|
900
|
+
/**
|
|
901
|
+
* Stop showing a deleted parent's children as live. Their status is not touched: OmO tasks outlive
|
|
902
|
+
* their parent's turn and process (shared-host workers detach, omo-task.js `DK`/`jK`), so only OmO
|
|
903
|
+
* can say a task ended.
|
|
904
|
+
*/
|
|
829
905
|
stopParent(parentID) {
|
|
830
906
|
this.finishCalls(parentID, true);
|
|
831
907
|
for (const child of this.children.values()) {
|
|
832
908
|
if (child.session.info.parentID !== parentID)
|
|
833
909
|
continue;
|
|
834
910
|
this.stopParent(child.session.info.id);
|
|
835
|
-
if (child.session.status.type === "busy") {
|
|
836
|
-
child.task.details = { ...child.task.details, status: "cancelled" };
|
|
837
|
-
this.refresh(child, true, false);
|
|
838
|
-
}
|
|
839
911
|
this.closeWatchers(child);
|
|
840
912
|
}
|
|
841
913
|
}
|