@fitchmultz/pi-subagents 0.44.4 → 0.44.5

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.
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+ import https from "node:https";
3
+ import { lstatSync, readFileSync } from "node:fs";
4
+ import { parseArgs } from "node:util";
5
+
6
+ function help() {
7
+ console.log(`Usage: node intercom-remote.mjs <register|list|inbox|send|ask|reply|ack> [options] [IDs...]
8
+
9
+ Environment (required):
10
+ INTERCOM_BRIDGE_CA Path to trusted bridge CA certificate
11
+ INTERCOM_BRIDGE_CERT Path to your client certificate
12
+ INTERCOM_BRIDGE_KEY Path to your private client key (owned, mode 0600)
13
+ INTERCOM_BRIDGE_URL HTTPS origin (default: https://127.0.0.1:9443)
14
+
15
+ Options:
16
+ --to PEER Recipient name or ID for send/ask
17
+ --message TEXT Message for send/ask/reply
18
+ --reply-to ID Inbound ask ID from inbox, for reply
19
+ --timeout-ms MS Ask timeout (100..120000, default: 120000)
20
+ -h, --help Show help
21
+
22
+ Examples:
23
+ node intercom-remote.mjs list
24
+ node intercom-remote.mjs ask --to planner --message 'What is the current status?'
25
+ node intercom-remote.mjs inbox
26
+ node intercom-remote.mjs reply --reply-to QUESTION_ID --message 'Proceed.'
27
+ node intercom-remote.mjs ack MESSAGE_ID
28
+
29
+ Outputs JSON. Exit: 0 success, 1 API/TLS/transport failure, 2 invalid arguments.
30
+ No automatic retries: a lost response does not prove a message was unsent.`);
31
+ }
32
+
33
+ let options, body, action, origin, timeout;
34
+ try {
35
+ const parsed = parseArgs({ allowPositionals: true, options: {
36
+ help: { type: "boolean", short: "h" }, to: { type: "string" }, message: { type: "string" },
37
+ "reply-to": { type: "string" }, "timeout-ms": { type: "string" },
38
+ } });
39
+ if (parsed.values.help) { help(); process.exit(0); }
40
+ options = parsed.values;
41
+ [action] = parsed.positionals;
42
+ if (!["register", "list", "inbox", "send", "ask", "reply", "ack"].includes(action)) throw new Error("Choose an action; use --help.");
43
+ const allowed = ["send", "ask"].includes(action) ? ["to", "message", ...(action === "ask" ? ["timeout-ms"] : [])] : action === "reply" ? ["reply-to", "message"] : [];
44
+ if (Object.keys(options).some((key) => !allowed.includes(key)) ||
45
+ (action !== "ack" && parsed.positionals.length !== 1)) throw new Error("Unexpected arguments for this action.");
46
+ timeout = options["timeout-ms"] === undefined ? 120000 : Number(options["timeout-ms"]);
47
+ if (!Number.isInteger(timeout) || timeout < 100 || timeout > 120000) throw new Error("--timeout-ms must be 100..120000.");
48
+ body = ["send", "ask"].includes(action) ? { to: options.to, message: options.message, ...(action === "ask" ? { timeoutMs: timeout } : {}) }
49
+ : action === "reply" ? { replyTo: options["reply-to"], message: options.message }
50
+ : action === "ack" ? { ids: parsed.positionals.slice(1) } : {};
51
+ if (["send", "ask", "reply"].includes(action) && (!body.message?.trim() || !(action === "reply" ? body.replyTo : body.to)?.trim())) throw new Error("Recipient/ask ID and message are required.");
52
+ if (action === "ack" && body.ids.length === 0) throw new Error("ack needs at least one message ID.");
53
+ origin = new URL(process.env.INTERCOM_BRIDGE_URL || "https://127.0.0.1:9443");
54
+ if (origin.protocol !== "https:" || origin.username || origin.password || origin.pathname !== "/" || origin.search || origin.hash) throw new Error("INTERCOM_BRIDGE_URL must be an HTTPS origin, without credentials or a path.");
55
+ } catch (error) {
56
+ console.error(JSON.stringify({ ok: false, error: { code: "invalid_arguments", message: error.message } }));
57
+ process.exit(2);
58
+ }
59
+
60
+ try {
61
+ const files = {};
62
+ for (const field of ["CA", "CERT", "KEY"]) {
63
+ const path = process.env[`INTERCOM_BRIDGE_${field}`];
64
+ if (!path) throw new Error(`INTERCOM_BRIDGE_${field} is required.`);
65
+ if (field === "KEY") {
66
+ const st = lstatSync(path);
67
+ if (!st.isFile() || st.uid !== process.getuid() || (st.mode & 0o077)) throw new Error("Client key must be an owned private regular file (0600).");
68
+ }
69
+ files[field] = readFileSync(path);
70
+ }
71
+ origin.pathname = `/v1/${action}`;
72
+ const payload = JSON.stringify(body);
73
+ const get = ["list", "inbox"].includes(action);
74
+ const result = await new Promise((resolve, reject) => {
75
+ const req = https.request(origin, {
76
+ method: get ? "GET" : "POST", ca: files.CA, cert: files.CERT, key: files.KEY,
77
+ rejectUnauthorized: true, minVersion: "TLSv1.2", agent: false,
78
+ headers: get ? {} : { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payload) },
79
+ }, (res) => {
80
+ const chunks = [];
81
+ let bytes = 0;
82
+ res.on("data", (chunk) => {
83
+ bytes += chunk.length;
84
+ if (bytes > 16 * 1024 * 1024) { req.destroy(new Error("Bridge response exceeds 16 MiB.")); return; }
85
+ chunks.push(chunk);
86
+ });
87
+ res.on("error", reject);
88
+ res.on("end", () => {
89
+ try { resolve({ status: res.statusCode, value: JSON.parse(Buffer.concat(chunks).toString("utf8")) }); }
90
+ catch { reject(new Error("Bridge returned invalid JSON.")); }
91
+ });
92
+ });
93
+ const timer = setTimeout(() => req.destroy(new Error("Bridge request deadline exceeded; delivery may be unknown.")), (action === "ask" ? timeout : 0) + 15000);
94
+ req.once("close", () => clearTimeout(timer));
95
+ req.once("error", reject);
96
+ req.end(get ? undefined : payload);
97
+ });
98
+ console.log(JSON.stringify(result.value, null, 2));
99
+ if (result.status < 200 || result.status >= 300 || result.value?.ok !== true) process.exitCode = 1;
100
+ } catch (error) {
101
+ console.error(JSON.stringify({ ok: false, error: { code: "transport_error", message: error.message, delivery: "unknown; do not blindly retry mutations" } }));
102
+ process.exitCode = 1;
103
+ }
@@ -77,6 +77,10 @@ for (const path of [
77
77
  "dist/extension/index.js",
78
78
  "dist/pi-intercom/index.js",
79
79
  "dist/pi-intercom/broker/broker.js",
80
+ "dist/pi-intercom/bridge.js",
81
+ "scripts/intercom-bridge-credentials.mjs",
82
+ "scripts/intercom-remote.mjs",
83
+ "docs/intercom-bridge.md",
80
84
  "dist/runs/background/subagent-runner-launcher.js",
81
85
  "src/extension/index.ts",
82
86
  "src/extension/schemas.ts",
@@ -67,7 +67,7 @@ export function registerCompactSubagentTools(pi: ExtensionAPI, options: {
67
67
  name: "agent_runs",
68
68
  defaultActive: false,
69
69
  label: "Agent Runs",
70
- description: `List ${childSafe ? "only this child's directly owned" : "your delegated"} runs across working directories (questions/failures, then live work, then unreviewed results; 20 per page). Filter globally by agent/state/text, sort, and page with the returned cursor. history reads 100 bounded native-entry previews; search finds saved visible text using words or quoted phrases, not operators or prefixes. Browse freshness is not completion or delivery proof. Inspect concise results, paths and continuations; full:true includes the full task/configuration. Answer durable questions, nudge, stop, continue, or save parent-only review. Review notes are not sent to children; put actionable instructions in continue/nudge. Inspect/review/nudge never restart finished work. Continue/answer can launch a saved child; async:false waits for its actual result. Saved continuations keep settings unless agent selects a current profile; model overrides win. Live guidance never mutates model or acceptance. profiles lists roles, sources, context and model/thinking/fallback defaults. History survives reload.`,
70
+ description: `List ${childSafe ? "only this child's directly owned" : "your delegated"} runs across working directories (questions/failures, then live work, then unreviewed results; 20 per page). Filter globally by agent/state/text, sort, and page with the returned cursor. history reads 100 bounded native-entry previews; search finds saved visible text using words or quoted phrases, not operators or prefixes. questions pages 20 compact previews (offset/limit); questionId selects one, and questionPath/answerPath retain full records. Browse freshness is not completion or delivery proof. Inspect concise results, paths and continuations; full:true includes the full task/configuration. Answer durable questions, nudge, stop, continue, or save parent-only review. Review notes are not sent to children; put actionable instructions in continue/nudge. Inspect/review/nudge never restart finished work. Continue/answer can launch a saved child; async:false waits for its actual result. Saved continuations keep settings unless agent selects a current profile; model overrides win. Live guidance never mutates model or acceptance. profiles lists roles, sources, context and model/thinking/fallback defaults. History survives reload.`,
71
71
  parameters: AgentRunsParams,
72
72
  async execute(id, params, signal, onUpdate, ctx) {
73
73
  const normalized = normalizeEverydayParams(params, true);
@@ -279,13 +279,13 @@ const HistoryQueryFields = {
279
279
  export const AgentRunsValidationParams = Type.Object({
280
280
  action: Type.Enum(["list", "inspect", "history", "search", "nudge", "stop", "continue", "profiles", "questions", "answer", "review"] as const, { type: "string" }),
281
281
  id: Type.Optional(Type.String({ minLength: 1, description: "Run ID or unambiguous prefix." })),
282
- questionId: Type.Optional(Type.String({ minLength: 1, description: "Durable supervisor question ID for answer." })),
282
+ questionId: Type.Optional(Type.String({ minLength: 1, description: "Durable supervisor question ID for exact questions lookup or answer." })),
283
283
  async: Type.Optional(Type.Boolean({ description: "Continue/answer: false waits for the actual continuation result." })),
284
284
  index: Type.Optional(Type.Integer({ minimum: 0, description: "Child index for a multi-child run." })),
285
285
  message: Type.Optional(Type.String({ minLength: 1, description: "Guidance, follow-up, answer, or optional parent-only review note. Review notes are not sent to the child; put actionable instructions in continue/nudge. Only continue/answer can start a saved child." })),
286
286
  decision: Type.Optional(Type.Enum(["accepted", "needs_changes"] as const, { type: "string", description: "Parent-only review outcome; not sent to the child. Separate from execution and runtime acceptance checks." })),
287
- offset: Type.Optional(Type.Integer({ minimum: 0, description: "List offset; history is retained regardless of page size." })),
288
- limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 100, description: "Results per list/search/history page (default 20; history 100)." })),
287
+ offset: Type.Optional(Type.Integer({ minimum: 0, description: "List/questions offset; history is retained regardless of page size." })),
288
+ limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 100, description: "Results per list/questions/search/history page (default 20; history 100)." })),
289
289
  ...HistoryQueryFields,
290
290
  full: Type.Optional(Type.Boolean({ description: "Inspect only: include the full task and saved launch configuration. Default is a concise report; stored data is unchanged." })),
291
291
  agent: Type.Optional(Type.String({ minLength: 1, description: "List/search: filter by child agent. Continue/answer: adopt this current profile, including model, thinking and fallbacks; a separate model override wins. Otherwise keep saved settings. Required for old runs without a saved profile." })),
@@ -303,8 +303,8 @@ export const AgentRunsValidationParams = Type.Object({
303
303
  { if: { properties: { action: { enum: ["review"] } } }, then: requiredObject("decision") },
304
304
  { if: { anyOf: [requiredObject("async"), requiredObject("model"), requiredObject("cwd"), requiredObject("output"), requiredObject("acceptance")] }, then: { properties: { action: { enum: ["continue", "answer"] } } } },
305
305
  { if: requiredObject("agent"), then: { properties: { action: { enum: ["list", "search", "continue", "answer"] } } } },
306
- { if: requiredObject("offset"), then: { properties: { action: { enum: ["list"] } } } },
307
- { if: requiredObject("limit"), then: { properties: { action: { enum: ["list", "history", "search"] } } } },
306
+ { if: requiredObject("offset"), then: { properties: { action: { enum: ["list", "questions"] } } } },
307
+ { if: requiredObject("limit"), then: { properties: { action: { enum: ["list", "questions", "history", "search"] } } } },
308
308
  { if: requiredObject("cursor"), then: { properties: { action: { enum: ["list", "history", "search"] } } } },
309
309
  { if: requiredObject("sort"), then: { properties: { action: { enum: ["list", "search"] } } } },
310
310
  { not: { anyOf: [requiredObject("cursor", "offset"), requiredObject("cursor", "before")] } },
@@ -339,9 +339,9 @@ export const SubagentParams = Type.Object({
339
339
  runId: Type.Optional(Type.String({
340
340
  description: "Target run ID; prefer id. Defaults to the most recently active controllable run for interrupt/extend/nudge."
341
341
  })),
342
- questionId: Type.Optional(Type.String({ minLength: 1, description: "Durable supervisor question ID for answer." })),
342
+ questionId: AgentRunsValidationParams.properties.questionId,
343
343
  decision: Type.Optional(Type.Enum(["accepted", "needs_changes"] as const, { type: "string", description: "Parent-only review decision; not sent to the child. Put actionable instructions in resume/nudge. Does not launch work or change runtime acceptance." })),
344
- offset: Type.Optional(Type.Integer({ minimum: 0, description: "Status list offset." })),
344
+ offset: Type.Optional(Type.Integer({ minimum: 0, description: "Status list/questions offset." })),
345
345
  limit: AgentRunsValidationParams.properties.limit,
346
346
  ...HistoryQueryFields,
347
347
  full: Type.Optional(Type.Boolean({ description: "Exact status only: include the full task and saved launch configuration; default is concise." })),
@@ -418,8 +418,8 @@ export const SubagentParams = Type.Object({
418
418
  { if: requiredObject("decision"), then: { ...requiredObject("action"), properties: { action: { enum: ["review"] } } } },
419
419
  { if: { ...requiredObject("action"), properties: { action: { enum: ["history"] } } }, then: { anyOf: [requiredObject("id"), requiredObject("runId")] } },
420
420
  { if: { ...requiredObject("action"), properties: { action: { enum: ["search"] } } }, then: requiredObject("query") },
421
- { if: requiredObject("offset"), then: { ...requiredObject("action"), properties: { action: { enum: ["status"] } } } },
422
- { if: requiredObject("limit"), then: { ...requiredObject("action"), properties: { action: { enum: ["status", "history", "search"] } } } },
421
+ { if: requiredObject("offset"), then: { ...requiredObject("action"), properties: { action: { enum: ["status", "questions"] } } } },
422
+ { if: requiredObject("limit"), then: { ...requiredObject("action"), properties: { action: { enum: ["status", "questions", "history", "search"] } } } },
423
423
  { if: requiredObject("cursor"), then: { ...requiredObject("action"), properties: { action: { enum: ["status", "history", "search"] } } } },
424
424
  { if: requiredObject("sort"), then: { ...requiredObject("action"), properties: { action: { enum: ["status", "search"] } } } },
425
425
  { if: { allOf: [{ ...requiredObject("action"), properties: { action: { enum: ["search"] } } }, requiredObject("index")] }, then: { anyOf: [requiredObject("id"), requiredObject("runId")] } },
@@ -87,6 +87,15 @@ export class SubagentHistoryIndex {
87
87
  }
88
88
  await this.ready;
89
89
  }
90
+ /** Arms an ordinary deadline only in the continuation that saw the current admission and worker settle, never behind a replacement admission. */
91
+ private async admitted<T>(method: string, input?: unknown, signal?: AbortSignal, timeout?: number): Promise<T> {
92
+ await this.ensure();
93
+ const ready = this.ready, child = this.process;
94
+ await ready;
95
+ // setOwner() or a restart may have replaced readiness during either await; wait for the replacement.
96
+ if (!child || ready !== this.ready || child !== this.process) return this.admitted<T>(method, input, signal, timeout);
97
+ return this.send<T>(method, input, signal, timeout);
98
+ }
90
99
  async setOwner(input: HistoryOwner): Promise<void> {
91
100
  if (this.closed) throw new HistoryIndexError("CLOSED", "History index is closed.");
92
101
  if (!this.process) await Promise.all(this.children.values());
@@ -104,20 +113,16 @@ export class SubagentHistoryIndex {
104
113
  await this.ensure();
105
114
  if (run.ownerSessionId !== this.owner!.ownerSessionId || foreground && foreground.runId !== run.runId) throw new HistoryIndexError("OWNERSHIP", "Run does not belong to the current owner.");
106
115
  const copy = structuredClone(run), fg = foreground && structuredClone(foreground);
107
- await this.send<void>("updateRun", { run: copy, foreground: fg });
116
+ await this.admitted<void>("updateRun", { run: copy, foreground: fg });
108
117
  this.owner!.runs = [...this.owner!.runs.filter((entry) => entry.runId !== run.runId), copy];
109
118
  if (fg) this.owner!.foregroundRuns = [...(this.owner!.foregroundRuns ?? []).filter((entry) => entry.runId !== run.runId), fg];
110
119
  }
111
120
  private async query<T>(method: string, input: { signal?: AbortSignal }): Promise<T> {
112
- await this.ensure();
113
121
  const { signal, ...wire } = input;
114
- return this.send<T>(method, wire, signal);
122
+ return this.admitted<T>(method, wire, signal);
115
123
  }
116
124
  /** Tool discovery waits for run metadata only, not transcript backfill. */
117
- async needsControls(): Promise<boolean> {
118
- await this.ensure();
119
- return this.send("needsControls", undefined, undefined, 15_000);
120
- }
125
+ needsControls(): Promise<boolean> { return this.admitted("needsControls", undefined, undefined, 15_000); }
121
126
  listRuns(options: HistoryRunOptions = {}): Promise<HistoryRunPage> { return this.query("listRuns", options); }
122
127
  historyPage(input: HistoryPageInput): Promise<HistoryPage> { return this.query("historyPage", input); }
123
128
  search(input: HistorySearchInput): Promise<HistorySearchPage> { return this.query("search", input); }
@@ -130,8 +135,7 @@ export class SubagentHistoryIndex {
130
135
  result(input: Pick<HistoryEntryInput, "runId" | "index" | "signal">): Promise<HistoryResult | null> { return this.query("result", input); }
131
136
  async refresh(runId?: string, options: { signal?: AbortSignal } = {}): Promise<void> {
132
137
  if (!this.closed) this.unavailable = undefined;
133
- await this.ensure();
134
- return this.send("refresh", { runId }, options.signal, 120_000);
138
+ return this.admitted("refresh", { runId }, options.signal, 120_000);
135
139
  }
136
140
  onChanged(listener: () => void): () => void { this.listeners.add(listener); return () => this.listeners.delete(listener); }
137
141
  /** Hard cancellation affects this instance's outstanding work, not native sessions or other indexers. */
@@ -175,8 +175,13 @@ export class SourceIngest {
175
175
  private fd: number;
176
176
  private initial: fs.BigIntStats;
177
177
  private source: SourceRow;
178
- private prefix: Hash = createHash("sha256");
179
178
  private recordDigest: Hash = createHash("sha256");
179
+ /** prefix_digest is "start:sha256" of the last indexed line; earlier releases hashed the whole prefix. */
180
+ private legacy = false;
181
+ private expected: string | null = null;
182
+ private verified: Hash = createHash("sha256");
183
+ private line: Hash = createHash("sha256");
184
+ private lineStart = 0;
180
185
  private verifyAt = 0;
181
186
  private position: number;
182
187
  private frames: JournalFrames;
@@ -208,6 +213,10 @@ export class SourceIngest {
208
213
  this.headerSeen = source.cursor > 0;
209
214
  store.clearUnpublished(source.id);
210
215
  store.run("DELETE FROM pending_text");
216
+ // Appends re-hash only the last indexed line, so live sources cost their new bytes, not their size.
217
+ const anchor = /^(\d+):([0-9a-f]{64})$/.exec(source.prefix_digest ?? "");
218
+ this.legacy = !anchor; this.expected = anchor ? anchor[2] : source.prefix_digest;
219
+ this.verifyAt = this.lineStart = anchor ? Number(anchor[1]) : 0;
211
220
  this.phase = !force && source.stamp === stamp(this.initial) ? "done" : source.cursor ? "verify" : "read";
212
221
  if (this.phase !== "done") store.run("UPDATE sources SET state='indexing',error=NULL WHERE id=?", source.id);
213
222
  this.frames = new JournalFrames(previewProjection, (record) => {
@@ -224,7 +233,7 @@ export class SourceIngest {
224
233
  const current = this.store.source(this.id)!;
225
234
  if (!current || current.generation !== this.source.generation || current.cursor !== this.source.cursor) throw new HistoryIndexError("SOURCE_CHANGED", "Another writer advanced this source; reconcile before replaying.");
226
235
  work?.();
227
- this.store.run("UPDATE sources SET cursor=?,prefix_digest=?,identity=? WHERE id=?", end, this.prefix.copy().digest("hex"), identity(this.initial), this.id);
236
+ this.store.run("UPDATE sources SET cursor=?,prefix_digest=?,identity=? WHERE id=?", end, `${this.source.cursor}:${this.recordDigest.copy().digest("hex")}`, identity(this.initial), this.id);
228
237
  this.store.bump();
229
238
  });
230
239
  this.source.cursor = end;
@@ -293,11 +302,19 @@ export class SourceIngest {
293
302
  const count = fs.readSync(this.fd, bytes, 0, bytes.length, this.verifyAt);
294
303
  if (count !== bytes.length) throw new Error("Journal truncated during prefix verification.");
295
304
  this.store.operations.sourceBytesRead += count;
296
- this.prefix.update(bytes); this.verifyAt += count;
305
+ this.verified.update(bytes);
306
+ if (this.legacy) {
307
+ // A whole-prefix check also finds the last indexed line, which anchors every later check.
308
+ const before = this.source.cursor - 2 - this.verifyAt, lf = before < 0 ? -1 : bytes.lastIndexOf(10, before);
309
+ if (lf < 0) this.line.update(bytes);
310
+ else { this.lineStart = this.verifyAt + lf + 1; this.line = createHash("sha256").update(bytes.subarray(lf + 1)); }
311
+ }
312
+ this.verifyAt += count;
297
313
  if (this.verifyAt === this.source.cursor) {
298
- if (this.prefix.copy().digest("hex") !== this.source.prefix_digest) {
314
+ if (this.verified.digest("hex") !== this.expected) {
299
315
  this.store.resetSource(this.id); throw new HistoryIndexError("SOURCE_CHANGED", "Journal prefix changed; a new physical generation was scheduled.");
300
316
  }
317
+ if (this.legacy) this.store.run("UPDATE sources SET prefix_digest=? WHERE id=?", `${this.lineStart}:${this.line.digest("hex")}`, this.id);
301
318
  this.phase = "read";
302
319
  }
303
320
  return false;
@@ -313,7 +330,7 @@ export class SourceIngest {
313
330
  }
314
331
  const newline = this.buffer.indexOf(10);
315
332
  const bytes = newline < 0 ? this.buffer : this.buffer.subarray(0, newline + 1);
316
- this.recordDigest.update(bytes); this.prefix.update(bytes); this.frames.write(bytes); this.text.flush(this.store);
333
+ this.recordDigest.update(bytes); this.frames.write(bytes); this.text.flush(this.store);
317
334
  this.buffer = this.buffer.subarray(bytes.length);
318
335
  if (newline >= 0) this.finishRecord(this.frames.offset);
319
336
  return false;
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { ownedRunView } from "../runs/shared/run-records.ts";
4
4
  import { acceptanceHumanAction } from "../runs/shared/acceptance-evaluation.ts";
5
- import { getRunMetadataDir, listRunQuestions } from "../runs/shared/supervisor-questions.ts";
5
+ import { getRunMetadataDir, listRunQuestions, summarizeSupervisorQuestion } from "../runs/shared/supervisor-questions.ts";
6
6
  import { HistoryIndexError } from "./types.ts";
7
7
  import type { ForegroundResumeRun, HistoryOwner, HistoryRunRow, OwnedRun, Request, Response } from "./types.ts";
8
8
  import type { SubagentState } from "../shared/types.ts";
@@ -28,7 +28,10 @@ const controlQueries: number[] = [];
28
28
  function replyControls(id: number): void {
29
29
  send({ id, value: Boolean(store!.get("SELECT id FROM runs WHERE attention<4 OR state='live' LIMIT 1")) });
30
30
  }
31
- function send(response: Response): void { if (process.connected) process.send!(response); }
31
+ function send(response: Response): void {
32
+ // The parent can disconnect after process.connected was read; then this and later replies are undeliverable, so stop as on disconnect.
33
+ if (process.connected) process.send!(response, undefined, undefined, (error) => { if (error) shutdown(); });
34
+ }
32
35
  function changed(): void { send({ changed: true }); }
33
36
  function errorReply(id: number, error: unknown): void {
34
37
  const code = error instanceof HistoryIndexError ? error.code : "UNAVAILABLE";
@@ -43,10 +46,10 @@ function compactView(view: HistoryRunRow): HistoryRunRow {
43
46
  return {
44
47
  ...view, task: safeText(view.task, 2048), error: view.error && safeText(view.error), diagnosis: view.diagnosis && safeText(view.diagnosis), recoveryError: view.recoveryError && safeText(view.recoveryError),
45
48
  review: view.review && { ...view.review, message: view.review.message && safeText(view.review.message) }, continuations: [],
46
- questions: view.questions?.map((question) => ({ questionId: question.questionId, runId: question.runId, ownerSessionId: question.ownerSessionId, ownerTarget: question.ownerTarget,
47
- agent: question.agent, index: question.index, childSessionId: question.childSessionId, childTarget: question.childTarget, sessionFile: question.sessionFile,
48
- cwd: question.cwd, pid: question.pid, processIdentity: question.processIdentity, createdAt: question.createdAt, reason: question.reason, message: safeText(question.message, 2048), state: question.state,
49
- answer: question.answer && { ...question.answer, message: safeText(question.answer.message, 2048) }, delivery: question.delivery, revival: question.revival })),
49
+ questions: view.questions?.map((question) => {
50
+ const summary = summarizeSupervisorQuestion(question);
51
+ return { ...summary, message: safeText(question.message, 2048), answer: summary.answer && { ...summary.answer, message: safeText(question.answer?.message, 2048) } };
52
+ }),
50
53
  children: view.children.map((child) => ({
51
54
  agent: child.agent, index: child.index, workflowNodeId: child.workflowNodeId, sessionFile: child.sessionFile, task: child.task && safeText(child.task), label: child.label && safeText(child.label, 256),
52
55
  state: child.state, configuration: child.configuration, missingSession: child.missingSession, identityUnavailable: child.identityUnavailable, modelSelection: child.modelSelection,
@@ -314,4 +317,5 @@ async function handle(request: Request): Promise<void> {
314
317
  } catch (error) { errorReply(request.id, error); }
315
318
  }
316
319
  process.on("message", (request: Request) => { void handle(request); });
317
- process.on("disconnect", () => { closed = true; clearInterval(timer); clear(); process.exit(0); });
320
+ function shutdown(): void { closed = true; clearInterval(timer); clear(); process.exit(0); }
321
+ process.on("disconnect", shutdown);