pi-crew 0.11.11 → 0.11.13

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 (50) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/dist/index.mjs +551 -519
  3. package/package.json +1 -1
  4. package/src/config/config-validation.ts +9 -0
  5. package/src/config/defaults.ts +7 -0
  6. package/src/config/env-vars.ts +24 -0
  7. package/src/config/types.ts +11 -0
  8. package/src/extension/registration/tool-registration.ts +7 -0
  9. package/src/extension/registration/verify-gate-tool.ts +263 -0
  10. package/src/extension/team-tool/run-deadline.ts +22 -1
  11. package/src/extension/team-tool/run.ts +38 -2
  12. package/src/extension/team-tool-types.ts +4 -0
  13. package/src/extension/team-tool.ts +31 -3
  14. package/src/prompt/inbox-poll.ts +141 -3
  15. package/src/runtime/broker/crew-broker.ts +48 -2
  16. package/src/runtime/broker/protocol/lock-busy.ts +47 -1
  17. package/src/runtime/broker/protocol/request-parsers.ts +8 -1
  18. package/src/runtime/budget-enforcement.ts +36 -20
  19. package/src/runtime/child-pi/child-pi-constants.ts +24 -0
  20. package/src/runtime/child-pi/child-pi-kill.ts +59 -2
  21. package/src/runtime/child-pi/child-pi-rpc.ts +1021 -0
  22. package/src/runtime/child-pi/child-pi-steering.ts +10 -8
  23. package/src/runtime/child-pi/child-pi-streams.ts +69 -19
  24. package/src/runtime/child-pi/child-pi.ts +29 -30
  25. package/src/runtime/child-pi/owned-process.ts +434 -0
  26. package/src/runtime/child-pi/postmortem-registry.ts +134 -0
  27. package/src/runtime/classifier/ambient-noise-gate.ts +126 -0
  28. package/src/runtime/classifier/classifier-service.ts +277 -61
  29. package/src/runtime/dispatch-batch.ts +32 -0
  30. package/src/runtime/live-session/live-agent-control.ts +2 -0
  31. package/src/runtime/live-session/live-agent-manager.ts +72 -2
  32. package/src/runtime/live-session/live-session-runtime.ts +435 -40
  33. package/src/runtime/merge-loop.ts +63 -35
  34. package/src/runtime/recovery/crash-recovery.ts +141 -3
  35. package/src/runtime/recovery/retry-executor.ts +29 -0
  36. package/src/runtime/run-tracker.ts +28 -0
  37. package/src/runtime/stale-reconciler.ts +36 -1
  38. package/src/runtime/task-runner.ts +103 -0
  39. package/src/runtime/verification/classifier-pre-gate.ts +262 -0
  40. package/src/runtime/verification/verification-gates.ts +5 -1
  41. package/src/runtime/verification/verify-gate.ts +394 -0
  42. package/src/schema/config-schema.ts +13 -0
  43. package/src/state/contracts.ts +18 -0
  44. package/src/state/coordination/mailbox.ts +203 -55
  45. package/src/state/stores/sqlite-run-state.ts +378 -0
  46. package/src/state/stores/state-store.ts +134 -6
  47. package/src/ui/agents-jobs-browser.ts +154 -8
  48. package/src/ui/events-state-source.ts +358 -0
  49. package/src/ui/run-snapshot-cache.ts +87 -3
  50. package/src/ui/widget/widget-model.ts +13 -2
@@ -21,6 +21,8 @@ import type { TaskAttemptState, TeamRunManifest, TeamTaskState } from "../state/
21
21
  import { logInternalError } from "../utils/internal-error.ts";
22
22
  import type { WorkflowConfig, WorkflowStep } from "../workflows/workflow-config.ts";
23
23
  import { isDelegateShadowTask } from "./broker/delegate/shadow-lifecycle.ts";
24
+ import { classifyAmbientNoiseGate } from "./classifier/ambient-noise-gate.ts";
25
+ import { resolveClassifierEnabled, resolveClassifierModel } from "./classifier/classifier-service.ts";
24
26
  import { readCrewAgents, recordFromTask, saveCrewAgents } from "./crew-agent-records.ts";
25
27
  import { appendDeadletter } from "./deadletter.ts";
26
28
  import { classifyHeartbeat, DEFAULT_GRADIENT_THRESHOLDS } from "./heartbeat/heartbeat-gradient.ts";
