antechamber 1.0.3 → 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 +7 -0
- 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,13 @@ 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
|
+
|
|
10
17
|
## [1.0.3] - 2026-10-20
|
|
11
18
|
|
|
12
19
|
### Added
|
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()];
|