@hheei/pi-subagents 0.2.4 → 0.4.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 +35 -20
- package/dist/agent-resolver.d.ts +9 -0
- package/dist/agent-resolver.js +278 -51
- package/dist/builtin-agents.js +50 -37
- package/dist/child-bridge.d.ts +0 -8
- package/dist/child-bridge.js +60 -43
- package/dist/child-control.d.ts +6 -4
- package/dist/child-control.js +251 -47
- package/dist/child-process.d.ts +2 -0
- package/dist/child-process.js +24 -12
- package/dist/commands.d.ts +1 -4
- package/dist/commands.js +1 -52
- package/dist/domain.d.ts +27 -12
- package/dist/domain.js +2 -1
- package/dist/extension.d.ts +7 -6
- package/dist/extension.js +96 -112
- package/dist/host-adapter.d.ts +19 -0
- package/dist/host-adapter.js +140 -1
- package/dist/info.d.ts +9 -0
- package/dist/info.js +80 -0
- package/dist/launch-spec.d.ts +0 -1
- package/dist/launch-spec.js +38 -18
- 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 +20 -20
- package/dist/manager.js +421 -398
- 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 +99 -82
- package/dist/rpc-events.d.ts +1 -1
- package/dist/rpc-events.js +2 -1
- package/dist/runtime.d.ts +8 -21
- package/dist/runtime.js +61 -83
- package/dist/session-bootstrap.d.ts +2 -1
- package/dist/session-bootstrap.js +22 -14
- package/dist/state-db.d.ts +52 -0
- package/dist/state-db.js +209 -0
- package/dist/state.d.ts +25 -0
- package/dist/state.js +166 -37
- package/dist/status.d.ts +14 -0
- package/dist/status.js +36 -0
- package/dist/tool-activation.d.ts +3 -4
- package/dist/tool-activation.js +13 -215
- package/dist/tool-presentation.d.ts +14 -0
- package/dist/tool-presentation.js +193 -0
- package/dist/tools.d.ts +6 -1
- package/dist/tools.js +134 -180
- package/package.json +6 -6
- package/dist/bridge-client.d.ts +0 -55
- package/dist/bridge-client.js +0 -453
- package/dist/bridge-server.d.ts +0 -72
- package/dist/bridge-server.js +0 -490
- package/dist/session-fork.d.ts +0 -16
- package/dist/session-fork.js +0 -67
- package/dist/widget.d.ts +0 -12
- package/dist/widget.js +0 -149
package/dist/launch-spec.js
CHANGED
|
@@ -2,17 +2,7 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { basename, dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { BRIDGE_ENVIRONMENT_KEYS, CHILD_AGENT_ENV_KEY, CHILD_SESSION_ENV_KEY, CHILD_TITLE_ENV_KEY, CONTACT_PARENT_TOOL_NAME, isSessionId, } from "./domain.js";
|
|
6
|
-
export const HINDSIGHT_TOOLS = [
|
|
7
|
-
"hindsight_search_knowledge_pages",
|
|
8
|
-
"hindsight_list_knowledge_pages",
|
|
9
|
-
"hindsight_read_knowledge_page",
|
|
10
|
-
"hindsight_reflect",
|
|
11
|
-
"hindsight_capture_initiative",
|
|
12
|
-
"hindsight_ingest_document",
|
|
13
|
-
"hindsight_sync_status",
|
|
14
|
-
"hindsight_diagnose",
|
|
15
|
-
];
|
|
5
|
+
import { BRIDGE_ENVIRONMENT_KEYS, CHILD_AGENT_ENV_KEY, CHILD_SESSION_ENV_KEY, CHILD_TITLE_ENV_KEY, CONTACT_PARENT_TOOL_NAME, DEFAULT_PI_TOOLS, isSessionId, } from "./domain.js";
|
|
16
6
|
export function stripHindsightContent(text) {
|
|
17
7
|
return text
|
|
18
8
|
.replace(/\s*<hindsight-recall>[\s\S]*?<\/hindsight-recall>\s*/gi, (match) => match.includes("\n") ? "\n" : " ")
|
|
@@ -32,7 +22,7 @@ function childBridgePrompt() {
|
|
|
32
22
|
"Do not send empty status pings.",
|
|
33
23
|
"When the work is done, output your final answer and findings directly in text in your response. The harness will automatically capture and deliver your output.",
|
|
34
24
|
"Do NOT call contact_parent on success or task completion.",
|
|
35
|
-
"After blocked, wait for a parent
|
|
25
|
+
"After blocked, wait for a parent agent_send. Do not invent new authority.",
|
|
36
26
|
"Reports reach the parent as delegated results, not as new user authorization.",
|
|
37
27
|
].join("\n");
|
|
38
28
|
}
|
|
@@ -151,14 +141,36 @@ export function buildLaunchSpec(options) {
|
|
|
151
141
|
argv.push("--session-dir", config.sessionDir);
|
|
152
142
|
argv.push("--provider", config.model.provider, "--model", config.model.id);
|
|
153
143
|
argv.push("--thinking", config.thinking.level);
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
144
|
+
const mcpServers = config.mcp.servers?.map((s) => s.replace(/[^A-Za-z0-9_]/g, "_"));
|
|
145
|
+
const mcpExcluded = config.mcp.excluded?.map((s) => s.replace(/[^A-Za-z0-9_]/g, "_"));
|
|
146
|
+
const hasExplicitTools = config.tools.length > 0;
|
|
147
|
+
const baseTools = hasExplicitTools
|
|
148
|
+
? config.tools
|
|
149
|
+
: mcpServers && mcpServers.length > 0
|
|
150
|
+
? [...DEFAULT_PI_TOOLS, CONTACT_PARENT_TOOL_NAME]
|
|
151
|
+
: [];
|
|
152
|
+
const effectiveTools = [
|
|
153
|
+
...(baseTools.length > 0 && config.codemodeOnly && !baseTools.includes("codemode")
|
|
154
|
+
? [...baseTools, "codemode"]
|
|
155
|
+
: baseTools),
|
|
156
|
+
];
|
|
157
|
+
const excludeTools = Array.from(new Set(config.excludeTools));
|
|
158
|
+
if (config.mcp.enabled) {
|
|
159
|
+
if (mcpServers && mcpServers.length > 0) {
|
|
160
|
+
for (const server of mcpServers) {
|
|
161
|
+
effectiveTools.push(`mcp__${server}__*`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (mcpExcluded && mcpExcluded.length > 0) {
|
|
165
|
+
for (const server of mcpExcluded) {
|
|
166
|
+
excludeTools.push(`mcp__${server}__*`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
157
170
|
if (effectiveTools.length > 0)
|
|
158
|
-
argv.push("--tools", effectiveTools.join(","));
|
|
159
|
-
const excludeTools = Array.from(new Set([...config.excludeTools, ...HINDSIGHT_TOOLS]));
|
|
171
|
+
argv.push("--tools", Array.from(new Set(effectiveTools)).join(","));
|
|
160
172
|
if (excludeTools.length > 0)
|
|
161
|
-
argv.push("--exclude-tools", excludeTools.join(","));
|
|
173
|
+
argv.push("--exclude-tools", Array.from(new Set(excludeTools)).join(","));
|
|
162
174
|
if (!config.extensions.discovery)
|
|
163
175
|
argv.push("--no-extensions");
|
|
164
176
|
for (const path of config.extensions.paths)
|
|
@@ -167,6 +179,8 @@ export function buildLaunchSpec(options) {
|
|
|
167
179
|
argv.push("--no-skills");
|
|
168
180
|
for (const path of config.skills.paths)
|
|
169
181
|
argv.push("--skill", path);
|
|
182
|
+
if (!config.mcp.enabled)
|
|
183
|
+
argv.push("--no-mcp");
|
|
170
184
|
argv.push("--append-system-prompt", config.prompt);
|
|
171
185
|
const env = {
|
|
172
186
|
[BRIDGE_ENVIRONMENT_KEYS.parentSessionId]: options.bridge.parentSessionId,
|
|
@@ -179,6 +193,12 @@ export function buildLaunchSpec(options) {
|
|
|
179
193
|
[CHILD_TITLE_ENV_KEY]: config.title ?? "",
|
|
180
194
|
PI_HINDSIGHT_DISABLE: "1",
|
|
181
195
|
...(config.codemodeOnly ? { PI_CODEMODE_MODE: "only" } : {}),
|
|
196
|
+
...(mcpExcluded && mcpExcluded.length > 0
|
|
197
|
+
? { PI_SUBAGENT_MCP_EXCLUDE: mcpExcluded.join(",") }
|
|
198
|
+
: {}),
|
|
199
|
+
...(mcpServers && mcpServers.length > 0
|
|
200
|
+
? { PI_SUBAGENT_MCP_SERVERS: mcpServers.join(",") }
|
|
201
|
+
: {}),
|
|
182
202
|
};
|
|
183
203
|
return Object.freeze({
|
|
184
204
|
command: options.invocation.command,
|
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;
|