@hheei/pi-subagents 0.3.0 → 0.5.0
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/README.md +5 -5
- package/dist/child-control.d.ts +1 -1
- package/dist/child-control.js +89 -26
- package/dist/child-process.d.ts +2 -0
- package/dist/child-process.js +24 -12
- package/dist/domain.d.ts +15 -0
- package/dist/extension.d.ts +3 -4
- package/dist/extension.js +66 -79
- package/dist/host-adapter.d.ts +19 -0
- package/dist/host-adapter.js +139 -0
- package/dist/info.js +32 -6
- package/dist/lease.d.ts +83 -0
- package/dist/lease.js +239 -0
- package/dist/mailbox.d.ts +46 -0
- package/dist/mailbox.js +368 -0
- package/dist/manager.d.ts +17 -19
- package/dist/manager.js +301 -427
- package/dist/process-lock.d.ts +11 -0
- package/dist/process-lock.js +55 -0
- package/dist/protocol.d.ts +2 -2
- package/dist/protocol.js +1 -0
- package/dist/registry.d.ts +1 -1
- package/dist/registry.js +80 -77
- package/dist/rpc-events.d.ts +1 -1
- package/dist/rpc-events.js +2 -0
- package/dist/runtime.d.ts +6 -29
- package/dist/runtime.js +79 -96
- package/dist/state-db.d.ts +52 -0
- package/dist/state-db.js +209 -0
- package/dist/state.d.ts +12 -0
- package/dist/state.js +107 -21
- package/dist/tool-presentation.d.ts +2 -5
- package/dist/tool-presentation.js +38 -5
- package/dist/tools.js +3 -2
- package/package.json +6 -6
- package/dist/bridge-client.d.ts +0 -60
- package/dist/bridge-client.js +0 -459
- package/dist/bridge-server.d.ts +0 -72
- package/dist/bridge-server.js +0 -490
package/dist/host-adapter.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
1
2
|
import { errorMessage, runCommand, shellQuote } from "@hheei/pi-ext-core";
|
|
2
3
|
const DEFAULT_HOST_ORDER = ["herdr", "cmux"];
|
|
3
4
|
/** Selects a presentation host visibly; an explicit unavailable host never falls back. */
|
|
@@ -302,6 +303,144 @@ export function herdrPanelFocused(tabList, workspaceList, tabId) {
|
|
|
302
303
|
const workspaceFocused = workspace?.focused;
|
|
303
304
|
return typeof workspaceFocused === "boolean" ? workspaceFocused : undefined;
|
|
304
305
|
}
|
|
306
|
+
export async function listHerdrAttachments(runner, timeoutMs) {
|
|
307
|
+
const status = await runner.run("herdr", ["status", "server"], { timeoutMs });
|
|
308
|
+
if (status.timedOut || status.exitCode !== 0)
|
|
309
|
+
return { known: false };
|
|
310
|
+
const socket = statusField(status.stdout, "socket");
|
|
311
|
+
if (socket === undefined)
|
|
312
|
+
return { known: false };
|
|
313
|
+
let generation;
|
|
314
|
+
try {
|
|
315
|
+
const stat = statSync(socket);
|
|
316
|
+
generation = `${stat.dev}:${stat.ino}`;
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
return { known: false };
|
|
320
|
+
}
|
|
321
|
+
const tabs = await runner.run("herdr", ["tab", "list"], { timeoutMs });
|
|
322
|
+
if (tabs.timedOut || tabs.exitCode !== 0)
|
|
323
|
+
return { known: false };
|
|
324
|
+
const present = herdrTabIds(tabs.stdout);
|
|
325
|
+
if (present === undefined)
|
|
326
|
+
return { known: false };
|
|
327
|
+
return { known: true, generation, present };
|
|
328
|
+
}
|
|
329
|
+
export function herdrTabIds(output) {
|
|
330
|
+
const entries = herdrCollection(output, "tabs");
|
|
331
|
+
if (entries === undefined)
|
|
332
|
+
return undefined;
|
|
333
|
+
const ids = new Set();
|
|
334
|
+
for (const entry of entries) {
|
|
335
|
+
if (typeof entry !== "object" || entry === null)
|
|
336
|
+
continue;
|
|
337
|
+
const id = entry.tab_id;
|
|
338
|
+
if (typeof id === "string" && id !== "")
|
|
339
|
+
ids.add(id);
|
|
340
|
+
}
|
|
341
|
+
return ids;
|
|
342
|
+
}
|
|
343
|
+
export async function listCmuxAttachments(runner, timeoutMs) {
|
|
344
|
+
const panels = await runner.run("cmux", ["--json", "list-panels"], { timeoutMs });
|
|
345
|
+
if (panels.timedOut || panels.exitCode !== 0)
|
|
346
|
+
return { known: false };
|
|
347
|
+
const present = cmuxSurfaceIds(panels.stdout);
|
|
348
|
+
if (present === undefined)
|
|
349
|
+
return { known: false };
|
|
350
|
+
return { known: true, generation: "cmux", present };
|
|
351
|
+
}
|
|
352
|
+
export function cmuxSurfaceIds(output) {
|
|
353
|
+
let parsed;
|
|
354
|
+
try {
|
|
355
|
+
parsed = JSON.parse(output);
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
const panels = findNamedObject(parsed, "result")?.panels ?? findNamedObject(parsed, "panels");
|
|
361
|
+
const list = Array.isArray(panels) ? panels : Array.isArray(parsed) ? parsed : undefined;
|
|
362
|
+
if (list === undefined)
|
|
363
|
+
return undefined;
|
|
364
|
+
const ids = new Set();
|
|
365
|
+
for (const entry of list) {
|
|
366
|
+
if (typeof entry === "string" && entry !== "")
|
|
367
|
+
ids.add(entry);
|
|
368
|
+
else if (typeof entry === "object" && entry !== null) {
|
|
369
|
+
const id = entry.surface_id;
|
|
370
|
+
if (typeof id === "string" && id !== "")
|
|
371
|
+
ids.add(id);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return ids;
|
|
375
|
+
}
|
|
376
|
+
export function rebindHerdrTab(options) {
|
|
377
|
+
const runner = options.runner ?? systemHostCommandRunner;
|
|
378
|
+
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
379
|
+
return hostAttachment({
|
|
380
|
+
identity: { host: "herdr", attachmentId: options.tabId, createdBy: options.ownerId },
|
|
381
|
+
launch: { stdout: "", stderr: "", exitCode: 0, timedOut: false },
|
|
382
|
+
noun: "tab",
|
|
383
|
+
reportsFocus: true,
|
|
384
|
+
observe: async () => {
|
|
385
|
+
const tabs = await runner.run("herdr", ["tab", "list"], { timeoutMs });
|
|
386
|
+
if (tabs.timedOut || tabs.exitCode !== 0)
|
|
387
|
+
return { process: tabs };
|
|
388
|
+
const ids = herdrTabIds(tabs.stdout);
|
|
389
|
+
if (ids === undefined)
|
|
390
|
+
return { process: tabs };
|
|
391
|
+
return { process: tabs, alive: ids.has(options.tabId) };
|
|
392
|
+
},
|
|
393
|
+
close: () => runner.run("herdr", ["tab", "close", options.tabId], { timeoutMs }),
|
|
394
|
+
ownsAttachment: options.ownsAttachment,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
export function rebindCmuxSurface(options) {
|
|
398
|
+
const runner = options.runner ?? systemHostCommandRunner;
|
|
399
|
+
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
400
|
+
return hostAttachment({
|
|
401
|
+
identity: { host: "cmux", attachmentId: options.surfaceId, createdBy: options.ownerId },
|
|
402
|
+
launch: { stdout: "", stderr: "", exitCode: 0, timedOut: false },
|
|
403
|
+
noun: "surface",
|
|
404
|
+
reportsFocus: false,
|
|
405
|
+
observe: async () => {
|
|
406
|
+
const process = await runner.run("cmux", ["--json", "list-panels"], { timeoutMs });
|
|
407
|
+
if (process.timedOut || process.exitCode !== 0)
|
|
408
|
+
return { process };
|
|
409
|
+
const ids = cmuxSurfaceIds(process.stdout);
|
|
410
|
+
if (ids === undefined)
|
|
411
|
+
return { process };
|
|
412
|
+
return { process, alive: ids.has(options.surfaceId) };
|
|
413
|
+
},
|
|
414
|
+
close: () => runner.run("cmux", ["--json", "close-surface", "--surface", options.surfaceId], {
|
|
415
|
+
timeoutMs,
|
|
416
|
+
}),
|
|
417
|
+
ownsAttachment: options.ownsAttachment,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
function herdrCollection(output, collection) {
|
|
421
|
+
let parsed;
|
|
422
|
+
try {
|
|
423
|
+
parsed = JSON.parse(output);
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
return undefined;
|
|
427
|
+
}
|
|
428
|
+
const list = findNamedObject(parsed, "result")?.[collection];
|
|
429
|
+
return Array.isArray(list) ? list : undefined;
|
|
430
|
+
}
|
|
431
|
+
function statusField(output, name) {
|
|
432
|
+
for (const line of output.split("\n")) {
|
|
433
|
+
const separator = line.indexOf(":");
|
|
434
|
+
if (separator < 0)
|
|
435
|
+
continue;
|
|
436
|
+
if (line.slice(0, separator).trim() !== name)
|
|
437
|
+
continue;
|
|
438
|
+
const value = line.slice(separator + 1).trim();
|
|
439
|
+
if (value !== "")
|
|
440
|
+
return value;
|
|
441
|
+
}
|
|
442
|
+
return undefined;
|
|
443
|
+
}
|
|
305
444
|
function herdrListEntry(output, collection, key, id) {
|
|
306
445
|
let parsed;
|
|
307
446
|
try {
|
package/dist/info.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { stripVTControlCharacters } from "node:util";
|
|
2
2
|
import { Text } from "@earendil-works/pi-tui";
|
|
3
|
+
import { readMessageView } from "@hheei/pi-ext-core";
|
|
3
4
|
import { Type } from "typebox";
|
|
4
5
|
import { Value } from "typebox/value";
|
|
5
6
|
import { VISUAL_SUBAGENT_GLYPH, VISUAL_SUBAGENT_TONE } from "./domain.js";
|
|
@@ -22,19 +23,44 @@ export function displaySubagentText(text) {
|
|
|
22
23
|
.replaceAll("\t", " ")
|
|
23
24
|
.replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/gu, "");
|
|
24
25
|
}
|
|
25
|
-
|
|
26
|
+
function shownSubagent(data) {
|
|
26
27
|
if (!Value.Check(infoSchema, data))
|
|
27
28
|
return undefined;
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
return {
|
|
30
|
+
id: formatSubagentId(displaySubagentText(data.id).replace(/\s+/gu, " ")),
|
|
31
|
+
agent: displaySubagentText(data.agent).replace(/\s+/gu, " "),
|
|
32
|
+
status: data.status,
|
|
33
|
+
message: displaySubagentText(data.message),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function renderSubagentInfo(data, expanded, theme) {
|
|
37
|
+
const shown = shownSubagent(data);
|
|
38
|
+
if (shown === undefined)
|
|
39
|
+
return undefined;
|
|
30
40
|
const heading = theme.fg("dim", "info · agents · ") +
|
|
31
|
-
theme.fg(VISUAL_SUBAGENT_TONE[
|
|
32
|
-
theme.fg("dim", ` ${id} ${agent} · ${
|
|
33
|
-
return expanded ? `${heading}\n${
|
|
41
|
+
theme.fg(VISUAL_SUBAGENT_TONE[shown.status], VISUAL_SUBAGENT_GLYPH[shown.status]) +
|
|
42
|
+
theme.fg("dim", ` ${shown.id} ${shown.agent} · ${shown.status}`);
|
|
43
|
+
return expanded ? `${heading}\n${shown.message}` : heading;
|
|
34
44
|
}
|
|
35
45
|
/** Native transcript entries survive resume and never enter the model context. */
|
|
36
46
|
export function registerSubagentInfo(pi) {
|
|
37
47
|
pi.registerEntryRenderer(SUBAGENT_INFO_TYPE, (entry, options, theme) => {
|
|
48
|
+
const create = readMessageView(pi);
|
|
49
|
+
const shown = create === undefined ? undefined : shownSubagent(entry.data);
|
|
50
|
+
if (create !== undefined && shown !== undefined) {
|
|
51
|
+
const message = shown.message.trim();
|
|
52
|
+
return create({
|
|
53
|
+
theme,
|
|
54
|
+
header: {
|
|
55
|
+
kind: "agents",
|
|
56
|
+
summary: `${VISUAL_SUBAGENT_GLYPH[shown.status]} ${shown.id} ${shown.agent} · ${shown.status}`,
|
|
57
|
+
tone: VISUAL_SUBAGENT_TONE[shown.status],
|
|
58
|
+
},
|
|
59
|
+
...(message === "" ? {} : { body: shown.message }),
|
|
60
|
+
stateHolder: entry,
|
|
61
|
+
forcedExpanded: options.expanded,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
38
64
|
const text = renderSubagentInfo(entry.data, options.expanded, theme);
|
|
39
65
|
return text === undefined ? undefined : new Text(text, 1, 0);
|
|
40
66
|
});
|
package/dist/lease.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { SubagentRecord } from "./domain.js";
|
|
2
|
+
import { type SubagentRegistry } from "./registry.js";
|
|
3
|
+
/**
|
|
4
|
+
* Durable ownership of one parent session's children. Callers learn three operations. The parent
|
|
5
|
+
* and child lifecycle matrices stay behind them.
|
|
6
|
+
*
|
|
7
|
+
* `commit` is the only write: intent, a runtime, or its removal. `roster` is this session's ledger.
|
|
8
|
+
* `ready` is the barrier before a model turn: it rebinds surfaces that are still there and
|
|
9
|
+
* reopens only the panels a new host generation took down. Cancel is not an operation.
|
|
10
|
+
*/
|
|
11
|
+
export declare const RUNTIME_ENDED_RESUME = "The child runtime ended while a turn was in flight; the session is unchanged and a send resumes it";
|
|
12
|
+
export declare const PARENT_EXIT_NOTE = "The child runtime ended with the parent process; a later send starts it again from the same session";
|
|
13
|
+
/** How a removed runtime is recorded. `clear` only drops it. `drop` is a gone writer. */
|
|
14
|
+
export type RuntimeGoneOutcome = "clear" | "drop" | "parent-exit" | "finished";
|
|
15
|
+
export declare const LEASE_EDGES: readonly ["spawn-intent", "runtime-bound", "attachment-bound", "stop-intent", "runtime-gone", "left-session"];
|
|
16
|
+
export type LeaseEdge = {
|
|
17
|
+
readonly type: "spawn-intent";
|
|
18
|
+
} | {
|
|
19
|
+
readonly type: "runtime-bound";
|
|
20
|
+
readonly runtimeIdentity: string;
|
|
21
|
+
readonly endpoint: string;
|
|
22
|
+
} | {
|
|
23
|
+
readonly type: "attachment-bound";
|
|
24
|
+
readonly attachment: NonNullable<NonNullable<SubagentRecord["runtime"]>["attachment"]>;
|
|
25
|
+
} | {
|
|
26
|
+
readonly type: "stop-intent";
|
|
27
|
+
} | {
|
|
28
|
+
readonly type: "runtime-gone";
|
|
29
|
+
readonly outcome?: RuntimeGoneOutcome;
|
|
30
|
+
} | {
|
|
31
|
+
readonly type: "left-session";
|
|
32
|
+
};
|
|
33
|
+
export type HostList = {
|
|
34
|
+
readonly known: false;
|
|
35
|
+
} | {
|
|
36
|
+
readonly known: true;
|
|
37
|
+
readonly generation: string;
|
|
38
|
+
readonly present: ReadonlySet<string>;
|
|
39
|
+
};
|
|
40
|
+
export interface HostDirectory {
|
|
41
|
+
readonly host: "herdr" | "cmux";
|
|
42
|
+
list(): Promise<HostList>;
|
|
43
|
+
/** Opens the same child session in a new panel. Does not write the lease. */
|
|
44
|
+
open(record: SubagentRecord): Promise<{
|
|
45
|
+
readonly id: string;
|
|
46
|
+
} | {
|
|
47
|
+
readonly failure: string;
|
|
48
|
+
}>;
|
|
49
|
+
/** True when a human Pi already has this child session open. Those children are not rehydrated. */
|
|
50
|
+
occupied?(record: SubagentRecord): boolean;
|
|
51
|
+
}
|
|
52
|
+
export interface ProcessDirectory {
|
|
53
|
+
alive(pid: number): boolean;
|
|
54
|
+
}
|
|
55
|
+
export type LeaseReady = {
|
|
56
|
+
readonly ok: true;
|
|
57
|
+
readonly rebound: readonly string[];
|
|
58
|
+
readonly rehydrated: readonly string[];
|
|
59
|
+
readonly stopped: readonly string[];
|
|
60
|
+
/** Children whose runtime was dropped because the writer is gone. */
|
|
61
|
+
readonly released: readonly string[];
|
|
62
|
+
} | {
|
|
63
|
+
readonly ok: false;
|
|
64
|
+
readonly reason: "unknown";
|
|
65
|
+
};
|
|
66
|
+
export interface ConversationLease {
|
|
67
|
+
commit(id: string, edge: LeaseEdge, expectedRevision?: number): Promise<SubagentRecord>;
|
|
68
|
+
roster(): Promise<readonly SubagentRecord[]>;
|
|
69
|
+
ready(): Promise<LeaseReady>;
|
|
70
|
+
}
|
|
71
|
+
type RegistryStore = Pick<SubagentRegistry, "get" | "list" | "update" | "path">;
|
|
72
|
+
export declare function createConversationLease(options: {
|
|
73
|
+
readonly registry: RegistryStore;
|
|
74
|
+
readonly hosts: HostDirectory;
|
|
75
|
+
readonly processes: ProcessDirectory;
|
|
76
|
+
readonly forgetRuntime?: (runtimeIdentity: string) => void;
|
|
77
|
+
}): ConversationLease;
|
|
78
|
+
export declare function commitLease(registry: Pick<SubagentRegistry, "get" | "update">, id: string, edge: LeaseEdge, expectedRevision?: number, expectedRuntimeIdentity?: string): Promise<SubagentRecord>;
|
|
79
|
+
/** `undefined` means the edge changes nothing, so the caller must not write. */
|
|
80
|
+
export declare function applyLeaseEdge(record: SubagentRecord, edge: LeaseEdge): SubagentRecord | undefined;
|
|
81
|
+
export declare function readHostGeneration(filePath: string, parentSessionId: string): Promise<string | undefined>;
|
|
82
|
+
export declare function writeHostGeneration(filePath: string, parentSessionId: string, generation: string): Promise<void>;
|
|
83
|
+
export {};
|
package/dist/lease.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { processLockHeld, processLockPath } from "./process-lock.js";
|
|
3
|
+
import { SubagentRegistryError } from "./registry.js";
|
|
4
|
+
import { openStateDb, readMeta, writeMeta } from "./state-db.js";
|
|
5
|
+
/**
|
|
6
|
+
* Durable ownership of one parent session's children. Callers learn three operations. The parent
|
|
7
|
+
* and child lifecycle matrices stay behind them.
|
|
8
|
+
*
|
|
9
|
+
* `commit` is the only write: intent, a runtime, or its removal. `roster` is this session's ledger.
|
|
10
|
+
* `ready` is the barrier before a model turn: it rebinds surfaces that are still there and
|
|
11
|
+
* reopens only the panels a new host generation took down. Cancel is not an operation.
|
|
12
|
+
*/
|
|
13
|
+
export const RUNTIME_ENDED_RESUME = "The child runtime ended while a turn was in flight; the session is unchanged and a send resumes it";
|
|
14
|
+
export const PARENT_EXIT_NOTE = "The child runtime ended with the parent process; a later send starts it again from the same session";
|
|
15
|
+
export const LEASE_EDGES = [
|
|
16
|
+
"spawn-intent",
|
|
17
|
+
"runtime-bound",
|
|
18
|
+
"attachment-bound",
|
|
19
|
+
"stop-intent",
|
|
20
|
+
"runtime-gone",
|
|
21
|
+
"left-session",
|
|
22
|
+
];
|
|
23
|
+
export function createConversationLease(options) {
|
|
24
|
+
const { registry, hosts, processes, forgetRuntime } = options;
|
|
25
|
+
return {
|
|
26
|
+
commit(id, edge, expectedRevision) {
|
|
27
|
+
return commitLease(registry, id, edge, expectedRevision);
|
|
28
|
+
},
|
|
29
|
+
roster() {
|
|
30
|
+
return registry.list();
|
|
31
|
+
},
|
|
32
|
+
ready() {
|
|
33
|
+
return reconcile(registry, hosts, processes, forgetRuntime);
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export async function commitLease(registry, id, edge, expectedRevision, expectedRuntimeIdentity) {
|
|
38
|
+
const current = await registry.get(id);
|
|
39
|
+
if (current === undefined) {
|
|
40
|
+
throw new SubagentRegistryError("unknown_child", `Unknown child ${id}`);
|
|
41
|
+
}
|
|
42
|
+
const next = applyLeaseEdge(current, edge);
|
|
43
|
+
if (next === undefined)
|
|
44
|
+
return current;
|
|
45
|
+
return registry.update(id, expectedRevision ?? current.revision, () => next, expectedRuntimeIdentity);
|
|
46
|
+
}
|
|
47
|
+
/** `undefined` means the edge changes nothing, so the caller must not write. */
|
|
48
|
+
export function applyLeaseEdge(record, edge) {
|
|
49
|
+
switch (edge.type) {
|
|
50
|
+
case "spawn-intent":
|
|
51
|
+
return record.intent === "active" ? undefined : { ...record, intent: "active" };
|
|
52
|
+
case "attachment-bound": {
|
|
53
|
+
if (record.runtime?.attachment !== undefined)
|
|
54
|
+
return undefined;
|
|
55
|
+
if (record.runtime === undefined) {
|
|
56
|
+
throw new Error(`Child ${record.subagentId} has no runtime to attach`);
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
...record,
|
|
60
|
+
runtime: { ...record.runtime, attachment: edge.attachment },
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
case "runtime-bound": {
|
|
64
|
+
if (record.runtime?.runtimeIdentity === edge.runtimeIdentity &&
|
|
65
|
+
record.runtime.endpoint === edge.endpoint &&
|
|
66
|
+
record.runtime.attachment === undefined) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
...record,
|
|
71
|
+
runtime: { runtimeIdentity: edge.runtimeIdentity, endpoint: edge.endpoint },
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
case "stop-intent":
|
|
75
|
+
return record.intent === "stopped" ? undefined : { ...record, intent: "stopped" };
|
|
76
|
+
case "runtime-gone":
|
|
77
|
+
return applyRuntimeGone(record, edge.outcome ?? "clear");
|
|
78
|
+
case "left-session":
|
|
79
|
+
if (record.runtime === undefined && record.state === "done")
|
|
80
|
+
return undefined;
|
|
81
|
+
return {
|
|
82
|
+
...omitRuntime(record),
|
|
83
|
+
state: "done",
|
|
84
|
+
interrupted: "The child left its session; the surface is no longer controlled by the parent",
|
|
85
|
+
};
|
|
86
|
+
default: {
|
|
87
|
+
const unreachable = edge;
|
|
88
|
+
return unreachable;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function applyRuntimeGone(record, outcome) {
|
|
93
|
+
if (outcome === "clear") {
|
|
94
|
+
if (record.runtime === undefined)
|
|
95
|
+
return undefined;
|
|
96
|
+
return omitRuntime(record);
|
|
97
|
+
}
|
|
98
|
+
if (outcome === "drop") {
|
|
99
|
+
if (record.intent === "stopped") {
|
|
100
|
+
if (record.runtime === undefined)
|
|
101
|
+
return undefined;
|
|
102
|
+
return omitRuntime(record);
|
|
103
|
+
}
|
|
104
|
+
if (record.runtime === undefined && record.state !== "running")
|
|
105
|
+
return undefined;
|
|
106
|
+
const rest = omitRuntime(record);
|
|
107
|
+
if (record.state !== "running")
|
|
108
|
+
return rest;
|
|
109
|
+
return { ...rest, state: "blocked", interrupted: RUNTIME_ENDED_RESUME };
|
|
110
|
+
}
|
|
111
|
+
if (outcome === "parent-exit") {
|
|
112
|
+
if (record.runtime === undefined) {
|
|
113
|
+
if (record.intent === "stopped" || record.state !== "running")
|
|
114
|
+
return undefined;
|
|
115
|
+
return { ...record, state: "done", interrupted: PARENT_EXIT_NOTE };
|
|
116
|
+
}
|
|
117
|
+
const rest = omitRuntime(record);
|
|
118
|
+
return {
|
|
119
|
+
...rest,
|
|
120
|
+
...(record.state === "running" ? { state: "done" } : {}),
|
|
121
|
+
interrupted: PARENT_EXIT_NOTE,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (record.runtime === undefined && record.state === "done")
|
|
125
|
+
return undefined;
|
|
126
|
+
const rest = record.runtime === undefined ? record : omitRuntime(record);
|
|
127
|
+
return { ...rest, state: "done" };
|
|
128
|
+
}
|
|
129
|
+
async function reconcile(registry, hosts, processes, forgetRuntime) {
|
|
130
|
+
const list = await hosts.list();
|
|
131
|
+
if (!list.known)
|
|
132
|
+
return { ok: false, reason: "unknown" };
|
|
133
|
+
const all = await registry.list();
|
|
134
|
+
const released = new Set();
|
|
135
|
+
for (const record of all) {
|
|
136
|
+
if (record.runtime === undefined)
|
|
137
|
+
continue;
|
|
138
|
+
const lockPath = processLockPath(registry.path, record.parentSessionId, record.subagentId);
|
|
139
|
+
if (lockPath === undefined || !existsSync(lockPath))
|
|
140
|
+
continue;
|
|
141
|
+
let held = true;
|
|
142
|
+
try {
|
|
143
|
+
held = processLockHeld(lockPath);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (held)
|
|
149
|
+
continue;
|
|
150
|
+
await releaseRuntime(registry, record, { type: "runtime-gone", outcome: "drop" }, forgetRuntime);
|
|
151
|
+
released.add(record.subagentId);
|
|
152
|
+
}
|
|
153
|
+
const records = all.filter((record) => !released.has(record.subagentId));
|
|
154
|
+
const parentSessionId = all[0]?.parentSessionId;
|
|
155
|
+
const stored = parentSessionId === undefined
|
|
156
|
+
? undefined
|
|
157
|
+
: await readHostGeneration(registry.path, parentSessionId);
|
|
158
|
+
const rebound = [];
|
|
159
|
+
const rehydrated = [];
|
|
160
|
+
const stopped = [];
|
|
161
|
+
let held = false;
|
|
162
|
+
const panels = records.flatMap((record) => {
|
|
163
|
+
const id = panelId(record);
|
|
164
|
+
return id === undefined ? [] : [{ record, id }];
|
|
165
|
+
});
|
|
166
|
+
const present = panels.filter((item) => list.present.has(item.id));
|
|
167
|
+
const missing = panels.filter((item) => !list.present.has(item.id));
|
|
168
|
+
for (const item of present)
|
|
169
|
+
rebound.push(item.record.subagentId);
|
|
170
|
+
const generationChanged = stored !== undefined && stored !== list.generation;
|
|
171
|
+
const reopen = generationChanged || (stored === undefined && present.length === 0 && missing.length > 0);
|
|
172
|
+
if (reopen) {
|
|
173
|
+
for (const item of missing) {
|
|
174
|
+
if (item.record.intent !== "active")
|
|
175
|
+
continue;
|
|
176
|
+
if (hosts.occupied?.(item.record) === true) {
|
|
177
|
+
held = true;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const opened = await hosts.open(item.record);
|
|
181
|
+
if ("failure" in opened)
|
|
182
|
+
continue;
|
|
183
|
+
await commitLease(registry, item.record.subagentId, {
|
|
184
|
+
type: "attachment-bound",
|
|
185
|
+
attachment: { kind: "panel", host: hosts.host, id: opened.id },
|
|
186
|
+
});
|
|
187
|
+
rehydrated.push(item.record.subagentId);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
for (const item of missing) {
|
|
192
|
+
if (item.record.intent === "active") {
|
|
193
|
+
await commitLease(registry, item.record.subagentId, { type: "stop-intent" });
|
|
194
|
+
stopped.push(item.record.subagentId);
|
|
195
|
+
}
|
|
196
|
+
await releaseRuntime(registry, item.record, { type: "runtime-gone" }, forgetRuntime);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
for (const record of records) {
|
|
200
|
+
const attachment = record.runtime?.attachment;
|
|
201
|
+
if (attachment?.kind !== "process")
|
|
202
|
+
continue;
|
|
203
|
+
if (processes.alive(attachment.pid)) {
|
|
204
|
+
rebound.push(record.subagentId);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
await releaseRuntime(registry, record, { type: "runtime-gone", outcome: "drop" }, forgetRuntime);
|
|
208
|
+
released.add(record.subagentId);
|
|
209
|
+
}
|
|
210
|
+
if (!held && parentSessionId !== undefined && stored !== list.generation)
|
|
211
|
+
await writeHostGeneration(registry.path, parentSessionId, list.generation);
|
|
212
|
+
return { ok: true, rebound, rehydrated, stopped, released: [...released] };
|
|
213
|
+
}
|
|
214
|
+
function panelId(record) {
|
|
215
|
+
const attachment = record.runtime?.attachment;
|
|
216
|
+
return attachment?.kind === "panel" ? attachment.id : undefined;
|
|
217
|
+
}
|
|
218
|
+
async function releaseRuntime(registry, record, edge, forgetRuntime) {
|
|
219
|
+
const identity = record.runtime?.runtimeIdentity;
|
|
220
|
+
const next = await commitLease(registry, record.subagentId, edge);
|
|
221
|
+
if (identity !== undefined && next.runtime === undefined)
|
|
222
|
+
forgetRuntime?.(identity);
|
|
223
|
+
}
|
|
224
|
+
function omitRuntime(record) {
|
|
225
|
+
const { runtime: _runtime, ...rest } = record;
|
|
226
|
+
return rest;
|
|
227
|
+
}
|
|
228
|
+
export function readHostGeneration(filePath, parentSessionId) {
|
|
229
|
+
const value = readMeta(openStateDb(filePath), parentSessionId, "hostGeneration");
|
|
230
|
+
return Promise.resolve(value === "" ? undefined : value);
|
|
231
|
+
}
|
|
232
|
+
export function writeHostGeneration(filePath, parentSessionId, generation) {
|
|
233
|
+
const db = openStateDb(filePath);
|
|
234
|
+
const current = readMeta(db, parentSessionId, "hostGeneration");
|
|
235
|
+
if (current === generation)
|
|
236
|
+
return Promise.resolve();
|
|
237
|
+
writeMeta(db, parentSessionId, "hostGeneration", generation);
|
|
238
|
+
return Promise.resolve();
|
|
239
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface MailboxRequest {
|
|
2
|
+
readonly childId: string;
|
|
3
|
+
readonly operation: string;
|
|
4
|
+
readonly payload: unknown;
|
|
5
|
+
}
|
|
6
|
+
export interface MailboxTransport {
|
|
7
|
+
readonly endpoint: string;
|
|
8
|
+
isConnected(childId: string): boolean;
|
|
9
|
+
disconnect(childId: string): void;
|
|
10
|
+
request(childId: string, operation: string, payload?: unknown, options?: {
|
|
11
|
+
readonly timeoutMs?: number;
|
|
12
|
+
readonly signal?: AbortSignal;
|
|
13
|
+
}): Promise<unknown>;
|
|
14
|
+
onEvent(listener: (childId: string, event: unknown) => void): () => void;
|
|
15
|
+
onConnectionChange(listener: (childId: string, connected: boolean) => void): () => void;
|
|
16
|
+
waitUntilSeen(childId: string, options?: {
|
|
17
|
+
readonly timeoutMs?: number;
|
|
18
|
+
readonly signal?: AbortSignal;
|
|
19
|
+
}): Promise<boolean>;
|
|
20
|
+
close(): void;
|
|
21
|
+
}
|
|
22
|
+
export declare function createMailboxTransport(options: {
|
|
23
|
+
readonly filePath: string;
|
|
24
|
+
readonly parentSessionId: string;
|
|
25
|
+
readonly handleRequest: (request: MailboxRequest) => Promise<unknown>;
|
|
26
|
+
readonly pollMs?: number;
|
|
27
|
+
readonly diagnose?: (message: string) => void;
|
|
28
|
+
}): MailboxTransport;
|
|
29
|
+
export interface ChildMailbox {
|
|
30
|
+
readonly connected: boolean;
|
|
31
|
+
start(): void;
|
|
32
|
+
report(operation: string, payload: unknown, signal?: AbortSignal): Promise<unknown>;
|
|
33
|
+
sendEvent(event: unknown): void;
|
|
34
|
+
close(): void;
|
|
35
|
+
}
|
|
36
|
+
export declare function attachChildMailbox(options: {
|
|
37
|
+
readonly filePath: string;
|
|
38
|
+
readonly parentSessionId: string;
|
|
39
|
+
readonly subagentId: string;
|
|
40
|
+
readonly pollMs?: number;
|
|
41
|
+
readonly handleRequest: (request: {
|
|
42
|
+
readonly operation: string;
|
|
43
|
+
readonly payload: unknown;
|
|
44
|
+
}) => Promise<unknown>;
|
|
45
|
+
readonly diagnose?: (message: string) => void;
|
|
46
|
+
}): ChildMailbox;
|