pi-crew 0.11.55 → 0.11.56

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.
@@ -74,6 +74,7 @@ import { checkProcessLiveness } from "../process-status.ts";
74
74
  import { DEFAULT_RETRY_POLICY } from "../recovery/retry-executor.ts";
75
75
  import { runWorker } from "../run-worker.ts";
76
76
  import { parseSessionUsage, usageFromValue } from "../session-usage.ts";
77
+ import { createSpawnPhaseTracer } from "../spawn-phase-trace.ts";
77
78
  import { recordSupervisorContact, supervisorContactFromEvent } from "../supervisor-contact.ts";
78
79
  import type { ResultSource, TaskExecutionResult } from "./post-execution.ts";
79
80
  import type { StreamBridgeHandle, TaskExecutionContext } from "./pre-execution.ts";
@@ -827,7 +828,14 @@ export async function runChildProcessTask(ctx: TaskExecutionContext): Promise<Ta
827
828
  touch: () => persistHeartbeat(),
828
829
  });
829
830
  let childResult: ChildPiRunResult;
831
+ // spawn.trace (2026-11-01, run team_20261101074334 forensics): a
832
+ // 7-minute log-silent hang between task.started and worker.spawned
833
+ // left no bisectable trace. This tracer prints a phase breadcrumb every
834
+ // 30s ONLY while the pre-spawn/spawn window is stuck that long — healthy
835
+ // spawns stay silent.
836
+ const spawnTrace = createSpawnPhaseTracer(task.id, i);
830
837
  try {
838
+ spawnTrace.phase("runWorker-await");
831
839
  childResult = await runWorker({
832
840
  cwd: task.cwd,
833
841
  task: userPrompt,
@@ -878,6 +886,9 @@ export async function runChildProcessTask(ctx: TaskExecutionContext): Promise<Ta
878
886
  attempt: i,
879
887
  steeringFile: resolveRealContainedPath(`${manifest.artifactsRoot}/steering`, `${task.id}.jsonl`),
880
888
  onSpawn: (pid) => {
889
+ // spawn.trace: the worker process exists — pre-spawn window closed.
890
+ spawnTrace.phase("worker-running");
891
+ spawnTrace.end();
881
892
  // NEW-4/G25: pin this incarnation's pid FIRST so the liveness pulse
882
893
  // corroborates against the real process from its first tick on.
883
894
  attemptWorkerPid = pid;
@@ -1112,6 +1123,9 @@ export async function runChildProcessTask(ctx: TaskExecutionContext): Promise<Ta
1112
1123
  // heartbeat anymore, including across the model-fallback respawn gap
1113
1124
  // (where nothing is alive and the heartbeat must be allowed to age).
1114
1125
  heartbeatPulse.stop();
1126
+ // spawn.trace: attempt settled — always clear the tracer (onSpawn may
1127
+ // never fire for a failed pre-spawn attempt).
1128
+ spawnTrace.end();
1115
1129
  if (timeoutHandle) clearTimeout(timeoutHandle);
1116
1130
  // W2 fix — release the listener so it doesn't leak. {once:true}
1117
1131
  // only auto-removes when the listener FIRES; if the timeout
@@ -91,6 +91,38 @@ function mergeTaskUpdateWithDisk(diskTasks: TeamTaskState[], incoming: TeamTaskS
91
91
  },
92
92
  };
93
93
  }
94
+
95
+ // R6-5 WAITING-PRESERVE (FINDING D root fix, live probes 10/11/12,
96
+ // 2026-11-01): a runner-side snapshot (child-executor persistHeartbeat's
97
+ // ~1s whole-record persist is the caught clobberer — stack pinned via
98
+ // saveRunTasksCoalesced instrumentation, probe12 run
99
+ // team_20261101040510) carries a dispatch-time task object that NEVER
100
+ // learned the broker parked the task (the park is written disk-side by the
101
+ // broker's wait.request handler under the run lock; the runner's in-memory
102
+ // copy keeps status "running", no `waiting`). Without this guard the first
103
+ // post-park heartbeat persist ERASED the park (tasks.json → running,
104
+ // waiting:null), which broke respond.force's live-park gate ("no-park"
105
+ // while the worker was verifiably parked — probe10) — the FINDING A
106
+ // residual class, now closed at the merge chokepoint every
107
+ // persistSingleTaskUpdate caller funnels through.
108
+ //
109
+ // Rule (mirror of R6-1's "the verdict is not ours to overwrite"): when
110
+ // DISK holds a live park (`waiting` payload, non-terminal status) and the
111
+ // incoming snapshot does not itself carry park-awareness
112
+ // (waiting === undefined) and is NON-terminal, the disk park survives —
113
+ // the heartbeat was never authoritative over an ask park. Legitimate
114
+ // clears do NOT pass through here: respond.ts requeueWaitingTask and the
115
+ // broker's wait-resolve victim flip write FULL task arrays directly via
116
+ // saveRunTasks under the run lock (verified 2026-11-01), and terminal
117
+ // settles (post-execution) are let through by the non-terminal condition.
118
+ if (
119
+ diskTask.status === "waiting" &&
120
+ diskTask.waiting !== undefined &&
121
+ incoming.waiting === undefined &&
122
+ !isTerminalTaskStatus(effective.status)
123
+ ) {
124
+ effective = { ...effective, status: "waiting" as const, waiting: diskTask.waiting };
125
+ }
94
126
  return effective;
95
127
  }
96
128
 
@@ -267,6 +267,21 @@ export const TEAM_EVENT_TYPES = [
267
267
  "mailbox.send",
268
268
  "mailbox.pickup",
269
269
  "mailbox.pickup_deduped",
270
+ // HƯỚNG A (ADR Option 3, 2026-11-01): broker-side respond delivery audit —
271
+ // fired by crew-broker's respond.append (challenge-response) and
272
+ // respond.force (one-shot live-park) handlers when a leader response row
273
+ // is appended BY THE BROKER PROCESS (w1a: broker-signed rows verify in
274
+ // every worker). data: {taskId, questionId, auth, requestId, messageId,
275
+ // deduped} (+ tag/brokerSocket on the forced path). Diagnostic only —
276
+ // never transitions run/task state; the mailbox row is the source of truth.
277
+ "respond.broker_append",
278
+ // HƯỚNG A follow-up (probe19, 2026-11-01): broker-side forced-steer audit
279
+ // — fired by crew-broker's steer.force handler when a foreign/restarted
280
+ // session's steer is delivered BY THE BROKER PROCESS (mailbox row +
281
+ // broker-signed steering line; w1a: broker-signed rows verify in every
282
+ // worker). data: {taskId, auth:"forced"}. Diagnostic only — never
283
+ // transitions run/task state.
284
+ "steer.broker_force",
270
285
  // Recovery
271
286
  "recovery.rerun_task",
272
287
  // Run-level budget/effectiveness/export/lifecycle bookkeeping
@@ -230,6 +230,21 @@ export function signMailboxMessage(message: MailboxMessage): MailboxMessage & {
230
230
  return { ...message, sig: signPayloadHash(coordinationPayloadHash(canonicalMailboxPayload(row))) };
231
231
  }
232
232
 
233
+ /** HƯỚNG A (ADR Option 3, 2026-11-01): challenge-response proof-signing seam
234
+ * for broker `respond.append`. Signs ARBITRARY challenge bytes (nonce ∥
235
+ * canonical payload — NO sha256 wrap, unlike the row signers above; the
236
+ * broker verifies with node:crypto Ed25519 against its dispatch-time heap
237
+ * binding of the dispatcher PUBLIC key) with THIS process's coordination
238
+ * private key. Only the dispatcher process holds that key — the keypair
239
+ * whose public half it stamped as manifest.dispatcherVerifyKey at dispatch
240
+ * time — so only the dispatcher can produce a passing signature; workers
241
+ * and other processes hold at most the public verify key. Private key
242
+ * stays heap-only like every other signer here. */
243
+ export function signCoordinationChallenge(payload: string): string {
244
+ const { privateKey } = getMailboxSigningKeypair();
245
+ return ed25519Sign(null, Buffer.from(payload, "utf8"), privateKey).toString("base64");
246
+ }
247
+
233
248
  /** Steering-JSONL line shape shared by every writer (team-tool, broker
234
249
  * steer.push, child-executor seed, wrap-up advisories). */
235
250
  export interface CoordinationSteerEntry {
@@ -455,6 +455,17 @@ export interface TeamRunManifest {
455
455
  * models.json happens to list first.
456
456
  */
457
457
  modelContext?: RunModelContext;
458
+ /** HƯỚNG A (2026-11-01, probe8): the ACTUAL broker socket path of the
459
+ * run's host process, stamped by the background runner at stdin-creds
460
+ * accept time (async topology: the runner's socket is NOT derivable from
461
+ * this manifest's sessionId — that hash belongs to the dispatcher-side
462
+ * synthetic session; live probe derived 7e4df036 while the run's broker
463
+ * listened on 9529a08a). Cross-process responders (respond-delivery.ts)
464
+ * prefer this field over derivation. Absent on pre-Á-wave manifests and
465
+ * on runs whose runner received no stdin creds — responders fall back to
466
+ * derivation, then to the local signed append (hướng C). Not a secret:
467
+ * same-uid readable either way; connecting still requires auth. */
468
+ brokerSocketPath?: string;
458
469
  /** FINDING B (2026-10-31) — w1a coordination verify-key distribution: the
459
470
  * base64 SPKI DER PUBLIC verify key of the DISPATCHING session process
460
471
  * (the leader that created this run; stamped via