@hheei/pi-subagents 0.2.3 → 0.2.4

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/dist/extension.js CHANGED
@@ -16,6 +16,7 @@ import { createSubagentWidget } from "./widget.js";
16
16
  const CHILD_ENV_KEYS = Object.values(BRIDGE_ENVIRONMENT_KEYS);
17
17
  /** How long startup waits for a child that survived a parent restart to reconnect its bridge. */
18
18
  const ADOPT_WINDOW_MS = 5_000;
19
+ const START_WINDOW_MS = 20_000;
19
20
  function diagnose(message) {
20
21
  if (process.env.DEBUG || process.env.PI_SUBAGENTS_DEBUG) {
21
22
  console.error(`pi-subagents: ${message}`);
@@ -237,7 +238,7 @@ export default function piSubagentsExtension(pi) {
237
238
  record,
238
239
  tokens,
239
240
  waitForBridge: (childId, signal) => server.waitForConnection(childId, {
240
- timeoutMs: ADOPT_WINDOW_MS,
241
+ timeoutMs: START_WINDOW_MS,
241
242
  ...(signal === undefined ? {} : { signal }),
242
243
  }),
243
244
  signal: runtime.signal,
@@ -254,7 +255,7 @@ export default function piSubagentsExtension(pi) {
254
255
  tokens,
255
256
  host: hostSelection.adapter,
256
257
  waitForBridge: (childId, signal) => server.waitForConnection(childId, {
257
- timeoutMs: ADOPT_WINDOW_MS,
258
+ timeoutMs: START_WINDOW_MS,
258
259
  ...(signal === undefined ? {} : { signal }),
259
260
  }),
260
261
  signal: runtime.signal,
package/dist/manager.js CHANGED
@@ -346,7 +346,7 @@ export class SubagentManager {
346
346
  ...withoutRuntime,
347
347
  intent: "stopped",
348
348
  state: "error",
349
- interrupted: "The child panel was closed while running; spawn a new child for more work",
349
+ interrupted: "The host reports that the child process exited while running; the cause is unknown. Spawn a new child for more work",
350
350
  };
351
351
  });
352
352
  if (retired !== undefined)
@@ -400,7 +400,9 @@ export class SubagentManager {
400
400
  this.#clearAutoReport(id);
401
401
  const timer = setTimeout(() => {
402
402
  this.#autoReportTimers.delete(id);
403
- void this.#performAutoReport(id, record);
403
+ void this.#performAutoReport(id, record).catch((error) => {
404
+ diagnose(`failed to report child ${id}: ${errorMessage(error)}`);
405
+ });
404
406
  }, 5_000);
405
407
  timer.unref?.();
406
408
  this.#autoReportTimers.set(id, timer);
@@ -443,7 +445,10 @@ export class SubagentManager {
443
445
  async #closeSettledPanel(id) {
444
446
  await this.#mutate(id, async () => {
445
447
  const record = await this.#deps.registry.get(id);
446
- if (record === undefined || record.state === "running" || record.intent === "stopped") {
448
+ if (record === undefined ||
449
+ record.state === "running" ||
450
+ record.intent === "stopped" ||
451
+ this.#working.has(id)) {
447
452
  return;
448
453
  }
449
454
  const attachment = this.#attachments.get(id);
@@ -516,83 +521,95 @@ export class SubagentManager {
516
521
  const record = (await this.#deps.registry.get(id)) ?? _record;
517
522
  if (record.intent === "stopped" || record.state === "running")
518
523
  return;
524
+ const activity = this.#activity.get(id) ?? 0;
525
+ const workVersion = this.#workVersions.get(id) ?? 0;
519
526
  const entries = await this.#entries(id);
520
527
  const text = entries !== undefined ? extractLastAssistantText(entries) : undefined;
521
- const hasError = record.state === "error" ||
522
- (record.interrupted !== undefined && record.interrupted.trim() !== "");
523
- const isBlocked = hasError;
524
- if (!isBlocked && (text === undefined || text.trim() === "")) {
528
+ await this.#mutate(id, async () => {
529
+ const current = await this.#deps.registry.get(id);
530
+ if (current === undefined ||
531
+ current.intent === "stopped" ||
532
+ current.state === "running" ||
533
+ this.#working.has(id) ||
534
+ (this.#activity.get(id) ?? 0) !== activity ||
535
+ (this.#workVersions.get(id) ?? 0) !== workVersion)
536
+ return;
537
+ const hasError = record.state === "error" ||
538
+ (record.interrupted !== undefined && record.interrupted.trim() !== "");
539
+ const isBlocked = hasError;
540
+ if (!isBlocked && (text === undefined || text.trim() === "")) {
541
+ const projector = this.#projectors.get(id);
542
+ if (projector !== undefined) {
543
+ projector.syncState("done");
544
+ }
545
+ await this.#update(id, (current) => {
546
+ const { interrupted: _interrupted, ...clean } = current;
547
+ return {
548
+ ...clean,
549
+ state: "done",
550
+ };
551
+ });
552
+ if (this.#taskRegistry !== undefined) {
553
+ this.#taskRegistry.settle(id, {
554
+ status: "completed",
555
+ output: "",
556
+ truncated: false,
557
+ });
558
+ }
559
+ return;
560
+ }
561
+ let message;
562
+ if (isBlocked) {
563
+ if (record.interrupted !== undefined && record.interrupted.trim() !== "") {
564
+ message =
565
+ text !== undefined && text.trim() !== ""
566
+ ? `${record.interrupted}\n\n${text}`
567
+ : record.interrupted;
568
+ }
569
+ else if (text !== undefined && text.trim() !== "") {
570
+ message = text;
571
+ }
572
+ else {
573
+ message = "Subagent became idle without completing its task (interrupted or failed).";
574
+ }
575
+ }
576
+ else {
577
+ message = text ?? "";
578
+ }
579
+ if (this.#lastDeliveredTurn.get(id) === message)
580
+ return;
581
+ this.#lastDeliveredTurn.set(id, message);
582
+ const reason = isBlocked ? "blocked" : "success";
583
+ const taskStatus = isBlocked ? "failed" : "completed";
525
584
  const projector = this.#projectors.get(id);
585
+ const updatedRecord = !isBlocked
586
+ ? await this.#update(id, (current) => {
587
+ const { interrupted: _interrupted, ...clean } = current;
588
+ return {
589
+ ...clean,
590
+ state: "done",
591
+ };
592
+ })
593
+ : await this.#update(id, (current) => ({
594
+ ...current,
595
+ state: "blocked",
596
+ interrupted: current.interrupted ?? message,
597
+ }));
526
598
  if (projector !== undefined) {
527
- projector.syncState("done");
599
+ projector.syncState(isBlocked ? "blocked" : "done");
600
+ }
601
+ this.#scheduleSettledPanelClose(id);
602
+ if (this.#deps.channel !== undefined) {
603
+ await this.contactParent(id, reason, message, updatedRecord);
528
604
  }
529
- await this.#update(id, (current) => {
530
- const { interrupted: _interrupted, ...clean } = current;
531
- return {
532
- ...clean,
533
- state: "done",
534
- };
535
- });
536
605
  if (this.#taskRegistry !== undefined) {
537
606
  this.#taskRegistry.settle(id, {
538
- status: "completed",
539
- output: "",
607
+ status: taskStatus,
608
+ output: message,
540
609
  truncated: false,
541
610
  });
