@hheei/pi-subagents 0.6.0 → 0.8.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 (60) hide show
  1. package/LICENSES/Apache-2.0.txt +202 -0
  2. package/NOTICE +10 -0
  3. package/README.md +14 -12
  4. package/dist/child-control.d.ts +0 -8
  5. package/dist/child-control.js +66 -3
  6. package/dist/commands.js +1 -1
  7. package/dist/controller-ipc.d.ts +38 -0
  8. package/dist/controller-ipc.js +292 -0
  9. package/dist/controller-main.d.ts +1 -0
  10. package/dist/controller-main.js +26 -0
  11. package/dist/controller-role.d.ts +40 -0
  12. package/dist/controller-role.js +37 -0
  13. package/dist/controller-socket.d.ts +6 -0
  14. package/dist/controller-socket.js +43 -0
  15. package/dist/controller.d.ts +25 -0
  16. package/dist/controller.js +217 -0
  17. package/dist/delegation.d.ts +73 -0
  18. package/dist/delegation.js +71 -0
  19. package/dist/domain.d.ts +20 -18
  20. package/dist/domain.js +6 -1
  21. package/dist/extension.js +43 -26
  22. package/dist/host-adapter.js +1 -1
  23. package/dist/launch-spec.d.ts +1 -3
  24. package/dist/launch-spec.js +39 -15
  25. package/dist/lease.d.ts +0 -1
  26. package/dist/lease.js +8 -11
  27. package/dist/mailbox.js +110 -34
  28. package/dist/manager.d.ts +52 -6
  29. package/dist/manager.js +585 -48
  30. package/dist/mcp/app.html +948 -0
  31. package/dist/mcp/delegate.d.ts +30 -0
  32. package/dist/mcp/delegate.js +44 -0
  33. package/dist/mcp/views.d.ts +5 -0
  34. package/dist/mcp/views.js +12 -0
  35. package/dist/mcp-server.d.ts +6 -0
  36. package/dist/mcp-server.js +287 -0
  37. package/dist/mcp-stdio.d.ts +2 -0
  38. package/dist/mcp-stdio.js +111 -0
  39. package/dist/process-lock.d.ts +1 -1
  40. package/dist/process-lock.js +4 -2
  41. package/dist/protocol.d.ts +5 -5
  42. package/dist/protocol.js +3 -4
  43. package/dist/receipts.d.ts +80 -0
  44. package/dist/receipts.js +326 -0
  45. package/dist/runtime.d.ts +5 -26
  46. package/dist/runtime.js +15 -57
  47. package/dist/session-bootstrap.js +2 -6
  48. package/dist/state-db.d.ts +11 -4
  49. package/dist/state-db.js +102 -24
  50. package/dist/state.d.ts +1 -0
  51. package/dist/state.js +29 -3
  52. package/dist/tool-activation.d.ts +2 -2
  53. package/dist/tool-activation.js +5 -5
  54. package/dist/tool-presentation.d.ts +2 -6
  55. package/dist/tool-presentation.js +75 -122
  56. package/dist/tools.d.ts +0 -3
  57. package/dist/tools.js +104 -132
  58. package/package.json +12 -5
  59. package/dist/json-lines.d.ts +0 -14
  60. package/dist/json-lines.js +0 -95
