@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.
Files changed (61) hide show
  1. package/README.md +35 -20
  2. package/dist/agent-resolver.d.ts +9 -0
  3. package/dist/agent-resolver.js +278 -51
  4. package/dist/builtin-agents.js +50 -37
  5. package/dist/child-bridge.d.ts +0 -8
  6. package/dist/child-bridge.js +60 -43
  7. package/dist/child-control.d.ts +6 -4
  8. package/dist/child-control.js +251 -47
  9. package/dist/child-process.d.ts +2 -0
  10. package/dist/child-process.js +24 -12
  11. package/dist/commands.d.ts +1 -4
  12. package/dist/commands.js +1 -52
  13. package/dist/domain.d.ts +27 -12
  14. package/dist/domain.js +2 -1
  15. package/dist/extension.d.ts +7 -6
  16. package/dist/extension.js +96 -112
  17. package/dist/host-adapter.d.ts +19 -0
  18. package/dist/host-adapter.js +140 -1
  19. package/dist/info.d.ts +9 -0
  20. package/dist/info.js +80 -0
  21. package/dist/launch-spec.d.ts +0 -1
  22. package/dist/launch-spec.js +38 -18
  23. package/dist/lease.d.ts +83 -0
  24. package/dist/lease.js +239 -0
  25. package/dist/mailbox.d.ts +46 -0
  26. package/dist/mailbox.js +368 -0
  27. package/dist/manager.d.ts +20 -20
  28. package/dist/manager.js +421 -398
  29. package/dist/process-lock.d.ts +11 -0
  30. package/dist/process-lock.js +55 -0
  31. package/dist/protocol.d.ts +2 -2
  32. package/dist/protocol.js +1 -0
  33. package/dist/registry.d.ts +1 -1
  34. package/dist/registry.js +99 -82
  35. package/dist/rpc-events.d.ts +1 -1
  36. package/dist/rpc-events.js +2 -1
  37. package/dist/runtime.d.ts +8 -21
  38. package/dist/runtime.js +61 -83
  39. package/dist/session-bootstrap.d.ts +2 -1
  40. package/dist/session-bootstrap.js +22 -14
  41. package/dist/state-db.d.ts +52 -0
  42. package/dist/state-db.js +209 -0
  43. package/dist/state.d.ts +25 -0
  44. package/dist/state.js +166 -37
  45. package/dist/status.d.ts +14 -0
  46. package/dist/status.js +36 -0
  47. package/dist/tool-activation.d.ts +3 -4
  48. package/dist/tool-activation.js +13 -215
  49. package/dist/tool-presentation.d.ts +14 -0
  50. package/dist/tool-presentation.js +193 -0
  51. package/dist/tools.d.ts +6 -1
  52. package/dist/tools.js +134 -180
  53. package/package.json +6 -6
  54. package/dist/bridge-client.d.ts +0 -55
  55. package/dist/bridge-client.js +0 -453
  56. package/dist/bridge-server.d.ts +0 -72
  57. package/dist/bridge-server.js +0 -490
  58. package/dist/session-fork.d.ts +0 -16
  59. package/dist/session-fork.js +0 -67
  60. package/dist/widget.d.ts +0 -12
  61. package/dist/widget.js +0 -149
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Node has no flock(2). A dedicated SQLite file held in BEGIN EXCLUSIVE uses the kernel POSIX
3
+ * lock, which is released when this process dies, including SIGKILL. A missed poll is not death.
4
+ * The parent must not keep the lock: a probe rolls back immediately.
5
+ */
6
+ export interface ProcessLock {
7
+ release(): void;
8
+ }
9
+ export declare function processLockPath(stateFile: string, parentSessionId: string, subagentId: string): string | undefined;
10
+ export declare function holdProcessLock(lockPath: string): ProcessLock;
11
+ export declare function processLockHeld(lockPath: string): boolean;
@@ -0,0 +1,55 @@
1
+ import { chmodSync, existsSync, mkdirSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { DatabaseSync } from "node:sqlite";
4
+ import { isSessionId } from "./domain.js";
5
+ export function processLockPath(stateFile, parentSessionId, subagentId) {
6
+ if (!isSessionId(parentSessionId) || !isSessionId(subagentId))
7
+ return undefined;
8
+ return join(dirname(stateFile), "locks", parentSessionId, `${subagentId}.lock`);
9
+ }
10
+ export function holdProcessLock(lockPath) {
11
+ mkdirSync(dirname(lockPath), { recursive: true, mode: 0o700 });
12
+ const db = new DatabaseSync(lockPath);
13
+ chmodSync(lockPath, 0o600);
14
+ db.exec("PRAGMA busy_timeout = 2000");
15
+ db.exec("BEGIN EXCLUSIVE");
16
+ let released = false;
17
+ return {
18
+ release() {
19
+ if (released)
20
+ return;
21
+ released = true;
22
+ try {
23
+ db.exec("ROLLBACK");
24
+ }
25
+ catch {
26
+ // Closing the connection still drops the kernel lock.
27
+ }
28
+ db.close();
29
+ },
30
+ };
31
+ }
32
+ export function processLockHeld(lockPath) {
33
+ if (!existsSync(lockPath))
34
+ return false;
35
+ const db = new DatabaseSync(lockPath);
36
+ try {
37
+ db.exec("PRAGMA busy_timeout = 0");
38
+ try {
39
+ db.exec("BEGIN IMMEDIATE");
40
+ }
41
+ catch (error) {
42
+ if (isDatabaseLocked(error))
43
+ return true;
44
+ throw error;
45
+ }
46
+ db.exec("ROLLBACK");
47
+ return false;
48
+ }
49
+ finally {
50
+ db.close();
51
+ }
52
+ }
53
+ function isDatabaseLocked(error) {
54
+ return typeof error === "object" && error !== null && "errcode" in error && error.errcode === 5;
55
+ }
@@ -7,7 +7,7 @@ export declare const DEFAULT_MAX_BUFFERED_EVENTS = 256;
7
7
  * input and state operations; the child returns reports and its Task result as requests, so a
8
8
  * report the parent refuses (a second Task result) is refused to the child that sent it.
9
9
  */
10
- export declare const BRIDGE_OPERATIONS: readonly ["prompt", "steer", "follow_up", "abort", "get_state", "get_entries", "shutdown", "contact_parent", "task_result"];
10
+ export declare const BRIDGE_OPERATIONS: readonly ["prompt", "steer", "follow_up", "abort", "get_state", "get_entries", "session_facts", "shutdown", "contact_parent", "task_result"];
11
11
  export type BridgeOperation = (typeof BRIDGE_OPERATIONS)[number];
12
12
  /** Child-branch event: the child left the bound session, quit, or was interrupted. */
13
13
  export declare const CHILD_LIFECYCLE_EVENT: "child_lifecycle";
@@ -41,7 +41,7 @@ export declare const RequestFrameSchema: Type.TObject<{
41
41
  version: Type.TLiteral<1>;
42
42
  type: Type.TLiteral<"request">;
43
43
  id: Type.TString;
44
- operation: Type.TUnion<Type.TLiteral<"contact_parent" | "steer" | "follow_up" | "prompt" | "abort" | "get_state" | "get_entries" | "shutdown" | "task_result">[]>;
44
+ operation: Type.TUnion<Type.TLiteral<"contact_parent" | "steer" | "follow_up" | "prompt" | "abort" | "get_state" | "get_entries" | "session_facts" | "shutdown" | "task_result">[]>;
45
45
  payload: Type.TOptional<Type.TUnknown>;
46
46
  }>;
47
47
  export declare const BridgeErrorSchema: Type.TObject<{
package/dist/protocol.js CHANGED
@@ -16,6 +16,7 @@ export const BRIDGE_OPERATIONS = [
16
16
  "abort",
17
17
  "get_state",
18
18
  "get_entries",
19
+ "session_facts",
19
20
  "shutdown",
20
21
  "contact_parent",
21
22
  "task_result",
@@ -30,7 +30,7 @@ export interface CreateSubagentRegistryOptions {
30
30
  readonly filePath?: string;
31
31
  readonly now?: () => Date;
32
32
  }
33
- export declare function defaultRegistryPath(parentSessionId: string): string;
34
33
  /** Strictly revalidates one record read from disk; partial or unknown shapes never become state. */
35
34
  export declare function parseSubagentRecord(value: unknown, key: string, parentSessionId: string, path: string): SubagentRecord;
35
+ export declare function defaultRegistryPath(_parentSessionId: string): string;
36
36
  export declare function createSubagentRegistry(options: CreateSubagentRegistryOptions): SubagentRegistry;
package/dist/registry.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { join } from "node:path";
2
2
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
3
- import { isRecord, readJsonSettingsRoot, updateJsonSettingsRoot } from "@hheei/pi-ext-core";
4
- import { isLaunchValueSource, isPersistenceState, isPresentation, isSessionId, isSubagentIntent, isSubagentState, isThinkingLevel, normalizeSubagentState, REGISTRY_VERSION, } from "./domain.js";
3
+ import { isRecord } from "@hheei/pi-ext-core";
4
+ import { isLaunchValueSource, isPersistenceState, isPresentation, isSessionId, isSubagentIntent, isSubagentState, isThinkingLevel, normalizeSubagentState, } from "./domain.js";
5
+ import { insertChild, listChildRecords, openStateDb, readChild, replaceChild, withWrite, } from "./state-db.js";
5
6
  export class SubagentRegistryError extends Error {
6
7
  code;
7
8
  constructor(code, message) {
@@ -10,15 +11,6 @@ export class SubagentRegistryError extends Error {
10
11
  this.code = code;
11
12
  }
12
13
  }
13
- export function defaultRegistryPath(parentSessionId) {
14
- return join(getAgentDir(), "pi-subagents", "registry", `${parentSessionId}.json`);
15
- }
16
- const ROOT_FIELDS = {
17
- version: true,
18
- parentSessionId: true,
19
- updatedAt: true,
20
- records: true,
21
- };
22
14
  const RECORD_FIELDS = {
23
15
  subagentId: true,
24
16
  parentSessionId: true,
@@ -57,11 +49,10 @@ const LAUNCH_CONFIG_FIELDS = {
57
49
  skills: true,
58
50
  prompt: true,
59
51
  bridgeExtensionPath: true,
60
- interactive: true,
61
52
  title: true,
62
53
  task: true,
63
- referencedSessionPath: true,
64
54
  codemodeOnly: true,
55
+ mcp: true,
65
56
  };
66
57
  const AGENT_FIELDS = {
67
58
  name: true,
@@ -75,7 +66,12 @@ const INVOCATION_FIELDS = { command: true, args: true };
75
66
  const MODEL_FIELDS = { provider: true, id: true, source: true };
76
67
  const THINKING_FIELDS = { level: true, source: true };
77
68
  const SELECTION_FIELDS = { discovery: true, paths: true };
78
- const RUNTIME_FIELDS = { runtimeIdentity: true, endpoint: true };
69
+ const RUNTIME_FIELDS = {
70
+ runtimeIdentity: true,
71
+ endpoint: true,
72
+ attachment: true,
73
+ };
74
+ const ATTACHMENT_FIELDS = { kind: true, host: true, id: true, pid: true };
79
75
  const USAGE_FIELDS = {
80
76
  inputTokens: true,
81
77
  outputTokens: true,
@@ -219,7 +215,58 @@ function parseRuntime(value, path) {
219
215
  expectKeys(raw, RUNTIME_FIELDS, "runtime", path);
220
216
  const runtimeIdentity = expectString(raw.runtimeIdentity, "runtime.runtimeIdentity", path);
221
217
  const endpoint = expectString(raw.endpoint, "runtime.endpoint", path);
222
- return Object.freeze({ runtimeIdentity, endpoint });
218
+ const attachment = raw.attachment === undefined ? undefined : parseAttachment(raw.attachment, path);
219
+ return Object.freeze({
220
+ runtimeIdentity,
221
+ endpoint,
222
+ ...(attachment === undefined ? {} : { attachment }),
223
+ });
224
+ }
225
+ function parseAttachment(value, path) {
226
+ const raw = expectObject(value, "runtime.attachment", path);
227
+ expectKeys(raw, ATTACHMENT_FIELDS, "runtime.attachment", path);
228
+ if (raw.kind === "panel") {
229
+ if (raw.pid !== undefined)
230
+ throw invalid(path, "runtime.attachment panel must not carry a pid");
231
+ const host = raw.host;
232
+ if (!isPanelHost(host))
233
+ throw invalid(path, "runtime.attachment.host is invalid");
234
+ const id = expectString(raw.id, "runtime.attachment.id", path);
235
+ if (id === "")
236
+ throw invalid(path, "runtime.attachment.id must not be empty");
237
+ return Object.freeze({ kind: "panel", host, id });
238
+ }
239
+ if (raw.kind === "process") {
240
+ if (raw.host !== undefined || raw.id !== undefined) {
241
+ throw invalid(path, "runtime.attachment process must not carry a panel id");
242
+ }
243
+ const pid = raw.pid;
244
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) {
245
+ throw invalid(path, "runtime.attachment.pid must be a positive integer");
246
+ }
247
+ return Object.freeze({ kind: "process", pid });
248
+ }
249
+ throw invalid(path, "runtime.attachment.kind is invalid");
250
+ }
251
+ function isPanelHost(value) {
252
+ return value === "herdr" || value === "cmux";
253
+ }
254
+ function parseMcpSelection(value, path) {
255
+ if (value === undefined)
256
+ return Object.freeze({ enabled: true });
257
+ const raw = expectObject(value, "launchConfig.mcp", path);
258
+ const enabled = expectOptionalBoolean(raw.enabled, "launchConfig.mcp.enabled", path, true);
259
+ const servers = raw.servers === undefined
260
+ ? undefined
261
+ : Object.freeze(expectStringArray(raw.servers, "launchConfig.mcp.servers", path));
262
+ const excluded = raw.excluded === undefined
263
+ ? undefined
264
+ : Object.freeze(expectStringArray(raw.excluded, "launchConfig.mcp.excluded", path));
265
+ return Object.freeze({
266
+ enabled,
267
+ ...(servers === undefined ? {} : { servers }),
268
+ ...(excluded === undefined ? {} : { excluded }),
269
+ });
223
270
  }
224
271
  function parseLaunchConfig(value, path) {
225
272
  const raw = expectObject(value, "launchConfig", path);
@@ -228,7 +275,6 @@ function parseLaunchConfig(value, path) {
228
275
  if (!isSessionId(sessionId))
229
276
  throw invalid(path, "launchConfig.sessionId is not a valid session id");
230
277
  const sessionPath = expectOptionalString(raw.sessionPath, "launchConfig.sessionPath", path);
231
- const referencedSessionPath = expectOptionalString(raw.referencedSessionPath, "launchConfig.referencedSessionPath", path);
232
278
  const extensions = parseSelection(raw.extensions, path, "extensions");
233
279
  const bridgeExtensionPath = expectString(raw.bridgeExtensionPath, "launchConfig.bridgeExtensionPath", path);
234
280
  if (!extensions.paths.includes(bridgeExtensionPath)) {
@@ -245,7 +291,6 @@ function parseLaunchConfig(value, path) {
245
291
  sessionId,
246
292
  sessionDir: expectString(raw.sessionDir, "launchConfig.sessionDir", path),
247
293
  ...(sessionPath === undefined ? {} : { sessionPath }),
248
- ...(referencedSessionPath === undefined ? {} : { referencedSessionPath }),
249
294
  agent: parseAgent(raw.agent, path),
250
295
  model: parseModel(raw.model, path),
251
296
  thinking: parseThinking(raw.thinking, path),
@@ -253,9 +298,9 @@ function parseLaunchConfig(value, path) {
253
298
  excludeTools: Object.freeze(expectStringArray(raw.excludeTools, "launchConfig.excludeTools", path)),
254
299
  extensions,
255
300
  skills: parseSelection(raw.skills, path, "skills"),
301
+ mcp: parseMcpSelection(raw.mcp, path),
256
302
  prompt: expectString(raw.prompt, "launchConfig.prompt", path),
257
303
  bridgeExtensionPath,
258
- interactive: expectOptionalBoolean(raw.interactive, "launchConfig.interactive", path, false),
259
304
  ...(expectOptionalBoolean(raw.codemodeOnly, "launchConfig.codemodeOnly", path, false)
260
305
  ? { codemodeOnly: true }
261
306
  : {}),
@@ -349,36 +394,8 @@ export function parseSubagentRecord(value, key, parentSessionId, path) {
349
394
  ...(unacknowledgedInput === undefined ? {} : { unacknowledgedInput }),
350
395
  });
351
396
  }
352
- function parseRoot(raw, path, parentSessionId) {
353
- if (Object.keys(raw).length === 0)
354
- return new Map();
355
- const version = raw.version;
356
- if (version === undefined)
357
- throw corrupt(path, "missing version");
358
- if (version !== REGISTRY_VERSION) {
359
- throw new SubagentRegistryError("unsupported_version", `Unsupported registry version ${String(version)} in ${path}; expected ${REGISTRY_VERSION}`);
360
- }
361
- if (raw.parentSessionId !== parentSessionId) {
362
- throw new SubagentRegistryError("parent_mismatch", `Registry ${path} belongs to parent session ${String(raw.parentSessionId)}, not ${parentSessionId}`);
363
- }
364
- const records = raw.records;
365
- if (!isRecord(records))
366
- throw corrupt(path, "records must be an object");
367
- const parsed = new Map();
368
- for (const [id, value] of Object.entries(records)) {
369
- parsed.set(id, parseSubagentRecord(value, id, parentSessionId, path));
370
- }
371
- return parsed;
372
- }
373
- function writeRoot(root, parentSessionId, records, now) {
374
- for (const key of Object.keys(root)) {
375
- if (ROOT_FIELDS[key] !== true)
376
- delete root[key];
377
- }
378
- root.version = REGISTRY_VERSION;
379
- root.parentSessionId = parentSessionId;
380
- root.updatedAt = now.toISOString();
381
- root.records = Object.fromEntries(records);
397
+ export function defaultRegistryPath(_parentSessionId) {
398
+ return join(getAgentDir(), "pi-subagents", "state.sqlite");
382
399
  }
383
400
  export function createSubagentRegistry(options) {
384
401
  const { parentSessionId } = options;
@@ -387,41 +404,43 @@ export function createSubagentRegistry(options) {
387
404
  }
388
405
  const path = options.filePath ?? defaultRegistryPath(parentSessionId);
389
406
  const now = options.now ?? (() => new Date());
390
- const readRecords = async (signal) => parseRoot(await readJsonSettingsRoot(path, signal), path, parentSessionId);
391
- const store = {
407
+ const db = openStateDb(path);
408
+ const load = (id) => {
409
+ const row = readChild(db, parentSessionId, id);
410
+ if (row === undefined)
411
+ return undefined;
412
+ return parseSubagentRecord(JSON.parse(row.record), id, parentSessionId, path);
413
+ };
414
+ return {
392
415
  path,
393
416
  parentSessionId,
394
- async get(id, signal) {
395
- return (await readRecords(signal)).get(id);
417
+ async get(id) {
418
+ return load(id);
396
419
  },
397
- async list(signal) {
398
- const records = [...(await readRecords(signal)).values()];
420
+ async list() {
421
+ const records = listChildRecords(db, parentSessionId).map((record) => {
422
+ const parsed = JSON.parse(record);
423
+ if (!isRecord(parsed) || typeof parsed.subagentId !== "string") {
424
+ throw corrupt(path, "child record is missing subagentId");
425
+ }
426
+ return parseSubagentRecord(parsed, parsed.subagentId, parentSessionId, path);
427
+ });
399
428
  records.sort((left, right) => left.createdAt === right.createdAt
400
429
  ? left.subagentId.localeCompare(right.subagentId)
401
430
  : left.createdAt.localeCompare(right.createdAt));
402
431
  return records;
403
432
  },
404
- async create(record, signal) {
405
- let stored;
406
- await updateJsonSettingsRoot(path, (root) => {
407
- const records = parseRoot(root, path, parentSessionId);
408
- const candidate = parseSubagentRecord(record, record.subagentId, parentSessionId, path);
409
- if (records.has(candidate.subagentId)) {
410
- throw new SubagentRegistryError("duplicate_child", `Child ${candidate.subagentId} already exists in ${path}`);
411
- }
412
- records.set(candidate.subagentId, candidate);
413
- writeRoot(root, parentSessionId, records, now());
414
- stored = candidate;
415
- }, signal);
416
- if (stored === undefined)
417
- throw corrupt(path, "create did not persist a record");
418
- return stored;
433
+ async create(record) {
434
+ const candidate = parseSubagentRecord(record, record.subagentId, parentSessionId, path);
435
+ const inserted = withWrite(db, () => insertChild(db, parentSessionId, candidate.subagentId, candidate.revision, JSON.stringify(candidate)));
436
+ if (!inserted) {
437
+ throw new SubagentRegistryError("duplicate_child", `Child ${candidate.subagentId} already exists in ${path}`);
438
+ }
439
+ return candidate;
419
440
  },
420
- async update(id, expectedRevision, updater, expectedRuntimeIdentity, signal) {
421
- let stored;
422
- await updateJsonSettingsRoot(path, (root) => {
423
- const records = parseRoot(root, path, parentSessionId);
424
- const current = records.get(id);
441
+ async update(id, expectedRevision, updater, expectedRuntimeIdentity) {
442
+ return withWrite(db, () => {
443
+ const current = load(id);
425
444
  if (current === undefined) {
426
445
  throw new SubagentRegistryError("unknown_child", `Unknown child ${id} in ${path}`);
427
446
  }
@@ -440,14 +459,12 @@ export function createSubagentRegistry(options) {
440
459
  revision: current.revision + 1,
441
460
  updatedAt: now().toISOString(),
442
461
  }, id, parentSessionId, path);
443
- records.set(id, next);
444
- writeRoot(root, parentSessionId, records, now());
445
- stored = next;
446
- }, signal);
447
- if (stored === undefined)
448
- throw corrupt(path, `update of ${id} did not persist a record`);
449
- return stored;
462
+ const replaced = replaceChild(db, parentSessionId, id, current.revision, next.revision, JSON.stringify(next));
463
+ if (!replaced) {
464
+ throw new SubagentRegistryError("stale_revision", `Child ${id} changed while it was being updated`);
465
+ }
466
+ return next;
467
+ });
450
468
  },
451
469
  };
452
- return store;
453
470
  }
@@ -2,5 +2,5 @@
2
2
  * The Pi events a child forwards to its parent. Everything else — streaming token noise,
3
3
  * undocumented internals — is dropped, so the parent's projection only sees turn-level facts.
4
4
  */
5
- export declare const FORWARDED_PI_EVENT_TYPES: readonly ["agent_start", "agent_end", "turn_start", "turn_end", "tool_execution_start", "tool_execution_update", "tool_execution_end", "auto_retry", "auto_retry_start", "auto_retry_end", "message_update", "agent_settled"];
5
+ export declare const FORWARDED_PI_EVENT_TYPES: readonly ["agent_start", "agent_end", "turn_start", "turn_end", "tool_execution_start", "tool_execution_end", "auto_retry", "auto_retry_start", "auto_retry_end", "message_update", "agent_settled", "compaction_start", "compaction_end"];
6
6
  export declare function shouldForwardPiEvent(event: unknown): boolean;
@@ -9,13 +9,14 @@ export const FORWARDED_PI_EVENT_TYPES = [
9
9
  "turn_start",
10
10
  "turn_end",
11
11
  "tool_execution_start",
12
- "tool_execution_update",
13
12
  "tool_execution_end",
14
13
  "auto_retry",
15
14
  "auto_retry_start",
16
15
  "auto_retry_end",
17
16
  "message_update",
18
17
  "agent_settled",
18
+ "compaction_start",
19
+ "compaction_end",
19
20
  ];
20
21
  export function shouldForwardPiEvent(event) {
21
22
  if (!isRecord(event))
package/dist/runtime.d.ts CHANGED
@@ -19,22 +19,16 @@ export interface RuntimeTokenStoreOptions {
19
19
  * The one socket a parent session listens on. A child dials this path, so a parent that restarts
20
20
  * re-listens in the same place and a surviving child reconnects instead of being rebuilt.
21
21
  */
22
- export declare function parentBridgeEndpoint(parentSessionId: string): string;
22
+ export declare function parentBridgeEndpoint(_parentSessionId: string): string;
23
+ /** Tokens live in the same sqlite file as the child ledger. Rows do not clobber each other. */
24
+ export declare function parentTokenFile(_parentSessionId: string): string;
23
25
  /**
24
- * Every parent session keeps its own token file. One shared file is rewritten wholesale by whichever
25
- * parent saved last, so two parent sessions in the same runtime directory would erase each other's
26
- * credentials and a child that survived a restart could no longer authorize itself.
27
- */
28
- export declare function parentTokenFile(parentSessionId: string): string;
29
- /**
30
- * Tokens outlive the parent process: a child that survived (a panel child, or one whose parent was
31
- * killed) reconnects with the token it was launched with, and a restarted parent must be able to
32
- * authorize it without holding the old process's memory. The store is a 0600 file in the 0700
33
- * runtime directory; the registry never sees a token.
26
+ * Tokens outlive the parent process. A restarted parent reads the same sqlite file and can still
27
+ * match the token a surviving child was launched with.
34
28
  */
35
29
  export declare function createRuntimeTokenStore(options: RuntimeTokenStoreOptions): RuntimeTokenStore;
36
30
  export interface LaunchChildOptions {
37
- readonly registry: Pick<SubagentRegistry, "update">;
31
+ readonly registry: Pick<SubagentRegistry, "get" | "update">;
38
32
  readonly record: SubagentRecord;
39
33
  readonly tokens: RuntimeTokenStore;
40
34
  /** Waits for this child's bridge connection; false when it did not connect in time. */
@@ -53,19 +47,12 @@ export interface OpenChildPanelOptions extends LaunchChildOptions {
53
47
  readonly host: HostAdapter;
54
48
  }
55
49
  /**
56
- * What a launch left behind. A launch that failed can still have started something — a child that
57
- * never dialed in, or a process that ignores SIGTERM — and then the caller must keep the handle:
58
- * dropping it would let the next send start a second runtime for a session the first one may still
59
- * own. `failure` is what the caller reports.
50
+ * What a launch left behind. `failure` is what the caller reports. A handle is kept when the host
51
+ * still has a tab, so stop can close it. It does not by itself forbid another send.
60
52
  */
61
53
  export interface LaunchOutcome<T> {
62
54
  readonly handle?: T;
63
55
  readonly failure?: string;
64
- /**
65
- * Set when something may still own the child's session and this process cannot observe it, so
66
- * the caller refuses to start a second runtime until an explicit stop.
67
- */
68
- readonly unconfirmed?: boolean;
69
56
  }
70
57
  /**
71
58
  * Opens one child in a host panel and waits for it to connect. The host owns this process, so the