@ian-pascoe/pi-minimal-subagents 0.7.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -226,8 +226,14 @@ Deleting a child first verifies its session header and persistent identity,
226
226
  then uses the optional `trash` command when available and falls back to
227
227
  unlinking its session file. Deletion prunes pending delivery state and retained
228
228
  recent-message projections sourced from the complete deleted subtree. Restore
229
- and clone perform the same ownership check and reopen the recorded child-session
230
- leaf.
229
+ and clone perform the same ownership check against the recorded child-session
230
+ leaf. Restoration loads metadata and validates saved sessions without starting
231
+ child runtimes or their extension services (including MCP Servers). Status,
232
+ saved transcripts, settled-result waits, cancellation of idle children, and
233
+ session management do not start runtimes. A message or an undelivered child-bound
234
+ result opens only its recipient's runtime on demand; saved Delivery Evidence
235
+ prevents already-delivered work from reopening it. Runtime initialization errors
236
+ are reported when that child is first needed.
231
237
 
232
238
  Registry replay and Delivery Evidence are scoped to the Root Agent's active
233
239
  session-tree branch. Registry writes use V2 records with complete field,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ian-pascoe/pi-minimal-subagents",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "private": false,
5
5
  "description": "Persistent nested subagents with bounded delegation for Pi",
6
6
  "keywords": [
@@ -126,10 +126,11 @@ export function reconcileCoordinatorToolAccess(
126
126
  activeToolNames: readonly string[],
127
127
  enabled: boolean,
128
128
  ): string[] {
129
- const ordinaryToolNames = activeToolNames.filter(
130
- (toolName) => !COORDINATOR_TOOL_NAME_SET.has(toolName),
129
+ const missing = new Set<string>(COORDINATOR_TOOL_NAMES);
130
+ const retained = activeToolNames.filter(
131
+ (toolName) => !COORDINATOR_TOOL_NAME_SET.has(toolName) || (enabled && missing.delete(toolName)),
131
132
  );
132
- return enabled ? [...ordinaryToolNames, ...COORDINATOR_TOOL_NAMES] : ordinaryToolNames;
133
+ return enabled ? [...retained, ...missing] : retained;
133
134
  }
134
135
 
135
136
  /** Resolve branch state over settings and include read-only Coordinator Tool activation. */
@@ -655,19 +655,6 @@ export class MinimalSubagentsCoordinator {
655
655
  });
656
656
  continue;
657
657
  }
658
- const runtime = await this.dependencies.sessions.openRuntime(agent);
659
- if (restoreEpoch !== this.lifecycleEpoch) {
660
- runtime.dispose();
661
- return;
662
- }
663
- if (!runtime.sessionLeafId) {
664
- runtime.dispose();
665
- throw new Error(
666
- `Minimal subagents session restoration: no selected session leaf for ${agent.agent_id}`,
667
- );
668
- }
669
- this.runtimes.set(agent.agent_id, runtime);
670
- agent.session_leaf_id = runtime.sessionLeafId;
671
658
  agent.availability = "available";
672
659
  if (previousAvailability !== "available")
673
660
  agent.latest_activity_at = this.now().toISOString();
@@ -926,6 +913,7 @@ export class MinimalSubagentsCoordinator {
926
913
  new Error(agent.clone_error ?? `No persistent session exists for ${agent.agent_id}`),
927
914
  );
928
915
  }
916
+ const openingForTurn = agent.active_turn_id !== undefined;
929
917
  const initialization = this.dependencies.sessions
930
918
  .openRuntime(agent)
931
919
  .then((runtime) => {
@@ -933,9 +921,29 @@ export class MinimalSubagentsCoordinator {
933
921
  runtime.dispose();
934
922
  throw new Error(`Minimal subagents runtime replaced while opening ${agent.agent_id}`);
935
923
  }
924
+ if (!runtime.sessionLeafId) {
925
+ runtime.dispose();
926
+ throw new Error(
927
+ `Minimal subagents session restoration: no selected session leaf for ${agent.agent_id}`,
928
+ );
929
+ }
936
930
  this.runtimes.set(agent.agent_id, runtime);
931
+ agent.session_leaf_id = runtime.sessionLeafId;
937
932
  return runtime;
938
933
  })
