antechamber 1.0.2 → 1.0.4
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 +19 -0
- package/README.md +30 -9
- package/dist/subagents.d.ts +7 -4
- package/dist/subagents.js +139 -47
- package/extensions/omo-compat/README.md +5 -2
- package/extensions/omo-compat/package.json +1 -1
- package/extensions/omo-dag/README.md +10 -8
- package/extensions/omo-dag/docs/DESIGN.md +9 -5
- package/extensions/omo-dag/package.json +1 -1
- package/extensions/omo-dag/service/main.js +18 -3
- package/extensions/omo-work/README.md +6 -3
- package/extensions/omo-work/package.json +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,25 @@ and this project uses [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.0.4] - 2026-10-20
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Keep subagents with colliding task IDs in different sessions separate, and recover a foreign child
|
|
15
|
+
archive from its own retained transcript.
|
|
16
|
+
|
|
17
|
+
## [1.0.3] - 2026-10-20
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- The OpenChamber extensions install by Git URL, and OpenChamber offers each release under
|
|
22
|
+
**Update** in Settings → Extensions: every release commits them to the `extension/omo-graph`,
|
|
23
|
+
`extension/omo-work` and `extension/omo-compat` branches, so add
|
|
24
|
+
`https://github.com/eliottness/antechamber#extension/omo-graph` (or `#extension/omo-work`,
|
|
25
|
+
`#extension/omo-compat`). The bare repository URL keeps failing with OpenChamber's "panel.id must
|
|
26
|
+
be kebab-case" toast, because it points at antechamber itself. ZIP installs still work but are
|
|
27
|
+
never updated.
|
|
28
|
+
|
|
10
29
|
## [1.0.2] - 2026-10-20
|
|
11
30
|
|
|
12
31
|
### Fixed
|
package/README.md
CHANGED
|
@@ -228,13 +228,14 @@ source fits where OpenChamber runs:
|
|
|
228
228
|
| OpenChamber runs | Add | Value |
|
|
229
229
|
|---|---|---|
|
|
230
230
|
| on the machine where antechamber is installed | **Folder** | the path `antechamber setup` prints: `$(npm root -g)/antechamber/extensions/omo-dag` for npm installs, `<checkout>/extensions/omo-dag` after `npm run build` from source |
|
|
231
|
-
| anywhere (remote host, browser-only access) | **URL** | `https://github.com/eliottness/antechamber/
|
|
232
|
-
| anywhere, offline | **ZIP file** |
|
|
231
|
+
| anywhere (remote host, browser-only access) | **URL** | `https://github.com/eliottness/antechamber#extension/omo-graph`, a Git install that OpenChamber offers to update after each release |
|
|
232
|
+
| anywhere, offline | **ZIP file** | `antechamber-omo-graph.zip` from a GitHub release, or built with `npm run build && npm run pack:extension -- antechamber-omo-graph.zip`. OpenChamber never updates ZIP installs |
|
|
233
233
|
|
|
234
234
|
Then approve its two permissions (follow sessions, run the local service) and
|
|
235
|
-
open it from the rail icon.
|
|
236
|
-
|
|
237
|
-
extension, and
|
|
235
|
+
open it from the rail icon. Keep the `#extension/omo-graph` part of the URL:
|
|
236
|
+
the bare repository URL makes OpenChamber clone antechamber itself, whose
|
|
237
|
+
`package.json` is not an extension, and OpenChamber words that rejection as
|
|
238
|
+
"panel.id must be kebab-case". Details:
|
|
238
239
|
[extensions/omo-dag/README.md](extensions/omo-dag/README.md).
|
|
239
240
|
|
|
240
241
|
## OpenChamber extension: OmO Work
|
|
@@ -248,8 +249,8 @@ with a Fire now button. On 2.x, monitors appear here instead of as subagent
|
|
|
248
249
|
sessions.
|
|
249
250
|
|
|
250
251
|
Add it like OmO Graph: the folder `antechamber setup` prints
|
|
251
|
-
(`.../extensions/omo-work`, built by `npm run build`), or
|
|
252
|
-
`https://github.com/eliottness/antechamber/
|
|
252
|
+
(`.../extensions/omo-work`, built by `npm run build`), or the URL
|
|
253
|
+
`https://github.com/eliottness/antechamber#extension/omo-work`.
|
|
253
254
|
Approve its local service. It needs an antechamber version that writes a
|
|
254
255
|
work-status token into its run record (this release or newer) and an
|
|
255
256
|
OpenChamber restart after upgrading. Details:
|
|
@@ -262,11 +263,31 @@ or newer. It tells OpenChamber how to display OmO tool calls that antechamber
|
|
|
262
263
|
maps onto OpenChamber's cards; today it heads eval cells with `JS Eval` or
|
|
263
264
|
`Python Eval` and the cell's summary. Add it the same way as OmO Graph: the
|
|
264
265
|
folder `antechamber setup` prints (`.../extensions/omo-compat`, no build
|
|
265
|
-
needed), or the
|
|
266
|
-
`https://github.com/eliottness/antechamber/
|
|
266
|
+
needed), or the URL
|
|
267
|
+
`https://github.com/eliottness/antechamber#extension/omo-compat`.
|
|
267
268
|
It asks for no permissions. Details:
|
|
268
269
|
[extensions/omo-compat/README.md](extensions/omo-compat/README.md).
|
|
269
270
|
|
|
271
|
+
## Upgrading the OpenChamber extensions
|
|
272
|
+
|
|
273
|
+
Every antechamber release ships the three extensions at its own version. How
|
|
274
|
+
an installed extension moves to a new release depends on how it was added:
|
|
275
|
+
|
|
276
|
+
- **Git URL** (`https://github.com/eliottness/antechamber#extension/...`):
|
|
277
|
+
Settings → Extensions shows **Update available** once a release is out
|
|
278
|
+
(OpenChamber checks at most hourly; **Check for updates** checks now).
|
|
279
|
+
**Update** replaces it in place and keeps its approvals and settings.
|
|
280
|
+
- **Folder**: the folder belongs to the antechamber installation, so it
|
|
281
|
+
changes with `antechamber upgrade` (or `git pull && npm ci && npm run build`
|
|
282
|
+
in a checkout). Toggle the extension off and on to restart its service.
|
|
283
|
+
- **ZIP** (file or release URL): OpenChamber never checks these. Add the new
|
|
284
|
+
archive and choose **Reinstall**, or switch to the Git URL once: remove the
|
|
285
|
+
extension, then add it by URL.
|
|
286
|
+
|
|
287
|
+
Each extension is built against the antechamber of the same release, so
|
|
288
|
+
upgrade antechamber too and restart OpenChamber to run it; `antechamber
|
|
289
|
+
doctor` shows the running version.
|
|
290
|
+
|
|
270
291
|
## Configuration
|
|
271
292
|
|
|
272
293
|
Connector settings use the `ANTECHAMBER_` prefix; the deprecated `OCPI_`
|
package/dist/subagents.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { type EvalAgent, type ToolResult } from "./tool-map.js";
|
|
1
|
+
import { SessionStore } from "./session-store.js";
|
|
3
2
|
import type { ManagedSession } from "./sessions.js";
|
|
3
|
+
import { type EvalAgent, type ToolResult } from "./tool-map.js";
|
|
4
4
|
import type { OCEvent, OCMessageWithParts, OCToolPart } from "./types.js";
|
|
5
5
|
export declare function stripTaskSuffix(text: string): string;
|
|
6
6
|
/** One stable renderer part per item, with the batch defaults inherited. */
|
|
@@ -9,8 +9,10 @@ export declare function taskParts(template: OCToolPart, args: Record<string, unk
|
|
|
9
9
|
export declare function updateTaskParts(parts: OCToolPart[], result: ToolResult, ended: boolean, now: number): void;
|
|
10
10
|
/** Wake messages carry arrays of completion envelopes, not a flat details object. */
|
|
11
11
|
export declare function taskCompletions(value: unknown): Record<string, unknown>[];
|
|
12
|
-
/**
|
|
13
|
-
export declare function
|
|
12
|
+
/** Keep bookmarks for known tasks; a bare native id is only safe while it has one parent. */
|
|
13
|
+
export declare function taskSessionId(id: string, parentPiId: string, store?: SessionStore): string;
|
|
14
|
+
/** Reverse lookup never resolves an unbound, ambiguous bare id by store enumeration order. */
|
|
15
|
+
export declare function findTaskBySession(sessionId: string, store?: SessionStore): {
|
|
14
16
|
path: string;
|
|
15
17
|
data: Record<string, unknown>;
|
|
16
18
|
} | undefined;
|
|
@@ -53,6 +55,7 @@ export declare class SubagentBridge {
|
|
|
53
55
|
private readonly taskParents;
|
|
54
56
|
constructor(store: SessionStore, sessions: Map<string, ManagedSession>, emit: (directory: string, event: OCEvent) => void, parse: ParseHistory);
|
|
55
57
|
private archive;
|
|
58
|
+
private sessionId;
|
|
56
59
|
wire(parent: ManagedSession): void;
|
|
57
60
|
start(parent: ManagedSession, original: OCToolPart, args: Record<string, unknown>): void;
|
|
58
61
|
result(parent: ManagedSession, callID: string, result: ToolResult, ended: boolean): boolean;
|
package/dist/subagents.js
CHANGED
|
@@ -3,7 +3,7 @@ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, rea
|
|
|
3
3
|
import { basename, dirname, join } from "node:path";
|
|
4
4
|
import { piAgentDir } from "./agent-dir.js";
|
|
5
5
|
import { syncSessionCost } from "./pi-event-translation.js";
|
|
6
|
-
import { ocIdFor, SessionPersistenceError } from "./session-store.js";
|
|
6
|
+
import { ocIdFor, SessionPersistenceError, SessionStore } from "./session-store.js";
|
|
7
7
|
import { mapTool, resultText, toolMappers } from "./tool-map.js";
|
|
8
8
|
import { ADAPTER_VERSION } from "./version.js";
|
|
9
9
|
// macOS watches directories through FSEvents, which goes live asynchronously after
|
|
@@ -106,7 +106,12 @@ export function updateTaskParts(parts, result, ended, now) {
|
|
|
106
106
|
const metadata = {
|
|
107
107
|
...previous,
|
|
108
108
|
task: data,
|
|
109
|
-
...(taskId(data.task_id)
|
|
109
|
+
...(taskId(data.task_id)
|
|
110
|
+
? {
|
|
111
|
+
sessionID: previous?.taskID === data.task_id ? previous.sessionID : ocIdFor(data.task_id),
|
|
112
|
+
taskID: data.task_id,
|
|
113
|
+
}
|
|
114
|
+
: {}),
|
|
110
115
|
};
|
|
111
116
|
// Preserve a final result for the fallback even after the parent JSONL is gone.
|
|
112
117
|
if (ended && output !== "" && !active(data.status))
|
|
@@ -206,16 +211,42 @@ function childRecord(id, path) {
|
|
|
206
211
|
const data = jsonObject(path);
|
|
207
212
|
return data?.task_id === id && typeof data.parent_session_id === "string" ? data : undefined;
|
|
208
213
|
}
|
|
209
|
-
/**
|
|
210
|
-
export function
|
|
214
|
+
/** Keep bookmarks for known tasks; a bare native id is only safe while it has one parent. */
|
|
215
|
+
export function taskSessionId(id, parentPiId, store = new SessionStore()) {
|
|
216
|
+
for (const sessionID of store.childIds()) {
|
|
217
|
+
const task = store.get(sessionID)?.task;
|
|
218
|
+
if (task?.id === id && task.parentPiId === parentPiId)
|
|
219
|
+
return sessionID;
|
|
220
|
+
}
|
|
221
|
+
const legacyID = ocIdFor(id);
|
|
222
|
+
const legacy = store.get(legacyID);
|
|
223
|
+
if (legacy !== undefined) {
|
|
224
|
+
if (legacy.deleted && legacy.parentID === (store.idFor(parentPiId) ?? ocIdFor(parentPiId)))
|
|
225
|
+
return legacyID;
|
|
226
|
+
return ocIdFor(`${parentPiId}:${id}`);
|
|
227
|
+
}
|
|
228
|
+
const qualifiedID = ocIdFor(`${parentPiId}:${id}`);
|
|
229
|
+
if (store.get(qualifiedID))
|
|
230
|
+
return qualifiedID;
|
|
231
|
+
for (const root of taskStores()) {
|
|
232
|
+
const data = childRecord(id, join(root, "tasks", `${id}.json`));
|
|
233
|
+
if (data && data.parent_session_id !== parentPiId)
|
|
234
|
+
return qualifiedID;
|
|
235
|
+
}
|
|
236
|
+
return legacyID;
|
|
237
|
+
}
|
|
238
|
+
/** Reverse lookup never resolves an unbound, ambiguous bare id by store enumeration order. */
|
|
239
|
+
export function findTaskBySession(sessionId, store = new SessionStore()) {
|
|
240
|
+
let found;
|
|
211
241
|
for (const { id, path } of taskFiles()) {
|
|
212
|
-
if (ocIdFor(id) !== sessionId)
|
|
213
|
-
continue;
|
|
214
242
|
const data = childRecord(id, path);
|
|
215
|
-
if (data)
|
|
216
|
-
|
|
243
|
+
if (!data || taskSessionId(id, String(data.parent_session_id), store) !== sessionId)
|
|
244
|
+
continue;
|
|
245
|
+
if (found && found.data.parent_session_id !== data.parent_session_id)
|
|
246
|
+
return undefined;
|
|
247
|
+
found = { path, data };
|
|
217
248
|
}
|
|
218
|
-
return
|
|
249
|
+
return found;
|
|
219
250
|
}
|
|
220
251
|
/**
|
|
221
252
|
* Whether a recorded argument names a task record's value. Codemode caps recorded strings at 512 code
|
|
@@ -266,7 +297,7 @@ export function evalAgentSession(parentPiId, args, claimed, after) {
|
|
|
266
297
|
const summary = text(best.data.task_summary);
|
|
267
298
|
const agentType = text(best.data.agent_type);
|
|
268
299
|
return {
|
|
269
|
-
sessionID:
|
|
300
|
+
sessionID: taskSessionId(best.id, parentPiId),
|
|
270
301
|
...(name !== undefined && name !== best.id ? { name } : {}),
|
|
271
302
|
...(summary !== undefined ? { summary } : {}),
|
|
272
303
|
...(agentType !== undefined ? { agentType } : {}),
|
|
@@ -283,6 +314,35 @@ export function findTask(id, parentPiId) {
|
|
|
283
314
|
}
|
|
284
315
|
return undefined;
|
|
285
316
|
}
|
|
317
|
+
function transcriptSessionId(raw) {
|
|
318
|
+
try {
|
|
319
|
+
const header = JSON.parse(raw.split("\n", 1)[0] ?? "");
|
|
320
|
+
return record(header) && header.type === "session" && typeof header.id === "string" ? header.id : undefined;
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
if (error instanceof SyntaxError)
|
|
324
|
+
return undefined;
|
|
325
|
+
throw error;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function transcriptSessionIdFromFile(path) {
|
|
329
|
+
let fd;
|
|
330
|
+
try {
|
|
331
|
+
fd = openSync(path, "r");
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
if (error instanceof Error && "code" in error && ["ENOENT", "ENOTDIR", "EACCES"].includes(String(error.code)))
|
|
335
|
+
return undefined;
|
|
336
|
+
throw error;
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
const buffer = Buffer.alloc(4096);
|
|
340
|
+
return transcriptSessionId(buffer.subarray(0, readSync(fd, buffer, 0, buffer.length, 0)).toString("utf8"));
|
|
341
|
+
}
|
|
342
|
+
finally {
|
|
343
|
+
closeSync(fd);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
286
346
|
/** Byte offsets and a byte carry retain split UTF-8 characters and torn JSON lines. */
|
|
287
347
|
export class JsonlTail {
|
|
288
348
|
offset = 0;
|
|
@@ -339,8 +399,14 @@ export class SubagentBridge {
|
|
|
339
399
|
this.emit = emit;
|
|
340
400
|
this.parse = parse;
|
|
341
401
|
}
|
|
342
|
-
archive(
|
|
343
|
-
|
|
402
|
+
archive(child) {
|
|
403
|
+
const previous = this.store.get(child.session.info.id)?.piPath;
|
|
404
|
+
const directory = join(this.store.stateDir, "children");
|
|
405
|
+
return ((previous !== undefined && dirname(previous) === directory ? previous : undefined) ??
|
|
406
|
+
join(directory, `${child.session.info.id === ocIdFor(child.task.id) ? child.task.id : child.session.info.id}.jsonl`));
|
|
407
|
+
}
|
|
408
|
+
sessionId(parent, id) {
|
|
409
|
+
return taskSessionId(id, parent.piSessionId ?? parent.restored?.piSessionId ?? "", this.store);
|
|
344
410
|
}
|
|
345
411
|
wire(parent) {
|
|
346
412
|
parent.hasRunningChildren = () => [...this.children.values()].some((child) => child.session.info.parentID === parent.info.id && child.session.status.type === "busy");
|
|
@@ -373,7 +439,7 @@ export class SubagentBridge {
|
|
|
373
439
|
if (message.customType !== "omo-senpi:wake")
|
|
374
440
|
return;
|
|
375
441
|
for (const data of taskCompletions(message.details)) {
|
|
376
|
-
const id =
|
|
442
|
+
const id = this.sessionId(parent, String(data.task_id));
|
|
377
443
|
// A registry-only child (after a restart) or a task with no card (spawned from
|
|
378
444
|
// `eval`) is not loaded yet; restore() covers both.
|
|
379
445
|
if (!this.children.has(id))
|
|
@@ -399,11 +465,14 @@ export class SubagentBridge {
|
|
|
399
465
|
for (const row of ev.data.tasks) {
|
|
400
466
|
if (!record(row) || !taskId(row.task_id))
|
|
401
467
|
continue;
|
|
402
|
-
|
|
468
|
+
if (typeof row.parent_session_id === "string" &&
|
|
469
|
+
row.parent_session_id !== (parent.piSessionId ?? parent.restored?.piSessionId))
|
|
470
|
+
continue;
|
|
471
|
+
const id = this.sessionId(parent, row.task_id);
|
|
403
472
|
if (!this.children.has(id))
|
|
404
473
|
this.restore(id);
|
|
405
474
|
const child = this.children.get(id);
|
|
406
|
-
if (!child)
|
|
475
|
+
if (!child || child.session.info.parentID !== parent.info.id)
|
|
407
476
|
continue;
|
|
408
477
|
const data = snapshotState(row);
|
|
409
478
|
// OmO re-sends every row ~150 ms after each progress event of any task; only changes matter.
|
|
@@ -440,12 +509,13 @@ export class SubagentBridge {
|
|
|
440
509
|
if (part.type !== "tool" || part.tool !== "task" || part.state.status === "pending")
|
|
441
510
|
continue;
|
|
442
511
|
const metadata = part.state.metadata;
|
|
443
|
-
if (!taskId(metadata
|
|
512
|
+
if (!metadata || !taskId(metadata.taskID))
|
|
444
513
|
continue;
|
|
445
514
|
const id = metadata.taskID;
|
|
446
515
|
const details = record(metadata.task) ? metadata.task : {};
|
|
447
|
-
|
|
448
|
-
|
|
516
|
+
const sessionID = this.sessionId(parent, id);
|
|
517
|
+
let child = this.children.get(sessionID);
|
|
518
|
+
if (!child && this.deleted(sessionID))
|
|
449
519
|
continue;
|
|
450
520
|
if (!child) {
|
|
451
521
|
child = this.create(parent, {
|
|
@@ -461,16 +531,17 @@ export class SubagentBridge {
|
|
|
461
531
|
child.task.details = { ...child.task.details, ...details };
|
|
462
532
|
}
|
|
463
533
|
child.live ||= live;
|
|
534
|
+
metadata.sessionID = child.session.info.id;
|
|
464
535
|
if (terminal(details.status) && part.state.status !== "running")
|
|
465
536
|
child.task.terminalAt = part.state.time.end;
|
|
466
537
|
child.parts.set(part.id, part);
|
|
467
538
|
this.refresh(child, true);
|
|
468
539
|
}
|
|
469
540
|
}
|
|
470
|
-
create(parent, task) {
|
|
471
|
-
const archived = this.store.get(
|
|
541
|
+
create(parent, task, id = taskSessionId(task.id, task.parentPiId, this.store)) {
|
|
542
|
+
const archived = this.store.get(id)?.archived;
|
|
472
543
|
const session = this.managedSession({
|
|
473
|
-
id
|
|
544
|
+
id,
|
|
474
545
|
parentID: parent.info.id,
|
|
475
546
|
projectID: parent.info.projectID,
|
|
476
547
|
directory: parent.info.directory,
|
|
@@ -504,7 +575,7 @@ export class SubagentBridge {
|
|
|
504
575
|
return this.removed.has(id) ? undefined : this.adopt(id);
|
|
505
576
|
if (entry.deleted || !entry.task || !entry.parentID)
|
|
506
577
|
return undefined;
|
|
507
|
-
const child = this.create(this.detachedParent(entry.parentID, entry.directory, entry), entry.task);
|
|
578
|
+
const child = this.create(this.detachedParent(entry.parentID, entry.directory, entry), entry.task, id);
|
|
508
579
|
child.session.info.title = entry.title ?? child.session.info.title;
|
|
509
580
|
this.refresh(child, false);
|
|
510
581
|
return child.session;
|
|
@@ -513,7 +584,7 @@ export class SubagentBridge {
|
|
|
513
584
|
* Tasks spawned outside a `task` tool call (e.g. `agent()` inside an `eval`
|
|
514
585
|
* cell) never produce a card, so their session is built from the task record.
|
|
515
586
|
*/
|
|
516
|
-
adopt(id, found = findTaskBySession(id)) {
|
|
587
|
+
adopt(id, found = findTaskBySession(id, this.store)) {
|
|
517
588
|
if (!found)
|
|
518
589
|
return undefined;
|
|
519
590
|
const parentPi = String(found.data.parent_session_id);
|
|
@@ -532,7 +603,7 @@ export class SubagentBridge {
|
|
|
532
603
|
...(typeof found.data.agent_type === "string" ? { subagent_type: found.data.agent_type } : {}),
|
|
533
604
|
},
|
|
534
605
|
details: found.data,
|
|
535
|
-
});
|
|
606
|
+
}, id);
|
|
536
607
|
this.refresh(child, false);
|
|
537
608
|
return child.session;
|
|
538
609
|
}
|
|
@@ -554,9 +625,9 @@ export class SubagentBridge {
|
|
|
554
625
|
childIdsByPi() {
|
|
555
626
|
const byPi = new Map();
|
|
556
627
|
for (const id of this.store.childIds()) {
|
|
557
|
-
const
|
|
558
|
-
if (piSessionId)
|
|
559
|
-
byPi.set(piSessionId, id);
|
|
628
|
+
const entry = this.store.get(id);
|
|
629
|
+
if (entry?.piSessionId && !entry.deleted)
|
|
630
|
+
byPi.set(entry.piSessionId, id);
|
|
560
631
|
}
|
|
561
632
|
return byPi;
|
|
562
633
|
}
|
|
@@ -589,16 +660,16 @@ export class SubagentBridge {
|
|
|
589
660
|
this.restore(id);
|
|
590
661
|
const childByPi = this.childIdsByPi();
|
|
591
662
|
for (const { id, path } of taskFiles()) {
|
|
592
|
-
|
|
593
|
-
if (this.children.has(sessionId) || this.removed.has(sessionId) || this.store.get(sessionId))
|
|
594
|
-
continue;
|
|
595
|
-
let parentPi = this.taskParents.get(id);
|
|
663
|
+
let parentPi = this.taskParents.get(path);
|
|
596
664
|
if (parentPi === undefined) {
|
|
597
665
|
const parent = childRecord(id, path)?.parent_session_id;
|
|
598
666
|
if (typeof parent !== "string")
|
|
599
667
|
continue;
|
|
600
|
-
this.taskParents.set(
|
|
668
|
+
this.taskParents.set(path, (parentPi = parent));
|
|
601
669
|
}
|
|
670
|
+
const sessionId = taskSessionId(id, parentPi, this.store);
|
|
671
|
+
if (this.children.has(sessionId) || this.removed.has(sessionId) || this.store.get(sessionId))
|
|
672
|
+
continue;
|
|
602
673
|
if (!this.parentFor(parentPi, childByPi))
|
|
603
674
|
continue;
|
|
604
675
|
const data = childRecord(id, path);
|
|
@@ -641,7 +712,7 @@ export class SubagentBridge {
|
|
|
641
712
|
}
|
|
642
713
|
}
|
|
643
714
|
refresh(child, publish, useRecord = true) {
|
|
644
|
-
const archive = this.archive(child
|
|
715
|
+
const archive = this.archive(child);
|
|
645
716
|
const found = this.reconcileTaskRecord(child, useRecord);
|
|
646
717
|
// OmO stops in-process workers with their parent and leaves them `running` but `persisted_only`
|
|
647
718
|
// until the revived parent reconciles them (omo-task.js `DK`/`jK`); they are not running.
|
|
@@ -695,32 +766,52 @@ export class SubagentBridge {
|
|
|
695
766
|
// A resumed live task can be newer than its archive; completed history prefers the archive.
|
|
696
767
|
const liveRun = running || previousStatus === "busy";
|
|
697
768
|
let raw = liveRun ? undefined : optionalFile(archive);
|
|
769
|
+
const expected = (child.livePath === undefined ? undefined : transcriptSessionIdFromFile(child.livePath)) ??
|
|
770
|
+
(typeof child.task.details.child_session_id === "string" && child.task.details.child_session_id !== ""
|
|
771
|
+
? child.task.details.child_session_id
|
|
772
|
+
: undefined);
|
|
773
|
+
const legacyArchive = join(this.store.stateDir, "children", `${child.task.id}.jsonl`);
|
|
774
|
+
if (!liveRun && raw === undefined && expected !== undefined) {
|
|
775
|
+
for (const path of [join(this.store.stateDir, "children", `native-${ocIdFor(expected)}.jsonl`), legacyArchive]) {
|
|
776
|
+
const retained = optionalFile(path);
|
|
777
|
+
if (retained !== undefined && transcriptSessionId(retained) === expected) {
|
|
778
|
+
raw = retained;
|
|
779
|
+
break;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
const foreign = raw !== undefined && expected !== undefined && transcriptSessionId(raw) !== expected;
|
|
784
|
+
if (foreign && raw !== undefined) {
|
|
785
|
+
const displaced = transcriptSessionId(raw);
|
|
786
|
+
if (displaced !== undefined) {
|
|
787
|
+
const retained = join(this.store.stateDir, "children", `native-${ocIdFor(displaced)}.jsonl`);
|
|
788
|
+
// This may be the other child's only history; retain it before repairing the shared path.
|
|
789
|
+
if (!existsSync(retained))
|
|
790
|
+
this.saveArchive(retained, raw);
|
|
791
|
+
}
|
|
792
|
+
raw = undefined;
|
|
793
|
+
}
|
|
698
794
|
if (running && child.recordPath)
|
|
699
795
|
this.watch(child, child.recordPath);
|
|
700
796
|
if (raw === undefined && child.livePath !== undefined) {
|
|
701
797
|
if (running)
|
|
702
798
|
this.watch(child, child.livePath);
|
|
703
799
|
child.tail.read(child.livePath);
|
|
704
|
-
if (child.tail.text !== "")
|
|
800
|
+
if (child.tail.text !== "" && (expected === undefined || transcriptSessionId(child.tail.text) === expected))
|
|
705
801
|
raw = child.tail.text;
|
|
706
802
|
}
|
|
707
803
|
if (raw !== undefined) {
|
|
708
|
-
const
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
if (record(header) && typeof header.id === "string")
|
|
712
|
-
child.session.piSessionId = header.id;
|
|
713
|
-
}
|
|
714
|
-
catch (error) {
|
|
715
|
-
if (!(error instanceof SyntaxError))
|
|
716
|
-
throw error;
|
|
717
|
-
}
|
|
804
|
+
const piSessionId = transcriptSessionId(raw);
|
|
805
|
+
if (piSessionId !== undefined)
|
|
806
|
+
child.session.piSessionId = piSessionId;
|
|
718
807
|
child.session.messages = this.parse(child.session, raw);
|
|
719
808
|
this.discover(child.session, child.session.messages);
|
|
720
|
-
if (!running && terminal(child.task.details.status) && (liveRun || !existsSync(archive)))
|
|
809
|
+
if (!running && terminal(child.task.details.status) && (liveRun || foreign || !existsSync(archive)))
|
|
721
810
|
this.saveArchive(archive, raw);
|
|
722
811
|
}
|
|
723
812
|
else {
|
|
813
|
+
if (expected !== undefined)
|
|
814
|
+
child.session.piSessionId = expected;
|
|
724
815
|
child.session.messages = this.fallback(child);
|
|
725
816
|
}
|
|
726
817
|
child.loaded = true;
|
|
@@ -836,7 +927,7 @@ export class SubagentBridge {
|
|
|
836
927
|
try {
|
|
837
928
|
this.store.set(info.id, {
|
|
838
929
|
piSessionId: child.session.piSessionId ?? child.task.id,
|
|
839
|
-
piPath: this.archive(child
|
|
930
|
+
piPath: this.archive(child),
|
|
840
931
|
directory: info.directory,
|
|
841
932
|
title: info.title,
|
|
842
933
|
parentID: info.parentID,
|
|
@@ -924,9 +1015,10 @@ export class SubagentBridge {
|
|
|
924
1015
|
this.store.set(id, {
|
|
925
1016
|
deleted: true,
|
|
926
1017
|
piSessionId: child.session.piSessionId ?? child.task.id,
|
|
927
|
-
piPath: this.archive(child
|
|
1018
|
+
piPath: this.archive(child),
|
|
928
1019
|
directory: info.directory,
|
|
929
1020
|
...(info.parentID !== undefined ? { parentID: info.parentID } : {}),
|
|
1021
|
+
task: child.task,
|
|
930
1022
|
created: info.time.created,
|
|
931
1023
|
updated: info.time.updated,
|
|
932
1024
|
});
|
|
@@ -31,8 +31,11 @@ In OpenChamber, open **Settings → Extensions → Add**:
|
|
|
31
31
|
`$(npm root -g)/antechamber/extensions/omo-compat` for an npm install, or
|
|
32
32
|
`<checkout>/extensions/omo-compat` from a source checkout. It needs no build.
|
|
33
33
|
- **URL**, from anywhere:
|
|
34
|
-
`https://github.com/eliottness/antechamber/
|
|
35
|
-
|
|
34
|
+
`https://github.com/eliottness/antechamber#extension/omo-compat`, a Git
|
|
35
|
+
install that OpenChamber offers to update after each release. Every GitHub
|
|
36
|
+
release also attaches
|
|
37
|
+
`https://github.com/eliottness/antechamber/releases/latest/download/antechamber-omo-compat.zip`,
|
|
38
|
+
which installs by URL too, but OpenChamber never updates ZIP installs.
|
|
36
39
|
|
|
37
40
|
There is nothing to approve and no rail icon. Reload open chats after
|
|
38
41
|
installing so their cards pick up the new header.
|
|
@@ -58,16 +58,18 @@ fits where OpenChamber runs:
|
|
|
58
58
|
- source checkout: `<checkout>/extensions/omo-dag`, after `npm run build`
|
|
59
59
|
(which also writes `panel/main.js` and `service/main.js`).
|
|
60
60
|
- **URL**, from anywhere, including a remote OpenChamber you only reach in a
|
|
61
|
-
browser:
|
|
62
|
-
`
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
browser: `https://github.com/eliottness/antechamber#extension/omo-graph`.
|
|
62
|
+
Every release commits the built extension to the `extension/omo-graph`
|
|
63
|
+
branch, so OpenChamber offers it under **Update** in Settings → Extensions.
|
|
64
|
+
To pin a version instead, install its release archive by URL,
|
|
65
|
+
`https://github.com/eliottness/antechamber/releases/download/vX.Y.Z/antechamber-omo-graph.zip`;
|
|
66
|
+
OpenChamber never updates ZIP installs.
|
|
67
|
+
- **ZIP file**, for offline hosts: that archive, downloaded, or built with
|
|
67
68
|
`npm run build && npm run pack:extension -- antechamber-omo-graph.zip`.
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
which is antechamber itself, and rejects it as
|
|
70
|
+
Keep the `#extension/omo-graph` part of the URL. Without it OpenChamber clones
|
|
71
|
+
the repository root, which is antechamber itself, and rejects it as
|
|
72
|
+
`invalid-manifest`, which its toast words as "panel.id must be kebab-case".
|
|
71
73
|
|
|
72
74
|
Approve the request. The extension asks for two things:
|
|
73
75
|
|
|
@@ -36,16 +36,20 @@ exceeds that bound, the service keeps the root and a valid task prefix, removes
|
|
|
36
36
|
edges to omitted tasks, and returns `truncated.omittedNodes`; the panel
|
|
37
37
|
validates that field and displays the count.
|
|
38
38
|
|
|
39
|
-
The connector names
|
|
40
|
-
`
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
The connector names root sessions `ses_<md5(piSessionId)>` (`ocIdFor` in
|
|
40
|
+
`src/session-store.ts`). Sub-agent links use their recorded registry ID when
|
|
41
|
+
available. Unregistered tasks use `ses_<md5(task_id)>` while the task ID has
|
|
42
|
+
one parent, or `ses_<md5(parent_session_id + ":" + task_id)>` when it collides
|
|
43
|
+
across parents. Discovery retains both colliding task records, and the graph
|
|
44
|
+
uses the same identity rules so `openSession` opens the correct child.
|
|
45
|
+
Legacy deletion tombstones retain their owning parent's bare identity, even
|
|
46
|
+
when they predate task metadata in the registry.
|
|
43
47
|
|
|
44
48
|
## Graph model (`src/graph.ts`)
|
|
45
49
|
|
|
46
50
|
- **Run**: every task whose `root_session_id` equals the resolved root. The
|
|
47
51
|
root is resolved from the open session: it is either a root itself, a
|
|
48
|
-
sub-agent (
|
|
52
|
+
sub-agent (resolved through its parent-qualified identity or registry), or a
|
|
49
53
|
registry entry.
|
|
50
54
|
- **Spawn edges**: `parent_session_id` → task. A task launched from inside a
|
|
51
55
|
sub-agent has that sub-agent's own session id as its parent (its
|
|
@@ -54,6 +54,7 @@ function normalizeRegistry(raw) {
|
|
|
54
54
|
for (const [id, value] of Object.entries(raw)) {
|
|
55
55
|
if (!isObject(value)) continue;
|
|
56
56
|
const entry = {};
|
|
57
|
+
if (value.deleted === true) entry.deleted = true;
|
|
57
58
|
if (text(value.piSessionId) !== void 0) entry.piSessionId = text(value.piSessionId);
|
|
58
59
|
if (text(value.parentID) !== void 0) entry.parentID = text(value.parentID);
|
|
59
60
|
if (text(value.title) !== void 0) entry.title = text(value.title);
|
|
@@ -65,6 +66,20 @@ function normalizeRegistry(raw) {
|
|
|
65
66
|
function ocIdFor(piSessionId) {
|
|
66
67
|
return `ses_${createHash("md5").update(piSessionId).digest("hex")}`;
|
|
67
68
|
}
|
|
69
|
+
function taskSessionId(task, tasks, registry) {
|
|
70
|
+
const registered = Object.entries(registry).find(
|
|
71
|
+
([, entry]) => entry.task?.id === task.task_id && entry.task.parentPiId === task.parent_session_id
|
|
72
|
+
);
|
|
73
|
+
if (registered) return registered[0];
|
|
74
|
+
const legacy = ocIdFor(task.task_id);
|
|
75
|
+
const parent = Object.entries(registry).find(([, entry]) => entry.piSessionId === task.parent_session_id)?.[0];
|
|
76
|
+
if (registry[legacy]?.deleted && registry[legacy]?.parentID === (parent ?? ocIdFor(task.parent_session_id ?? "")))
|
|
77
|
+
return legacy;
|
|
78
|
+
const qualified = ocIdFor(`${task.parent_session_id}:${task.task_id}`);
|
|
79
|
+
if (registry[legacy] !== void 0 || registry[qualified] !== void 0 || tasks.some((other) => other.task_id === task.task_id && other.parent_session_id !== task.parent_session_id))
|
|
80
|
+
return qualified;
|
|
81
|
+
return legacy;
|
|
82
|
+
}
|
|
68
83
|
var ACTIVE = /* @__PURE__ */ new Set(["running", "starting"]);
|
|
69
84
|
var WAITING = /* @__PURE__ */ new Set(["queued", "pending", "suspended"]);
|
|
70
85
|
var FAILED = /* @__PURE__ */ new Set(["failed", "error", "lost", "denied", "plan_error"]);
|
|
@@ -108,7 +123,7 @@ ${task.spawn_spec?.instructions ?? ""}`;
|
|
|
108
123
|
}
|
|
109
124
|
function resolveSession(sessionId, tasks, registry) {
|
|
110
125
|
const entry = registry[sessionId];
|
|
111
|
-
const focusTask = tasks.find((t) =>
|
|
126
|
+
const focusTask = tasks.find((t) => taskSessionId(t, tasks, registry) === sessionId);
|
|
112
127
|
let rootPi;
|
|
113
128
|
if (focusTask) rootPi = focusTask.root_session_id ?? focusTask.parent_session_id ?? entry?.task?.parentPiId;
|
|
114
129
|
else if (entry?.task?.parentPiId) rootPi = entry.task.parentPiId;
|
|
@@ -183,7 +198,7 @@ function buildGraph(sessionId, tasks, registry) {
|
|
|
183
198
|
return {
|
|
184
199
|
id: t.task_id,
|
|
185
200
|
kind: "task",
|
|
186
|
-
sessionId:
|
|
201
|
+
sessionId: taskSessionId(t, tasks, registry),
|
|
187
202
|
title: firstLine(t.task_summary ?? t.description ?? t.name, 90) ?? t.task_id,
|
|
188
203
|
state,
|
|
189
204
|
rawStatus: String(t.status ?? "unknown"),
|
|
@@ -322,7 +337,7 @@ var TaskStore = class {
|
|
|
322
337
|
const path = join(dir, "tasks", file);
|
|
323
338
|
if (!isRegularFile(path)) continue;
|
|
324
339
|
const task = this.tasks.read(path, normalizeTask);
|
|
325
|
-
if (task) out.set(task.task_id, task);
|
|
340
|
+
if (task) out.set(JSON.stringify([task.parent_session_id, task.task_id]), task);
|
|
326
341
|
}
|
|
327
342
|
}
|
|
328
343
|
return [...out.values()];
|
|
@@ -31,9 +31,12 @@ In OpenChamber, open **Settings → Extensions → Add**:
|
|
|
31
31
|
installed: the path `antechamber setup` prints,
|
|
32
32
|
`$(npm root -g)/antechamber/extensions/omo-work` for npm installs or
|
|
33
33
|
`<checkout>/extensions/omo-work` after `npm run build`.
|
|
34
|
-
- **URL**:
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
- **URL**: `https://github.com/eliottness/antechamber#extension/omo-work`, a
|
|
35
|
+
Git install that OpenChamber offers to update after each release. The
|
|
36
|
+
release archive,
|
|
37
|
+
`https://github.com/eliottness/antechamber/releases/latest/download/antechamber-omo-work.zip`,
|
|
38
|
+
also installs by URL, but OpenChamber never updates ZIP installs.
|
|
39
|
+
- **ZIP file**: that archive, or
|
|
37
40
|
`npm run build && npm run pack:work-extension -- antechamber-omo-work.zip`.
|
|
38
41
|
|
|
39
42
|
Approve its **local service**. The section then appears in the Work Status
|