@@ -0,0 +1,80 @@
1
+ export type ReceiptStatus = "reserved" | "dispatching" | "accepted" | "running" | "completed" | "blocked" | "failed" | "uncertain";
2
+ export type ReceiptDelivery = "pending" | "detached";
3
+ export interface Receipt {
4
+ readonly parentSessionId: string;
5
+ readonly delegationId: string;
6
+ readonly childId?: string;
7
+ readonly revision?: number;
8
+ readonly task: string;
9
+ readonly inputCheckpoint?: string;
10
+ readonly runBoundary?: number;
11
+ /** The compaction this delegation controls; set instead of a run boundary. */
12
+ readonly controlId?: string;
13
+ readonly status: ReceiptStatus;
14
+ readonly delivery: ReceiptDelivery;
15
+ readonly resultText?: string;
16
+ /** Recorded only when a dispatch itself failed; a turn failure leaves both unset. */
17
+ readonly safeToRetry?: boolean;
18
+ readonly sideEffects?: readonly string[];
19
+ readonly createdAt: number;
20
+ readonly updatedAt: number;
21
+ }
22
+ export interface ReceiptSettlement {
23
+ readonly childId: string;
24
+ readonly status: "completed" | "blocked" | "failed";
25
+ readonly message: string;
26
+ /** The turn that can settle this event; absent for a control operation. */
27
+ readonly workVersion?: number;
28
+ /** Set for a control operation's own settlement; such a receipt has no run boundary. */
29
+ readonly controlId?: string;
30
+ }
31
+ export declare function createDelegationId(): string;
32
+ export declare function terminalReceipt(status: ReceiptStatus): boolean;
33
+ export declare function reserveReceipt(filePath: string, input: {
34
+ readonly parentSessionId: string;
35
+ readonly task: string;
36
+ readonly childId?: string;
37
+ readonly delivery: ReceiptDelivery;
38
+ }): Receipt;
39
+ export declare function updateReceipt(filePath: string, parentSessionId: string, delegationId: string, patch: {
40
+ readonly from: readonly ReceiptStatus[];
41
+ readonly status: ReceiptStatus;
42
+ readonly childId?: string;
43
+ readonly inputCheckpoint?: string;
44
+ readonly runBoundary?: number;
45
+ readonly controlId?: string;
46
+ readonly resultText?: string;
47
+ readonly safeToRetry?: boolean;
48
+ readonly sideEffects?: readonly string[];
49
+ }): Receipt | undefined;
50
+ /** A dispatch that died before acceptance cannot be retried safely. */
51
+ export declare function sweepUncertainDispatch(filePath: string, parentSessionId: string): number;
52
+ export declare function settleReceipts(filePath: string, parentSessionId: string, event: ReceiptSettlement): readonly Receipt[];
53
+ /**
54
+ * A control operation that outlived the process that started it can never be confirmed: the child
55
+ * may have compacted, may still be compacting, or may never have started. It is recorded as
56
+ * uncertain instead of being replayed, because replaying compaction is not idempotent.
57
+ */
58
+ export declare function sweepControlReceipts(filePath: string, parentSessionId: string, message: string): readonly Receipt[];
59
+ export declare function listReceipts(filePath: string, parentSessionId: string, cursor?: number, limit?: number): {
60
+ readonly receipts: readonly Receipt[];
61
+ readonly nextCursor?: number;
62
+ };
63
+ /** Stop or shutdown: every open delegation for this child fails, so a waiter cannot hang. */
64
+ export declare function abandonReceipts(filePath: string, parentSessionId: string, childId: string, message: string): readonly Receipt[];
65
+ export declare function maxRunBoundary(filePath: string, parentSessionId: string, childId: string): number;
66
+ export declare function readStoredReceipt(filePath: string, parentSessionId: string, delegationId: string): Receipt | undefined;
67
+ export declare function readReceiptRevision(filePath: string, parentSessionId: string, childId: string, revision: number): Receipt | undefined;
68
+ export declare function latestReceiptForChild(filePath: string, parentSessionId: string, childId: string): Receipt | undefined;
69
+ /** The latest delegation of every agent, without reading any result body. */
70
+ export interface ReceiptSummary {
71
+ readonly childId: string;
72
+ readonly delegationId: string;
73
+ readonly revision: number;
74
+ readonly status: ReceiptStatus;
75
+ readonly hasOutput: boolean;
76
+ }
77
+ /** A failed or uncertain receipt stores a reason, not an answer. */
78
+ export declare function receiptCarriesOutput(status: ReceiptStatus, resultText: string | undefined): boolean;
79
+ export declare function latestReceiptSummaries(filePath: string, parentSessionId: string): ReadonlyMap<string, ReceiptSummary>;
80
+ export declare function waitForReceipt(filePath: string, parentSessionId: string, delegationId: string, signal?: AbortSignal): Promise<Receipt>;
@@ -0,0 +1,326 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { openStateDb, withWrite } from "./state-db.js";
3
+ const TERMINAL = new Set([
4
+ "completed",
5
+ "blocked",
6
+ "failed",
7
+ "uncertain",
8
+ ]);
9
+ const waiters = new Map();
10
+ export function createDelegationId() {
11
+ return `dep_${randomBytes(4).toString("hex")}`;
12
+ }
13
+ export function terminalReceipt(status) {
14
+ return TERMINAL.has(status);
15
+ }
16
+ export function reserveReceipt(filePath, input) {
17
+ const db = openStateDb(filePath);
18
+ const now = Date.now();
19
+ const delegationId = createDelegationId();
20
+ return withWrite(db, () => {
21
+ const revision = input.childId === undefined ? null : nextRevision(db, input.parentSessionId, input.childId);
22
+ db.prepare(`INSERT INTO delegations (
23
+ parent_session_id, delegation_id, child_id, delegation_revision, task, status, delivery, created_at, updated_at
24
+ ) VALUES (?, ?, ?, ?, ?, 'reserved', ?, ?, ?)`).run(input.parentSessionId, delegationId, input.childId ?? null, revision, input.task, input.delivery, now, now);
25
+ const stored = readReceipt(db, input.parentSessionId, delegationId);
26
+ if (stored === undefined)
27
+ throw new Error("Reserved delegation was not stored");
28
+ return stored;
29
+ });
30
+ }
31
+ export function updateReceipt(filePath, parentSessionId, delegationId, patch) {
32
+ const db = openStateDb(filePath);
33
+ const updated = withWrite(db, () => {
34
+ const current = readReceipt(db, parentSessionId, delegationId);
35
+ if (current === undefined || !patch.from.includes(current.status))
36
+ return undefined;
37
+ const revision = current.revision ??
38
+ (patch.childId === undefined ? null : nextRevision(db, parentSessionId, patch.childId));
39
+ db.prepare(`UPDATE delegations
40
+ SET status = ?, child_id = COALESCE(?, child_id), input_checkpoint = COALESCE(?, input_checkpoint),
41
+ delegation_revision = COALESCE(?, delegation_revision), run_boundary = COALESCE(?, run_boundary),
42
+ control_id = COALESCE(?, control_id), result_text = COALESCE(?, result_text),
43
+ safe_to_retry = COALESCE(?, safe_to_retry), side_effects = COALESCE(?, side_effects), updated_at = ?
44
+ WHERE parent_session_id = ? AND delegation_id = ? AND status = ?`).run(patch.status, patch.childId ?? null, patch.inputCheckpoint ?? null, revision, patch.runBoundary ?? null, patch.controlId ?? null, patch.resultText ?? null, patch.safeToRetry === undefined ? null : patch.safeToRetry ? 1 : 0, patch.sideEffects === undefined ? null : JSON.stringify(patch.sideEffects), Date.now(), parentSessionId, delegationId, current.status);
45
+ return readReceipt(db, parentSessionId, delegationId);
46
+ });
47
+ notify(parentSessionId, delegationId);
48
+ return updated;
49
+ }
50
+ /** A dispatch that died before acceptance cannot be retried safely. */
51
+ export function sweepUncertainDispatch(filePath, parentSessionId) {
52
+ const db = openStateDb(filePath);
53
+ const changed = withWrite(db, () => {
54
+ const rows = db
55
+ .prepare(`SELECT delegation_id FROM delegations
56
+ WHERE parent_session_id = ? AND status = 'dispatching'`)
57
+ .all(parentSessionId);
58
+ for (const row of rows) {
59
+ db.prepare(`UPDATE delegations SET status = 'uncertain', result_text = ?, updated_at = ?
60
+ WHERE parent_session_id = ? AND delegation_id = ? AND status = 'dispatching'`).run("Dispatch was not acknowledged before the controller stopped", Date.now(), parentSessionId, row.delegation_id);
61
+ notify(parentSessionId, row.delegation_id);
62
+ }
63
+ return rows.length;
64
+ });
65
+ return changed;
66
+ }
67
+ export function settleReceipts(filePath, parentSessionId, event) {
68
+ const db = openStateDb(filePath);
69
+ const settled = withWrite(db, () => {
70
+ // A control operation is bound to its own id: a late compaction report cannot settle a turn
71
+ // that started after it, and a turn's answer cannot be reported as a compaction.
72
+ const rows = (event.controlId === undefined
73
+ ? db
74
+ .prepare(`SELECT delegation_id FROM delegations
75
+ WHERE parent_session_id = ? AND child_id = ? AND run_boundary = ?
76
+ AND control_id IS NULL AND status IN ('accepted', 'running', 'dispatching')`)
77
+ .all(parentSessionId, event.childId, event.workVersion ?? 0)
78
+ : db
79
+ .prepare(`SELECT delegation_id FROM delegations
80
+ WHERE parent_session_id = ? AND child_id = ? AND control_id = ?
81
+ AND status IN ('accepted', 'running', 'dispatching')`)
82
+ .all(parentSessionId, event.childId, event.controlId));
83
+ const now = Date.now();
84
+ const stored = [];
85
+ for (const row of rows) {
86
+ db.prepare(`UPDATE delegations
87
+ SET status = ?, result_text = ?, run_boundary = COALESCE(?, run_boundary), updated_at = ?
88
+ WHERE parent_session_id = ? AND delegation_id = ? AND status IN ('accepted', 'running', 'dispatching')`).run(event.status, event.message, event.controlId === undefined ? (event.workVersion ?? 0) : null, now, parentSessionId, row.delegation_id);
89
+ const receipt = readReceipt(db, parentSessionId, row.delegation_id);
90
+ if (receipt !== undefined)
91
+ stored.push(receipt);
92
+ }
93
+ return stored;
94
+ });
95
+ for (const receipt of settled)
96
+ notify(receipt.parentSessionId, receipt.delegationId);
97
+ return settled;
98
+ }
99
+ /**
100
+ * A control operation that outlived the process that started it can never be confirmed: the child
101
+ * may have compacted, may still be compacting, or may never have started. It is recorded as
102
+ * uncertain instead of being replayed, because replaying compaction is not idempotent.
103
+ */
104
+ export function sweepControlReceipts(filePath, parentSessionId, message) {
105
+ const db = openStateDb(filePath);
106
+ const swept = withWrite(db, () => {
107
+ const rows = db
108
+ .prepare(`SELECT delegation_id FROM delegations
109
+ WHERE parent_session_id = ? AND control_id IS NOT NULL
110
+ AND status IN ('accepted', 'running')`)
111
+ .all(parentSessionId);
112
+ const now = Date.now();
113
+ const stored = [];
114
+ for (const row of rows) {
115
+ db.prepare(`UPDATE delegations SET status = 'uncertain', result_text = ?, updated_at = ?
116
+ WHERE parent_session_id = ? AND delegation_id = ? AND status IN ('accepted', 'running')`).run(message, now, parentSessionId, row.delegation_id);
117
+ const receipt = readReceipt(db, parentSessionId, row.delegation_id);
118
+ if (receipt !== undefined)
119
+ stored.push(receipt);
120
+ }
121
+ return stored;
122
+ });
123
+ for (const receipt of swept)
124
+ notify(receipt.parentSessionId, receipt.delegationId);
125
+ return swept;
126
+ }
127
+ export function listReceipts(filePath, parentSessionId, cursor = 0, limit = 20) {
128
+ const db = openStateDb(filePath);
129
+ const safeLimit = Number.isInteger(limit) && limit > 0 ? Math.min(limit, 50) : 20;
130
+ const safeCursor = Number.isInteger(cursor) && cursor > 0 ? cursor : 0;
131
+ const rows = db
132
+ .prepare(`SELECT delegation_id FROM delegations
133
+ WHERE parent_session_id = ? ORDER BY updated_at DESC LIMIT ? OFFSET ?`)
134
+ .all(parentSessionId, safeLimit + 1, safeCursor);
135
+ const receipts = rows.slice(0, safeLimit).flatMap((row) => {
136
+ const receipt = readReceipt(db, parentSessionId, row.delegation_id);
137
+ return receipt === undefined ? [] : [receipt];
138
+ });
139
+ return rows.length > safeLimit ? { receipts, nextCursor: safeCursor + safeLimit } : { receipts };
140
+ }
141
+ /** Stop or shutdown: every open delegation for this child fails, so a waiter cannot hang. */
142
+ export function abandonReceipts(filePath, parentSessionId, childId, message) {
143
+ const db = openStateDb(filePath);
144
+ const abandoned = withWrite(db, () => {
145
+ const rows = db
146
+ .prepare(`SELECT delegation_id FROM delegations
147
+ WHERE parent_session_id = ? AND child_id = ?
148
+ AND status IN ('reserved', 'dispatching', 'accepted', 'running')`)
149
+ .all(parentSessionId, childId);
150
+ const now = Date.now();
151
+ const stored = [];
152
+ for (const row of rows) {
153
+ db.prepare(`UPDATE delegations SET status = 'failed', result_text = ?, updated_at = ?
154
+ WHERE parent_session_id = ? AND delegation_id = ?
155
+ AND status IN ('reserved', 'dispatching', 'accepted', 'running')`).run(message, now, parentSessionId, row.delegation_id);
156
+ const receipt = readReceipt(db, parentSessionId, row.delegation_id);
157
+ if (receipt !== undefined)
158
+ stored.push(receipt);
159
+ }
160
+ return stored;
161
+ });
162
+ for (const receipt of abandoned)
163
+ notify(receipt.parentSessionId, receipt.delegationId);
164
+ return abandoned;
165
+ }
166
+ export function maxRunBoundary(filePath, parentSessionId, childId) {
167
+ const db = openStateDb(filePath);
168
+ const row = db
169
+ .prepare(`SELECT MAX(run_boundary) AS boundary FROM delegations
170
+ WHERE parent_session_id = ? AND child_id = ?`)
171
+ .get(parentSessionId, childId);
172
+ return row?.boundary ?? 0;
173
+ }
174
+ export function readStoredReceipt(filePath, parentSessionId, delegationId) {
175
+ return readReceipt(openStateDb(filePath), parentSessionId, delegationId);
176
+ }
177
+ export function readReceiptRevision(filePath, parentSessionId, childId, revision) {
178
+ const db = openStateDb(filePath);
179
+ const row = db
180
+ .prepare("SELECT delegation_id FROM delegations WHERE parent_session_id = ? AND child_id = ? AND delegation_revision = ?")
181
+ .get(parentSessionId, childId, revision);
182
+ return row === undefined ? undefined : readReceipt(db, parentSessionId, row.delegation_id);
183
+ }
184
+ export function latestReceiptForChild(filePath, parentSessionId, childId) {
185
+ const db = openStateDb(filePath);
186
+ const row = db
187
+ .prepare(`SELECT delegation_id FROM delegations
188
+ WHERE parent_session_id = ? AND child_id = ?
189
+ ORDER BY rowid DESC LIMIT 1`)
190
+ .get(parentSessionId, childId);
191
+ if (row === undefined)
192
+ return undefined;
193
+ return readReceipt(db, parentSessionId, row.delegation_id);
194
+ }
195
+ /** A failed or uncertain receipt stores a reason, not an answer. */
196
+ export function receiptCarriesOutput(status, resultText) {
197
+ return resultText !== undefined && status !== "failed" && status !== "uncertain";
198
+ }
199
+ export function latestReceiptSummaries(filePath, parentSessionId) {
200
+ const db = openStateDb(filePath);
201
+ const rows = db
202
+ .prepare(`SELECT child_id, delegation_id, delegation_revision, status,
203
+ result_text IS NOT NULL AND status NOT IN ('failed', 'uncertain') AS has_output
204
+ FROM delegations
205
+ WHERE parent_session_id = ? AND child_id IS NOT NULL
206
+ AND rowid IN (
207
+ SELECT MAX(rowid) FROM delegations
208
+ WHERE parent_session_id = ? AND child_id IS NOT NULL GROUP BY child_id
209
+ )`)
210
+ .all(parentSessionId, parentSessionId);
211
+ const summaries = new Map();
212
+ for (const row of rows) {
213
+ if (!isStatus(row.status) || row.delegation_revision === null)
214
+ continue;
215
+ summaries.set(row.child_id, {
216
+ childId: row.child_id,
217
+ delegationId: row.delegation_id,
218
+ revision: row.delegation_revision,
219
+ status: row.status,
220
+ hasOutput: row.has_output === 1,
221
+ });
222
+ }
223
+ return summaries;
224
+ }
225
+ export function waitForReceipt(filePath, parentSessionId, delegationId, signal) {
226
+ const current = readStoredReceipt(filePath, parentSessionId, delegationId);
227
+ if (current !== undefined && terminalReceipt(current.status))
228
+ return Promise.resolve(current);
229
+ return new Promise((resolve, reject) => {
230
+ const key = waiterKey(parentSessionId, delegationId);
231
+ const wake = () => {
232
+ const receipt = readStoredReceipt(filePath, parentSessionId, delegationId);
233
+ if (receipt === undefined || !terminalReceipt(receipt.status))
234
+ return;
235
+ finish();
236
+ resolve(receipt);
237
+ };
238
+ const fail = () => {
239
+ finish();
240
+ reject(signal?.reason instanceof Error ? signal.reason : new Error("Delegation wait aborted"));
241
+ };
242
+ const finish = () => {
243
+ signal?.removeEventListener("abort", fail);
244
+ const set = waiters.get(key);
245
+ set?.delete(wake);
246
+ if (set !== undefined && set.size === 0)
247
+ waiters.delete(key);
248
+ };
249
+ const set = waiters.get(key) ?? new Set();
250
+ set.add(wake);
251
+ waiters.set(key, set);
252
+ if (signal !== undefined) {
253
+ if (signal.aborted) {
254
+ fail();
255
+ return;
256
+ }
257
+ signal.addEventListener("abort", fail, { once: true });
258
+ }
259
+ wake();
260
+ });
261
+ }
262
+ function parseSideEffects(raw) {
263
+ try {
264
+ const parsed = JSON.parse(raw);
265
+ return Array.isArray(parsed)
266
+ ? parsed.filter((item) => typeof item === "string")
267
+ : [];
268
+ }
269
+ catch {
270
+ return [];
271
+ }
272
+ }
273
+ function nextRevision(db, parentSessionId, childId) {
274
+ const row = db
275
+ .prepare("SELECT MAX(delegation_revision) AS revision FROM delegations WHERE parent_session_id = ? AND child_id = ?")
276
+ .get(parentSessionId, childId);
277
+ return (row.revision ?? 0) + 1;
278
+ }
279
+ function readReceipt(db, parentSessionId, delegationId) {
280
+ const row = db
281
+ .prepare(`SELECT delegation_id, child_id, delegation_revision, task, input_checkpoint, run_boundary, control_id, status, delivery, result_text, safe_to_retry, side_effects, created_at, updated_at
282
+ FROM delegations WHERE parent_session_id = ? AND delegation_id = ?`)
283
+ .get(parentSessionId, delegationId);
284
+ if (row === undefined ||
285
+ !isStatus(row.status) ||
286
+ (row.delivery !== "pending" && row.delivery !== "detached")) {
287
+ return undefined;
288
+ }
289
+ return {
290
+ parentSessionId,
291
+ delegationId: row.delegation_id,
292
+ task: row.task,
293
+ status: row.status,
294
+ delivery: row.delivery,
295
+ createdAt: row.created_at,
296
+ updatedAt: row.updated_at,
297
+ ...(row.child_id === null ? {} : { childId: row.child_id }),
298
+ ...(row.delegation_revision === null ? {} : { revision: row.delegation_revision }),
299
+ ...(row.input_checkpoint === null ? {} : { inputCheckpoint: row.input_checkpoint }),
300
+ ...(row.run_boundary === null ? {} : { runBoundary: row.run_boundary }),
301
+ ...(row.control_id === null ? {} : { controlId: row.control_id }),
302
+ ...(row.safe_to_retry === null ? {} : { safeToRetry: row.safe_to_retry === 1 }),
303
+ ...(row.side_effects === null ? {} : { sideEffects: parseSideEffects(row.side_effects) }),
304
+ ...(row.result_text === null ? {} : { resultText: row.result_text }),
305
+ };
306
+ }
307
+ function isStatus(value) {
308
+ return (value === "reserved" ||
309
+ value === "dispatching" ||
310
+ value === "accepted" ||
311
+ value === "running" ||
312
+ value === "completed" ||
313
+ value === "blocked" ||
314
+ value === "failed" ||
315
+ value === "uncertain");
316
+ }
317
+ function waiterKey(parentSessionId, delegationId) {
318
+ return `${parentSessionId}\n${delegationId}`;
319
+ }
320
+ function notify(parentSessionId, delegationId) {
321
+ const set = waiters.get(waiterKey(parentSessionId, delegationId));
322
+ if (set === undefined)
323
+ return;
324
+ for (const wake of set)
325
+ wake();
326
+ }
package/dist/runtime.d.ts CHANGED
@@ -2,35 +2,14 @@ import { type ChildRuntime } from "./child-process.js";
2
2
  import type { SubagentRecord } from "./domain.js";