542
611
  }
543
- return;
544
- }
545
- let message;
546
- if (isBlocked) {
547
- if (record.interrupted !== undefined && record.interrupted.trim() !== "") {
548
- message =
549
- text !== undefined && text.trim() !== ""
550
- ? `${record.interrupted}\n\n${text}`
551
- : record.interrupted;
552
- }
553
- else if (text !== undefined && text.trim() !== "") {
554
- message = text;
555
- }
556
- else {
557
- message = "Subagent became idle without completing its task (interrupted or failed).";
558
- }
559
- }
560
- else {
561
- message = text ?? "";
562
- }
563
- if (this.#lastDeliveredTurn.get(id) === message)
564
- return;
565
- this.#lastDeliveredTurn.set(id, message);
566
- const reason = isBlocked ? "blocked" : "success";
567
- const taskStatus = isBlocked ? "failed" : "completed";
568
- const projector = this.#projectors.get(id);
569
- const updatedRecord = !isBlocked
570
- ? await this.#update(id, (current) => {
571
- const { interrupted: _interrupted, ...clean } = current;
572
- return {
573
- ...clean,
574
- state: "done",
575
- };
576
- })
577
- : await this.#update(id, (current) => ({
578
- ...current,
579
- state: "blocked",
580
- interrupted: current.interrupted ?? message,
581
- }));
582
- if (projector !== undefined) {
583
- projector.syncState(isBlocked ? "blocked" : "done");
584
- }
585
- this.#scheduleSettledPanelClose(id);
586
- if (this.#deps.channel !== undefined) {
587
- await this.contactParent(id, reason, message, updatedRecord);
588
- }
589
- if (this.#taskRegistry !== undefined) {
590
- this.#taskRegistry.settle(id, {
591
- status: taskStatus,
592
- output: message,
593
- truncated: false,
594
- });
595
- }
612
+ });
596
613
  }
597
614
  /** Reads a child's session entries; exposed for Task completion inspection. */
