antechamber 1.0.3 → 1.0.5
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 +14 -0
- package/dist/session-store.d.ts +3 -0
- package/dist/session-store.js +40 -2
- package/dist/sessions.js +32 -24
- package/dist/subagents.d.ts +7 -4
- package/dist/subagents.js +139 -47
- package/extensions/omo-compat/package.json +1 -1
- 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/package.json +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ and this project uses [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.0.5] - 2026-10-20
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Reduce startup CPU usage and boot delays for large session histories by committing restoration metadata
|
|
15
|
+
in one registry write, while preserving restored costs, child sessions and independent registry writers.
|
|
16
|
+
|
|
17
|
+
## [1.0.4] - 2026-10-20
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Keep subagents with colliding task IDs in different sessions separate, and recover a foreign child
|
|
22
|
+
archive from its own retained transcript.
|
|
23
|
+
|
|
10
24
|
## [1.0.3] - 2026-10-20
|
|
11
25
|
|
|
12
26
|
### Added
|
package/dist/session-store.d.ts
CHANGED
|
@@ -91,12 +91,15 @@ export declare class SessionStore {
|
|
|
91
91
|
readonly stateDir: string;
|
|
92
92
|
private readonly file;
|
|
93
93
|
private entries;
|
|
94
|
+
private batchState;
|
|
94
95
|
constructor(stateDir?: string);
|
|
95
96
|
private load;
|
|
96
97
|
private readEntries;
|
|
97
98
|
private withRegistryLock;
|
|
98
99
|
private persist;
|
|
99
100
|
private mutate;
|
|
101
|
+
/** Synchronous restoration reads its own changes, then merges them under one short-lived write lock. */
|
|
102
|
+
batch<T>(action: () => T): T;
|
|
100
103
|
get(ocSessionId: string): SessionRegistryEntry | undefined;
|
|
101
104
|
idFor(piSessionId: string): string | undefined;
|
|
102
105
|
childIds(): string[];
|
package/dist/session-store.js
CHANGED
|
@@ -378,12 +378,15 @@ export class SessionStore {
|
|
|
378
378
|
stateDir;
|
|
379
379
|
file;
|
|
380
380
|
entries;
|
|
381
|
+
batchState;
|
|
381
382
|
constructor(stateDir) {
|
|
382
383
|
const dir = stateDir ?? envSetting("STATE_DIR") ?? defaultStateDir();
|
|
383
384
|
this.stateDir = dir;
|
|
384
385
|
this.file = join(dir, "sessions.json");
|
|
385
386
|
}
|
|
386
387
|
load() {
|
|
388
|
+
if (this.batchState !== undefined)
|
|
389
|
+
return this.batchState.entries;
|
|
387
390
|
if (this.entries !== undefined)
|
|
388
391
|
return this.entries;
|
|
389
392
|
this.entries = this.readEntries();
|
|
@@ -452,6 +455,11 @@ export class SessionStore {
|
|
|
452
455
|
renameSync(tmp, this.file);
|
|
453
456
|
}
|
|
454
457
|
mutate(change) {
|
|
458
|
+
if (this.batchState !== undefined) {
|
|
459
|
+
this.batchState.changes.push(change);
|
|
460
|
+
change(this.batchState.entries);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
455
463
|
this.withRegistryLock(() => {
|
|
456
464
|
const entries = this.readEntries();
|
|
457
465
|
if (change(entries))
|
|
@@ -459,6 +467,31 @@ export class SessionStore {
|
|
|
459
467
|
this.entries = entries;
|
|
460
468
|
});
|
|
461
469
|
}
|
|
470
|
+
/** Synchronous restoration reads its own changes, then merges them under one short-lived write lock. */
|
|
471
|
+
batch(action) {
|
|
472
|
+
if (this.batchState !== undefined)
|
|
473
|
+
return action();
|
|
474
|
+
const changes = [];
|
|
475
|
+
const batch = { entries: structuredClone(this.load()), changes };
|
|
476
|
+
this.batchState = batch;
|
|
477
|
+
try {
|
|
478
|
+
const result = action();
|
|
479
|
+
this.batchState = undefined;
|
|
480
|
+
if (batch.changes.length !== 0) {
|
|
481
|
+
this.mutate((entries) => {
|
|
482
|
+
let changed = false;
|
|
483
|
+
for (const change of batch.changes)
|
|
484
|
+
if (change(entries))
|
|
485
|
+
changed = true;
|
|
486
|
+
return changed;
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
return result;
|
|
490
|
+
}
|
|
491
|
+
finally {
|
|
492
|
+
this.batchState = undefined;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
462
495
|
get(ocSessionId) {
|
|
463
496
|
return this.load().get(ocSessionId);
|
|
464
497
|
}
|
|
@@ -471,18 +504,23 @@ export class SessionStore {
|
|
|
471
504
|
.map(([id]) => id);
|
|
472
505
|
}
|
|
473
506
|
set(ocSessionId, entry) {
|
|
507
|
+
const saved = this.batchState === undefined ? entry : structuredClone(entry);
|
|
474
508
|
this.mutate((entries) => {
|
|
475
|
-
entries.set(ocSessionId,
|
|
509
|
+
entries.set(ocSessionId, this.batchState === undefined ? saved : structuredClone(saved));
|
|
476
510
|
return true;
|
|
477
511
|
});
|
|
478
512
|
}
|
|
479
513
|
/** Merge a partial update (e.g. title override, updated timestamp). */
|
|
480
514
|
update(ocSessionId, patch) {
|
|
515
|
+
const saved = this.batchState === undefined ? patch : structuredClone(patch);
|
|
481
516
|
this.mutate((entries) => {
|
|
482
517
|
const previous = entries.get(ocSessionId);
|
|
483
518
|
if (previous === undefined)
|
|
484
519
|
return false;
|
|
485
|
-
entries.set(ocSessionId, {
|
|
520
|
+
entries.set(ocSessionId, {
|
|
521
|
+
...previous,
|
|
522
|
+
...(this.batchState === undefined ? saved : structuredClone(saved)),
|
|
523
|
+
});
|
|
486
524
|
return true;
|
|
487
525
|
});
|
|
488
526
|
}
|
package/dist/sessions.js
CHANGED
|
@@ -308,14 +308,18 @@ export class SessionManager {
|
|
|
308
308
|
.sort((a, b) => this.lifecycle.lastUsed(b) - this.lifecycle.lastUsed(a));
|
|
309
309
|
}
|
|
310
310
|
list(directory) {
|
|
311
|
-
this.
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
311
|
+
return this.store.batch(() => {
|
|
312
|
+
this.refreshNativeTitles();
|
|
313
|
+
this.subagents.restoreAll();
|
|
314
|
+
if (directory !== undefined) {
|
|
315
|
+
this.ensureScanned(directory);
|
|
316
|
+
return [...this.sessions.values()]
|
|
317
|
+
.filter((s) => s.info.directory === directory)
|
|
318
|
+
.map((s) => this.sessionInfo(s));
|
|
319
|
+
}
|
|
320
|
+
// Unfiltered: keep the old semantics — live sessions only.
|
|
321
|
+
return [...this.sessions.values()].filter((s) => s.restored === undefined).map((s) => this.sessionInfo(s));
|
|
322
|
+
});
|
|
319
323
|
}
|
|
320
324
|
/**
|
|
321
325
|
* Every restorable session on disk (all directories) plus live ones. Backs the
|
|
@@ -323,10 +327,12 @@ export class SessionManager {
|
|
|
323
327
|
* global/recent view; `list(directory)` stays the per-project source of truth.
|
|
324
328
|
*/
|
|
325
329
|
listAll() {
|
|
326
|
-
this.
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
+
return this.store.batch(() => {
|
|
331
|
+
this.refreshNativeTitles();
|
|
332
|
+
this.ensureAllScanned();
|
|
333
|
+
this.subagents.restoreAll();
|
|
334
|
+
return [...this.sessions.values()].map((s) => this.sessionInfo(s));
|
|
335
|
+
});
|
|
330
336
|
}
|
|
331
337
|
sessionInfo(s) {
|
|
332
338
|
if (s.restored !== undefined)
|
|
@@ -334,18 +340,20 @@ export class SessionManager {
|
|
|
334
340
|
return s.info;
|
|
335
341
|
}
|
|
336
342
|
get(sessionId) {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
343
|
+
return this.store.batch(() => {
|
|
344
|
+
if (this.disposed)
|
|
345
|
+
return undefined;
|
|
346
|
+
const live = this.sessions.get(sessionId);
|
|
347
|
+
if (live !== undefined) {
|
|
348
|
+
this.refreshNativeTitles();
|
|
349
|
+
this.sessionInfo(live);
|
|
350
|
+
return live;
|
|
351
|
+
}
|
|
352
|
+
const restored = this.subagents.restore(sessionId) ?? this.resolveRestored(sessionId);
|
|
353
|
+
if (restored !== undefined)
|
|
354
|
+
this.sessionInfo(restored);
|
|
355
|
+
return restored;
|
|
356
|
+
});
|
|
349
357
|
}
|
|
350
358
|
async remove(sessionId) {
|
|
351
359
|
return this.lifecycle.run(() => this.removeLive(sessionId));
|
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
|
});
|
|
@@ -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()];
|