934
+ .catch((error) => {
935
+ if (!openingForTurn && this.agents.get(agent.agent_id) === agent) {
936
+ agent.availability = "unavailable";
937
+ agent.latest_activity_at = this.now().toISOString();
938
+ agent.unavailable_reason = error instanceof Error ? error.message : String(error);
939
+ this.dependencies.notify?.({
940
+ type: "unavailable",
941
+ agentId: agent.agent_id,
942
+ message: `${agent.agent_id} unavailable: ${agent.unavailable_reason}`,
943
+ });
944
+ }
945
+ throw error;
946
+ })
939
947
  .finally(() => {
940
948
  if (this.runtimeInitializations.get(agent.agent_id) === initialization) {
941
949
  this.runtimeInitializations.delete(agent.agent_id);
@@ -1307,6 +1315,7 @@ export class MinimalSubagentsCoordinator {
1307
1315
  }
1308
1316
  const target = this.requireUsableAgent(targetId, "message");
1309
1317
  const runtime = this.runtimes.get(targetId) ?? (await this.ensureRuntime(target));
1318
+ this.assertAccepting();
1310
1319
  if (!isCurrentDelivery()) {
1311
1320
  throw new Error("Minimal subagents delivery abandoned after session branch change");
1312
1321
  }
@@ -1430,34 +1439,45 @@ export class MinimalSubagentsCoordinator {
1430
1439
  }
1431
1440
 
1432
1441
  private hasDeliveryEvidence(delivery: PersistedDelivery): boolean {
1433
- if (delivery.destination_agent_id === "root") {
1434
- return this.dependencies.root.hasDeliveryEvidence(
1435
- delivery.source_agent_id,
1436
- delivery.source_turn_id,
1437
- );
1438
- }
1439
- return (
1440
- this.runtimes
1441
- .get(delivery.destination_agent_id)
1442
- ?.hasDeliveryEvidence(delivery.source_agent_id, delivery.source_turn_id) ?? false
1442
+ return this.hasRecipientDeliveryEvidence(
1443
+ delivery.destination_agent_id,
1444
+ delivery.source_agent_id,
1445
+ delivery.source_turn_id,
1443
1446
  );
1444
1447
  }
1445
1448
 
1446
1449
  private hasCoordinationDeliveryEvidence(delivery: PersistedCoordinationDelivery): boolean {
1447
- const sourceAgentId = delivery.message.details.source_agent_id;
1448
- const sourceTurnId = delivery.message.details.source_turn_id;
1449
- if (delivery.destination_agent_id === "root") {
1450
- return this.dependencies.root.hasDeliveryEvidence(
1451
- sourceAgentId,
1452
- sourceTurnId,
1453
- delivery.delivery_id,
1450
+ return this.hasRecipientDeliveryEvidence(
1451
+ delivery.destination_agent_id,
1452
+ delivery.message.details.source_agent_id,
1453
+ delivery.message.details.source_turn_id,
1454
+ delivery.delivery_id,
1455
+ );
1456
+ }
1457
+
1458
+ private hasRecipientDeliveryEvidence(
1459
+ targetId: string,
1460
+ sourceAgentId: string,
1461
+ sourceTurnId: string,
1462
+ deliveryId?: string,
1463
+ ): boolean {
1464
+ const endpoint = targetId === "root" ? this.dependencies.root : this.runtimes.get(targetId);
1465
+ if (endpoint) return endpoint.hasDeliveryEvidence(sourceAgentId, sourceTurnId, deliveryId);
1466
+ const agent = this.agents.get(targetId);
1467
+ if (!agent) return false;
1468
+ try {
1469
+ return (
1470
+ this.dependencies.sessions.hasDeliveryEvidence?.(
1471
+ agent,
1472
+ sourceAgentId,
1473
+ sourceTurnId,
1474
+ deliveryId,
1475
+ ) ?? false
1454
1476
  );
1477
+ } catch {
1478
+ // Unverified sessions cannot prove delivery; runtime opening reports their failure.
1479
+ return false;
1455
1480
  }
1456
- return (
1457
- this.runtimes
1458
- .get(delivery.destination_agent_id)
1459
- ?.hasDeliveryEvidence(sourceAgentId, sourceTurnId, delivery.delivery_id) ?? false
1460
- );
1461
1481
  }
1462
1482
 
1463
1483
  private settleDelivery(delivery: PersistedDelivery): void {
@@ -1518,7 +1538,9 @@ export class MinimalSubagentsCoordinator {
1518
1538
  capability_ceiling: [...agent.capability_ceiling],
1519
1539
  spawn_entry_id: agent.spawn_entry_id,
1520
1540
  recent_messages: structuredClone(agent.recent_messages),
1521
- recent_activity: buildRecentAgentActivity(runtime?.snapshotActivityMessages() ?? []),
1541
+ recent_activity: buildRecentAgentActivity(
1542
+ runtime?.snapshotActivityMessages() ?? this.inspectTranscript(agent.agent_id).messages,
1543
+ ),
1522
1544
  latest_result: agent.latest_result ? structuredClone(agent.latest_result) : undefined,
1523
1545
  missing_dependencies: [...agent.missing_dependencies],
1524
1546
  unavailable_reason: agent.unavailable_reason,
@@ -157,7 +157,7 @@ export function createRegistryEvent(
157
157
  };
158
158
  }
159
159
  case "agent-created":
160
- return { ...envelope, event, agent: data.agent };
160
+ return { ...envelope, event, agent: structuredClone(data.agent) };
161
161
  case "turn-started":
162
162
  return {
163
163
  ...envelope,
@@ -826,24 +826,49 @@ export class PiAgentSessionFactory implements AgentSessionFactory {
826
826
  stat.ctimeMs,
827
827
  ]);
828
828
  if (this.savedTranscript?.key === key) return this.savedTranscript.snapshot;
829
+ const manager = this.readSession(agent);
830
+ const snapshot = selectChildAgentTranscript(
831
+ manager.getBranch(agent.session_leaf_id).flatMap(sessionEntryToContextMessages),
832
+ );
833
+ this.savedTranscript = { key, snapshot };
834
+ return snapshot;
835
+ }
836
+
837
+ /** Inspect durable evidence without starting child extensions or changing the saved branch. */
838
+ hasDeliveryEvidence(
839
+ agent: PersistedAgent,
840
+ sourceAgentId: string,
841
+ sourceTurnId: string,
842
+ deliveryId?: string,
843
+ ): boolean {
844
+ return findDeliveryEvidence(
845
+ this.readSession(agent).getBranch(agent.session_leaf_id),
846
+ sourceAgentId,
847
+ sourceTurnId,
848
+ deliveryId,
849
+ );
850
+ }
851
+
852
+ private readSession(agent: PersistedAgent): SessionManager {
853
+ if (!agent.session_file || !agent.session_id || !agent.session_leaf_id) {
854
+ throw new Error(`Child Session Position is unavailable for ${agent.agent_id}.`);
855
+ }
856
+ const sessionFile = canonicalPath(agent.session_file);
829
857
  const entries = parseSessionEntries(readFileSync(sessionFile, "utf8"));
830
858
  if (entries[0]?.type !== "session")
831
859
  throw new Error(`Invalid child session file: ${sessionFile}`);
832
860
  // SessionManager.open can migrate/rewrite files; an in-memory reader cannot write them.
833
861
  const manager = SessionManager.inMemory(this.options.cwd, undefined, entries);
834
862
  verifyChildSessionIdentity(manager, agent, this.options.rootSessionId);
835
- const snapshot = selectChildAgentTranscript(
836
- manager.getBranch(agent.session_leaf_id).flatMap(sessionEntryToContextMessages),
837
- );
838
- this.savedTranscript = { key, snapshot };
839
- return snapshot;
863
+ return manager;
840
864
  }
841
865
 
842
866
  resolveLaunchMissingDependencies(agent: PersistedAgent): Promise<string[]> {
843
867
  return this.findMissingDependencies(agent, false);
844
868
  }
845
869
 
846
- resolveRestorationMissingDependencies(agent: PersistedAgent): Promise<string[]> {
870
+ async resolveRestorationMissingDependencies(agent: PersistedAgent): Promise<string[]> {
871
+ this.readSession(agent);
847
872
  return this.findMissingDependencies(agent, this.options.modelScopeRestricted);
848
873
  }
849
874
 
@@ -261,6 +261,13 @@ export interface AgentSessionFactory {
261
261
  openRuntime(agent: PersistedAgent): Promise<ChildAgentRuntime>;
262
262
  /** Read verified saved history independently of runtime restoration dependencies. */
263
263
  readTranscript?(agent: PersistedAgent): ChildAgentTranscriptSnapshot;
264
+ /** Read verified selected-branch evidence without opening a runtime. */
265
+ hasDeliveryEvidence?(
266
+ agent: PersistedAgent,
267
+ sourceAgentId: string,
268
+ sourceTurnId: string,
269
+ deliveryId?: string,
270
+ ): boolean;
264
271
  resolveLaunchMissingDependencies(agent: PersistedAgent): Promise<string[]>;
265
272
  resolveRestorationMissingDependencies(agent: PersistedAgent): Promise<string[]>;
266
273
  resolveThinkingLevel(modelId: string, requested: ThinkingLevel): ThinkingLevel;