@@ -764,6 +766,17 @@ export async function dispatchBatch(ctx: SchedulerContext, decision: DispatchBat
764
766
  // so the counter accumulates across retry attempts × model fallbacks.
765
767
  const policy = retryPolicyFromConfig(input.reliability);
766
768
  const spawnBudget: SpawnBudget = { count: 0, max: policy.maxTotalSpawns ?? 0 };
769
+ // U6A (spec 2026-10-09 §U6 Phase A): ambient-noise vs real-failure
770
+ // classifier gate on the retry loop. Dormant unless
771
+ // runtime.classifierEnabled (default FALSE — same flag as child-executor's
772
+ // retry-triage; env PI_CREW_CLASSIFIER_ENABLED beats config). Disabled
773
+ // path passes NO hook at all: zero behavior change, zero registry touch.
774
+ // Every classifier failure mode (opencode unconfigured, classify throw,
775
+ // stopReason error) falls back inside classifyAmbientNoiseGate to
776
+ // proceed-with-retry — the gate can only SKIP a queued retry, never fail
777
+ // the task with a new error.
778
+ const noiseGateEnabled = resolveClassifierEnabled(input.runtimeConfig?.classifierEnabled);
779
+ const noiseGateModel = resolveClassifierModel(input.runtimeConfig?.classifierModel);
767
780
  const baseInput = {
768
781
  manifest: ctx.manifest,
769
782
  tasks: ctx.tasks,
@@ -890,6 +903,25 @@ export async function dispatchBatch(ctx: SchedulerContext, decision: DispatchBat
890
903
  policy,
891
904
  {
892
905
  signal: runController.signal,
906
+ // U6A: only consulted between attempts (never on attempt 1) and only
907
+ // when the gate is enabled — see noiseGateEnabled note above. The
908
+ // gate never rejects (soft-fail inside classifyAmbientNoiseGate and
909
+ // inside executeWithRetry's shouldRetry guard).
910
+ shouldRetry: noiseGateEnabled
911
+ ? (attempt, error) =>
912
+ classifyAmbientNoiseGate({
913
+ enabled: true,
914
+ modelRegistry: input.modelRegistry,
915
+ classifierModel: noiseGateModel,
916
+ failureSummary: error.message,
917
+ attempt,
918
+ maxAttempts: policy.maxAttempts,
919
+ eventsPath: ctx.manifest.eventsPath,
920
+ runId: ctx.manifest.runId,
921
+ taskId: task.id,
922
+ metricRegistry: input.metricRegistry,
923
+ }).then((gate) => gate.proceedWithRetry)
924
+ : undefined,
893
925
  attemptId: (attempt) => `${ctx.manifest.runId}:${task.id}:attempt-${attempt}`,
894
926
  onAttemptFailed: (attempt, error, delayMs, info) => {
895
927
  lastAttemptId = info.attemptId;
@@ -101,11 +101,13 @@ export async function applyLiveAgentControlRequest(input: {
101
101
  await session.prompt?.(request.message ?? "Please continue with the follow-up request.", {
102
102
  source: "api",
103
103
  expandPromptTemplates: false,
104
+ streamingBehavior: "followUp",
104
105
  });
105
106
  else if (request.operation === "resume")
106
107
  await session.prompt?.(request.message ?? "Please resume and report final status.", {
107
108
  source: "api",
108
109
  expandPromptTemplates: false,
110
+ streamingBehavior: "followUp",
109
111
  });
110
112
  else if (request.operation === "stop") await session.abort?.();
111
113
  return true;
@@ -62,6 +62,14 @@ export interface LiveAgentHandle {
62
62
  status: CrewAgentRecord["status"];
63
63
  pendingSteers: string[];
64
64
  pendingFollowUps: string[];
65
+ /**
66
+ * U4 dispose normalization: once the underlying session has been disposed
67
+ * (task finished / hung-tool escape / terminate), the handle is TERMINAL —
68
+ * steer/followUp/resume are rejected clearly instead of silently queueing
69
+ * messages into a dead session (the old asymmetric-dispose bug) or letting
70
+ * registerLiveAgent replay pending queues into it.
71
+ */
72
+ terminated?: boolean;
65
73
  /** Phase 7: Pending IRC messages for this agent. */
66
74
  pendingMessages: IrcMessage[];
67
75
  /** G1-G6: Real-time activity tracking (in-memory only). */
@@ -100,6 +108,11 @@ export function registerLiveAgent(
100
108
  pendingSteers: existing?.pendingSteers ?? [],
101
109
  pendingFollowUps: existing?.pendingFollowUps ?? [],
102
110
  pendingMessages: existing?.pendingMessages ?? [],
111
+ // U4: a re-register with a FRESH session revives the agentId (e.g. the
112
+ // D4 planning→execute handoff: a mock/placeholder handle registered first,
113
+ // the real session later). Any pending steer/followUp queued against the
114
+ // earlier handle below is routed to the NEW session on replay.
115
+ terminated: false,
103
116
  activity: existing?.activity ?? {
104
117
  activeTools: new Map(),
105
118
  toolUses: 0,
@@ -161,9 +174,13 @@ export function registerLiveAgent(
161
174
  handle.pendingFollowUps.length = 0;
162
175
  for (const message of pending)
163
176
  void handle.session
177
+ // U4: streamingBehavior is MANDATORY on every prompt() — without it a
178
+ // prompt that lands while the session is already streaming throws
179
+ // "Agent is already processing" (pi agent-session.js:1548).
164
180
  .prompt(message, {
165
181
  source: "api",
166
182
  expandPromptTemplates: false,
183
+ streamingBehavior: "followUp",
167
184
  })
168
185
  .catch((error) => logInternalError("live-agent-manager.prompt", error, `agentId=${handle.agentId}`));
169
186
  }
@@ -217,10 +234,49 @@ async function killLiveSessionProcessIfAlive(handle: LiveAgentHandle): Promise<v
217
234
  }
218
235
  }
219
236
 
237
+ /**
238
+ * U4 dispose normalization: dispose the underlying session AND make the
239
+ * handle terminal — drain any queued pendingSteers/pendingFollowUps (they can
240
+ * never be delivered to a disposed session) and set `terminated` so later
241
+ * steer/followUp calls are rejected clearly instead of queueing into a dead
242
+ * session. The handle itself stays in the registry for status/health/UI.
243
+ */
220
244
  export function disposeLiveAgentSession(agentIdOrTaskId: string): void {
221
245
  const handle = getLiveAgent(agentIdOrTaskId);
222
246
  if (!handle) return;
247
+ markLiveAgentHandleTerminated(handle);
248
+ safeDisposeLiveSession(handle);
249
+ }
250
+
251
+ /** Drain pending queues and mark a handle terminal (idempotent). */
252
+ function markLiveAgentHandleTerminated(handle: LiveAgentHandle): void {
253
+ // Drain: queued messages must not replay into the disposed session later
254
+ // (registerLiveAgent preserves pending* across re-registrations).
255
+ handle.pendingSteers.length = 0;
256
+ handle.pendingFollowUps.length = 0;
257
+ handle.terminated = true;
258
+ handle.updatedAt = new Date().toISOString();
259
+ invalidateSortedLiveAgents();
260
+ }
261
+
262
+ /**
263
+ * U4 hung-tool escape hatch — drop-reference.
264
+ *
265
+ * Synchronous last resort for a session whose prompt() will never settle
266
+ * (tool/provider holds the turn and ignores AbortSignal): terminate the
267
+ * handle and REMOVE it from the registry WITHOUT awaiting abort().
268
+ * terminateLiveAgent's `await session.abort()` would itself hang on such a
269
+ * session; here only synchronous cleanup runs (drain → mark → dispose →
270
+ * unregister). Any background abort that eventually settles is harmless.
271
+ */
272
+ export function dropLiveAgentReference(agentIdOrTaskId: string): LiveAgentHandle | undefined {
273
+ const handle = getLiveAgent(agentIdOrTaskId);
274
+ if (!handle) return undefined;
275
+ markLiveAgentHandleTerminated(handle);
223
276
  safeDisposeLiveSession(handle);
277
+ liveAgents.delete(handle.agentId);
278
+ invalidateSortedLiveAgents();
279
+ return handle;
224
280
  }
225
281
 
226
282
  export async function terminateLiveAgent(
@@ -254,6 +310,7 @@ export async function terminateLiveAgent(
254
310
  try {
255
311
  await handle.session.abort?.();
256
312
  } finally {
313
+ markLiveAgentHandleTerminated(handle);
257
314
  safeDisposeLiveSession(handle);
258
315
  liveAgents.delete(handle.agentId); // Move AFTER abort completes to prevent race
259
316
  invalidateSortedLiveAgents();
@@ -377,6 +434,10 @@ export function getLiveAgentContextPercent(agentIdOrTaskId: string): number | nu
377
434
  export async function steerLiveAgent(agentIdOrTaskId: string, message: string): Promise<LiveAgentHandle> {
378
435
  const handle = getLiveAgent(agentIdOrTaskId);
379
436
  if (!handle) throw new Error(`Live agent '${agentIdOrTaskId}' is not registered in this process.`);
437
+ // U4 dispose normalization: explicit rejection — never silently queue into
438
+ // (or double-deliver via replay of) a disposed session.
439
+ if (handle.terminated)
440
+ throw new Error(`Live agent '${agentIdOrTaskId}' session was disposed (terminated); steer is no longer accepted.`);
380
441
  if (typeof handle.session.steer !== "function") {
381
442
  handle.pendingSteers.push(message);
382
443
  return handle;
@@ -390,6 +451,8 @@ export async function steerLiveAgent(agentIdOrTaskId: string, message: string):
390
451
  export async function followUpLiveAgent(agentIdOrTaskId: string, prompt: string): Promise<LiveAgentHandle> {
391
452
  const handle = getLiveAgent(agentIdOrTaskId);
392
453
  if (!handle) throw new Error(`Live agent '${agentIdOrTaskId}' is not registered in this process.`);
454
+ if (handle.terminated)
455
+ throw new Error(`Live agent '${agentIdOrTaskId}' session was disposed (terminated); follow-up is no longer accepted.`);
393
456
  if (typeof handle.session.prompt !== "function") {
394
457
  handle.pendingFollowUps.push(prompt);
395
458
  return handle;
@@ -397,6 +460,8 @@ export async function followUpLiveAgent(agentIdOrTaskId: string, prompt: string)
397
460
  await handle.session.prompt(prompt, {
398
461
  source: "api",
399
462
  expandPromptTemplates: false,
463
+ // U4: mandatory streamingBehavior — see registerLiveAgent replay note.
464
+ streamingBehavior: "followUp",
400
465
  });
401
466
  handle.updatedAt = new Date().toISOString();
402
467
  invalidateSortedLiveAgents();
@@ -412,11 +477,15 @@ export async function stopLiveAgent(agentIdOrTaskId: string): Promise<LiveAgentH
412
477
  export async function resumeLiveAgent(agentIdOrTaskId: string, prompt: string): Promise<LiveAgentHandle> {
413
478
  const handle = getLiveAgent(agentIdOrTaskId);
414
479
  if (!handle) throw new Error(`Live agent '${agentIdOrTaskId}' is not registered in this process.`);
480
+ if (handle.terminated)
481
+ throw new Error(`Live agent '${agentIdOrTaskId}' session was disposed (terminated); resume requires a fresh session.`);
415
482
  if (typeof handle.session.prompt !== "function") throw new Error(`Live agent '${agentIdOrTaskId}' does not expose prompt().`);
416
483
  handle.status = "running";
417
484
  await handle.session.prompt(prompt, {
418
485
  source: "api",
419
486
  expandPromptTemplates: false,
487
+ // U4: mandatory streamingBehavior — see registerLiveAgent replay note.
488
+ streamingBehavior: "followUp",
420
489
  });
421
490
  handle.status = "completed";
422
491
  handle.updatedAt = new Date().toISOString();
@@ -514,7 +583,7 @@ export function sendIrcMessage(targetAgentId: string, message: IrcMessage): void
514
583
  if (typeof handle.session.prompt === "function") {
515
584
  const ircPrompt = `[Message from ${message.from}] ${message.content}`;
516
585
  void handle.session
517
- .prompt(ircPrompt, { source: "api", expandPromptTemplates: false })
586
+ .prompt(ircPrompt, { source: "api", expandPromptTemplates: false, streamingBehavior: "followUp" })
518
587
  .catch((error) => logInternalError("live-agent-manager.irc-deliver", error, `agentId=${handle.agentId}`));
519
588
  }
520
589
  }
@@ -559,6 +628,7 @@ export function broadcastIrcMessage(fromAgentId: string, message: IrcMessage): s
559
628
  .prompt(ircPrompt, {
560
629
  source: "api",
561
630
  expandPromptTemplates: false,
631
+ streamingBehavior: "followUp",
562
632
  })
563
633
  .catch((error) => logInternalError("live-agent-manager.irc-broadcast", error, `agentId=${handle.agentId}`));
564
634
  }
@@ -657,7 +727,7 @@ export async function respondAsBackground(
657
727
  if (!delivered && typeof handle.session.prompt === "function") {
658
728
  const promptText = `${deliveredTag}${awaitReply ? ` (reply correlation: ${corrId})` : ""}`;
659
729
  void handle.session
660
- .prompt(promptText, { source: "api", expandPromptTemplates: false })
730
+ .prompt(promptText, { source: "api", expandPromptTemplates: false, streamingBehavior: "followUp" })
661
731
  .catch((error) => logInternalError("live-agent-manager.respondAsBackground", error, `agentId=${handle.agentId}`));
662
732
  delivered = true;
663
733
  }