598
615
  async getEntries(id) {
@@ -848,7 +865,7 @@ export class SubagentManager {
848
865
  childId = record.subagentId;
849
866
  this.#working.add(childId);
850
867
  this.#notify();
851
- await withDeadline(this.#start(record), this.#deps.deadlineMs ?? 30_000);
868
+ await withDeadline(this.#start(record), this.#deps.deadlineMs ?? 60_000);
852
869
  try {
853
870
  await this.#request(record.subagentId, "prompt", { message: cleanTask });
854
871
  }
@@ -904,17 +921,36 @@ export class SubagentManager {
904
921
  };
905
922
  }
906
923
  catch (error) {
907
- if (childId !== undefined &&
908
- !this.#runtimeStarted(childId) &&
909
- !this.#unresolvedRuntimes.has(childId)) {
910
- this.#working.delete(childId);
911
- }
912
- this.#notify();
913
- return failure("spawn", errorMessage(error), undefined, undefined, [], true);
924
+ if (childId === undefined)
925
+ return failure("spawn", errorMessage(error));
926
+ const failedId = childId;
927
+ const reason = errorMessage(error);
928
+ const failed = await this.#mutate(failedId, async () => {
929
+ const record = await this.#deps.registry.get(failedId);
930
+ const uncertain = this.#runtimeStarted(failedId) ||
931
+ this.#unresolvedRuntimes.has(failedId) ||
932
+ record?.runtime !== undefined;
933
+ if (uncertain)
934
+ this.#unresolvedRuntimes.add(failedId);
935
+ if (!this.#isLive(failedId))
936
+ this.#working.delete(failedId);
937
+ await this.#update(failedId, (current) => ({
938
+ ...current,
939
+ state: "error",
940
+ interrupted: reason,
941
+ }));
942
+ this.#projectors.get(failedId)?.syncState("error");
943
+ return failure("spawn", reason, failedId, "error", [
944
+ "registry record persisted",
945
+ ...(uncertain ? ["runtime start or cleanup unconfirmed"] : ["no runtime remains"]),
946
+ ], !uncertain);
947
+ });
948
+ return failed;
914
949
  }
915
950
  }
916
951
  send(id, rawMessage, mode = "auto", signal) {
917
952
  const message = stripHindsightContent(rawMessage);
953
+ this.#bumpActivity(id);
918
954
  this.#clearSettledPanelClose(id);
919
955
  return this.#mutate(id, async () => {
920
956
  const record = await this.#deps.registry.get(id);
@@ -958,7 +994,7 @@ export class SubagentManager {
958
994
  this.#projectors.set(id, createStateProjector(resumedRecord.state));
959
995
  }
960
996
  try {
961
- await withDeadline(this.#start(resumedRecord), this.#deps.deadlineMs ?? 30_000);
997
+ await withDeadline(this.#start(resumedRecord), this.#deps.deadlineMs ?? 60_000);
962
998
  isResumed = true;
963
999
  }
964
1000
  catch (error) {
package/dist/runtime.js CHANGED
@@ -167,6 +167,10 @@ export async function launchChild(options) {
167
167
  };
168
168
  }
169
169
  options.tokens.forget(plan.runtimeIdentity);
170
+ await options.registry.update(record.subagentId, undefined, (current) => {
171
+ const { runtime: _runtime, ...rest } = current;
172
+ return rest;
173
+ }, plan.runtimeIdentity);
170
174
  return { failure: `Child ${record.subagentId} never connected to the parent bridge` };
171
175
  }
172
176
  /**
@@ -214,6 +218,10 @@ export async function openChildPanel(options) {
214
218
  };
215
219
  }
216
220
  options.tokens.forget(plan.runtimeIdentity);
221
+ await options.registry.update(record.subagentId, undefined, (current) => {
222
+ const { runtime: _runtime, ...rest } = current;
223
+ return rest;
224
+ }, plan.runtimeIdentity);
217
225
  return { failure: `Child ${record.subagentId} never connected to the parent bridge` };
218
226
  }
219
227
  /** Creates the runtime directory if needed; the socket itself is 0600 in a 0700 directory. */
package/dist/widget.js CHANGED
@@ -54,7 +54,7 @@ export function renderSubagentWidget(children, width, theme, nowMs = Date.now())
54
54
  const glyphStyled = theme.fg(tone, glyph);
55
55
  const idStyled = theme.fg("dim", `#${child.id}`);
56
56
  const nameStyled = theme.fg("text", child.displayName ?? child.agent);
57
- const elapsed = formatElapsed(child.createdAt, nowMs);
57
+ const elapsed = formatElapsed(child.createdAt, isTerminalState(child.state) ? Date.parse(child.updatedAt) : nowMs);
58
58
  const timeStyled = theme.fg("dim", elapsed === "" ? "0s" : elapsed);
59
59
  const stateStyled = theme.fg(tone, visual);
60
60
  const turnsCount = child.usage?.turns ?? 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hheei/pi-subagents",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Independent background Pi subagent sessions with durable identity and RPC control.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -43,8 +43,8 @@
43
43
  "typebox": "*"
44
44
  },
45
45
  "devDependencies": {
46
- "@earendil-works/pi-coding-agent": "1.0.0",
47
- "@earendil-works/pi-tui": "1.0.0"
46
+ "@earendil-works/pi-coding-agent": "1.0.2",
47
+ "@earendil-works/pi-tui": "1.0.2"
48
48
  },
49
49
  "scripts": {
50
50
  "build": "node -e \"import { rmSync } from 'node:fs'; rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.build.json"