3
3
  import type { HostAdapter, HostAttachment } from "./host-adapter.js";
4
4
  import type { SubagentRegistry } from "./registry.js";
5
- /** Per-runtime bridge tokens, kept out of the registry because they are credentials. */
6
- export interface RuntimeTokenStore {
7
- remember(runtimeIdentity: string, token: string): void;
8
- get(runtimeIdentity: string): string | undefined;
9
- forget(runtimeIdentity: string): void;
10
- }
11
- export interface RuntimeTokenStoreOptions {
12
- /** The parent session these tokens belong to. */
13
- readonly parentSessionId: string;
14
- /** Override for tests. Defaults to this parent's file in the shared runtime directory. */
15
- readonly file?: string;
16
- readonly diagnose?: (message: string) => void;
17
- }
18
5
  /**
19
- * The one socket a parent session listens on. A child dials this path, so a parent that restarts
20
- * re-listens in the same place and a surviving child reconnects instead of being rebuilt.
6
+ * The one mailbox a parent session reads. A child polls this path, so a parent that restarts reads
7
+ * the same file and a surviving child reconnects instead of being rebuilt.
21
8
  */
22
9
  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;
25
- /**
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.
28
- */
29
- export declare function createRuntimeTokenStore(options: RuntimeTokenStoreOptions): RuntimeTokenStore;
30
10
  export interface LaunchChildOptions {
31
11
  readonly registry: Pick<SubagentRegistry, "get" | "update">;
32
12
  readonly record: SubagentRecord;
33
- readonly tokens: RuntimeTokenStore;
34
13
  /** Waits for this child's bridge connection; false when it did not connect in time. */
35
14
  readonly waitForBridge: (childId: string, signal?: AbortSignal) => Promise<boolean>;
36
15
  readonly signal?: AbortSignal;
@@ -38,9 +17,9 @@ export interface LaunchChildOptions {
38
17
  }
39
18
  /**
40
19
  * Starts one headless child Pi and waits for it to connect. Nothing is driven over its stdio: the
41
- * process gets the parent's socket path and its own runtime token in the environment, and the
42
- * bridge is the only control channel. A child that never connects is terminated here rather than
43
- * left running with no way to reach it.
20
+ * process gets the parent's mailbox path in its environment, and the bridge is the only control
21
+ * channel. A child that never connects is terminated here rather than left running with no way to
22
+ * reach it.
44
23
  */
45
24
  export declare function launchChild(options: LaunchChildOptions): Promise<LaunchOutcome<ChildRuntime>>;
46
25
  export interface OpenChildPanelOptions extends LaunchChildOptions {
package/dist/runtime.js CHANGED
@@ -6,72 +6,33 @@
6
6
  * so "is this child alive?" is answered by a bridge connection, and the runtime metadata in the
7
7
  * registry only records which runtime and endpoint a child was last launched with.
8
8
  */
9
- import { randomBytes, randomUUID } from "node:crypto";
9
+ import { randomUUID } from "node:crypto";
10
10
  import { chmod, mkdir } from "node:fs/promises";
11
11
  import { tmpdir } from "node:os";
12
12
  import { join } from "node:path";
13
13
  import { errorMessage } from "@hheei/pi-ext-core";
14
14
  import { spawnChildRuntime } from "./child-process.js";
15
- import { buildLaunchSpec, withBridgeToken } from "./launch-spec.js";
15
+ import { buildLaunchSpec } from "./launch-spec.js";
16
16
  import { commitLease } from "./lease.js";
17
- import { defaultStatePath, deleteToken, openStateDb, readToken, writeToken } from "./state-db.js";
17
+ import { defaultStatePath } from "./state-db.js";
18
18
  function runtimeDirectory() {
19
19
  const user = typeof process.getuid === "function" ? process.getuid() : "user";
20
20
  return join(tmpdir(), `pi-subagents-${user}`);
21
21
  }
22
22
  /**
23
- * The one socket a parent session listens on. A child dials this path, so a parent that restarts
24
- * re-listens in the same place and a surviving child reconnects instead of being rebuilt.
23
+ * The one mailbox a parent session reads. A child polls this path, so a parent that restarts reads
24
+ * the same file and a surviving child reconnects instead of being rebuilt.
25
25
  */
26
26
  export function parentBridgeEndpoint(_parentSessionId) {
27
27
  return defaultStatePath();
28
28
  }
29
- /** Tokens live in the same sqlite file as the child ledger. Rows do not clobber each other. */
30
- export function parentTokenFile(_parentSessionId) {
31
- return defaultStatePath();
32
- }
33
- /**
34
- * Tokens outlive the parent process. A restarted parent reads the same sqlite file and can still
35
- * match the token a surviving child was launched with.
36
- */
37
- export function createRuntimeTokenStore(options) {
38
- const file = options.file ?? defaultStatePath();
39
- const diagnose = options.diagnose ?? (() => { });
40
- let db;
41
- try {
42
- db = openStateDb(file);
43
- }
44
- catch (error) {
45
- diagnose(`could not open the state database: ${errorMessage(error)}`);
46
- }
47
- const database = db;
48
- return {
49
- remember(runtimeIdentity, token) {
50
- if (database === undefined)
51
- return;
52
- writeToken(database, options.parentSessionId, runtimeIdentity, token);
53
- },
54
- get(runtimeIdentity) {
55
- if (database === undefined)
56
- return undefined;
57
- return readToken(database, options.parentSessionId, runtimeIdentity);
58
- },
59
- forget(runtimeIdentity) {
60
- if (database === undefined)
61
- return;
62
- deleteToken(database, options.parentSessionId, runtimeIdentity);
63
- },
64
- };
65
- }
66
29
  /**
67
- * Mints the runtime identity and its bridge token, remembers the token so the child's very first
68
- * hello can be authorized, and builds the one LaunchSpec for this presentation. The token travels
69
- * in the spec's environment, because who starts the child differs: the parent starts a background
70
- * child and the host starts a panel one.
30
+ * Mints the runtime identity and builds the one LaunchSpec for this presentation. Who starts the
31
+ * child differs (this parent for a background child, the host for a panel one), but the spec does
32
+ * not.
71
33
  */
72
- function planChildRuntime(record, tokens, presentation) {
34
+ function planChildRuntime(record, presentation) {
73
35
  const runtimeIdentity = randomUUID();
74
- const token = randomBytes(32).toString("hex");
75
36
  const endpoint = parentBridgeEndpoint(record.parentSessionId);
76
37
  const spec = buildLaunchSpec({
77
38
  config: record.launchConfig,
@@ -85,8 +46,7 @@ function planChildRuntime(record, tokens, presentation) {
85
46
  endpoint,
86
47
  },
87
48
  });
88
- tokens.remember(runtimeIdentity, token);
89
- return { runtimeIdentity, endpoint, spec: { ...spec, env: withBridgeToken(spec.env, token) } };
49
+ return { runtimeIdentity, endpoint, spec };
90
50
  }
91
51
  /** Evidence first: the record names the runtime that was asked for even if starting it fails. */
92
52
  async function recordRuntimeEvidence(registry, record, plan) {
@@ -98,13 +58,13 @@ async function recordRuntimeEvidence(registry, record, plan) {
98
58
  }
99
59
  /**
100
60
  * Starts one headless child Pi and waits for it to connect. Nothing is driven over its stdio: the
101
- * process gets the parent's socket path and its own runtime token in the environment, and the
102
- * bridge is the only control channel. A child that never connects is terminated here rather than
103
- * left running with no way to reach it.
61
+ * process gets the parent's mailbox path in its environment, and the bridge is the only control
62
+ * channel. A child that never connects is terminated here rather than left running with no way to
63
+ * reach it.
104
64
  */
105
65
  export async function launchChild(options) {
106
66
  const record = options.record;
107
- const plan = planChildRuntime(record, options.tokens, "background");
67
+ const plan = planChildRuntime(record, "background");
108
68
  await recordRuntimeEvidence(options.registry, record, plan);
109
69
  const runtime = spawnChildRuntime({
110
70
  command: plan.spec.command,
@@ -133,13 +93,12 @@ export async function launchChild(options) {
133
93
  failure: `Child ${record.subagentId} never connected to the parent bridge and did not stop (it may still be running)`,
134
94
  };
135
95
  }
136
- options.tokens.forget(plan.runtimeIdentity);
137
96
  await commitLease(options.registry, record.subagentId, { type: "runtime-gone", outcome: "clear" }, undefined, plan.runtimeIdentity);
138
97
  return { failure: `Child ${record.subagentId} never connected to the parent bridge` };
139
98
  }
140
99
  export async function openChildPanel(options) {
141
100
  const record = options.record;
142
- const plan = planChildRuntime(record, options.tokens, "panel");
101
+ const plan = planChildRuntime(record, "panel");
143
102
  await recordRuntimeEvidence(options.registry, record, plan);
144
103
  let attachment;
145
104
  try {
@@ -200,7 +159,6 @@ export async function openChildPanel(options) {
200
159
  failure: `Child ${record.subagentId} never connected to the parent bridge and its panel could not be closed (${cleanup.stderr || "its state could not be observed"}); close it manually`,
201
160
  };
202
161
  }
203
- options.tokens.forget(plan.runtimeIdentity);
204
162
  await commitLease(options.registry, record.subagentId, { type: "runtime-gone", outcome: "clear" }, undefined, plan.runtimeIdentity);
205
163
  const detail = earlyExitObserved?.detail ? ` (${earlyExitObserved.detail.trim()})` : "";
206
164
  return { failure: `Child ${record.subagentId} never connected to the parent bridge${detail}` };
@@ -165,15 +165,12 @@ export function createSubagentId(existingIds) {
165
165
  export async function resolveSubagentLaunch(options) {
166
166
  const task = options.input.task.trim();
167
167
  if (task === "")
168
- throw new Error("agent_spawn requires a non-empty task");
168
+ throw new Error("agents_delegate requires a non-empty task");
169
169
  const name = options.input.agent?.trim();
170
170
  if (name === undefined || name === "") {
171
- throw new Error("agent_spawn requires an explicit agent name; V1 has no built-in default agent");
171
+ throw new Error("agents_delegate requires an explicit agent name; V1 has no built-in default agent");
172
172
  }
173
173
  const cwd = resolve(options.input.cwd?.trim() || options.cwd);
174
- const requestedTitle = options.input.title?.trim();
175
- // A blank title is the same as no title: the child derives one from its own identity.
176
- const title = requestedTitle === "" ? undefined : requestedTitle;
177
174
  const subagentId = options.subagentId ?? createSubagentId(options.existingSubagentIds);
178
175
  const sessionId = options.sessionId ?? randomUUID();
179
176
  const bridgeExtensionPath = options.bridgeExtensionPath ?? resolveBridgeExtensionPath();
@@ -209,7 +206,6 @@ export async function resolveSubagentLaunch(options) {
209
206
  prompt: assembleChildPrompt(instructions),
210
207
  bridgeExtensionPath,
211
208
  ...(policy.codemodeOnly ? { codemodeOnly: true } : {}),
212
- ...(title === undefined ? {} : { title }),
213
209
  });
214
210
  }
215
211
  /**
@@ -1,5 +1,5 @@
1
1
  import { DatabaseSync } from "node:sqlite";
2
- /** The one durable file for child records, commands, and tokens. */
2
+ /** The one durable file for child records, the mailbox, and delegation receipts. */
3
3
  export declare function defaultStatePath(): string;
4
4
  export declare function openStateDb(filePath: string): DatabaseSync;
5
5
  export declare function withWrite<T>(db: DatabaseSync, run: () => T): T;
@@ -47,6 +47,13 @@ export declare function finishCommand(db: DatabaseSync, id: string, outcome: {
47
47
  readonly errorCode: string;
48
48
  readonly error: string;
49
49
  }): void;
50
- export declare function readToken(db: DatabaseSync, parentSessionId: string, runtimeIdentity: string): string | undefined;
51
- export declare function writeToken(db: DatabaseSync, parentSessionId: string, runtimeIdentity: string, token: string): void;
52
- export declare function deleteToken(db: DatabaseSync, parentSessionId: string, runtimeIdentity: string): void;
50
+ /**
51
+ * A settled command whose waiter died with its process has no reader left. Only a parent that is
52
+ * taking over the mailbox may run this: a live waiter deletes its own row when it reads it.
53
+ */
54
+ export declare function deleteSettledCommands(db: DatabaseSync, parentSessionId: string): void;
55
+ /**
56
+ * Keeps only the newest `keep` queued events for one child. Events are advisory snapshots, so a
57
+ * parent that stopped reading must not leave an unbounded backlog behind.
58
+ */
59
+ export declare function trimEventBacklog(db: DatabaseSync, parentSessionId: string, subagentId: string, keep: number): void;