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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,48 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.11.56] — hướng A broker-signed respond + FINDING D runner-hosted broker + R6-5 + steer.force + spawn-trace (2026-11-01)
4
+
5
+ Wave theme: **the broker-host (detached runner) becomes the trust anchor of async runs** — the dispatcher dies, the broker lives; the signer is foreign, the broker signs. Four commits, every layer live-proven.
6
+
7
+ ## [Unreleased] — hướng A: broker-signed respond delivery (ADR Option 3)
8
+
9
+ **Added (product, HƯỚNG A — broker-appended respond rows).** The 0.11.55 dispatcher-key distribution (hướng C) still left every responder that is NOT the
10
+ dispatch process undeliverable: a restarted leader, a foreign session, a scheduler host sign rows with keypairs no worker holds, so their rows drop as
11
+ noise at the w1a verify. Hướng A closes the class by having the BROKER PROCESS append the row — the embedded broker's keypair IS the worker's spawner
12
+ keypair in both dispatch modes (`lifecycle-handlers.ts` getOrStartBroker), so a broker-appended row verifies in every worker with zero new key
13
+ distribution. Three surfaces:
14
+
15
+ - `respond.challenge` + `respond.append` (broker, challenge-response): the broker binds the DISPATCHER'S PUBLIC verify key heap-only at construct
16
+ (`CrewBrokerOptions.dispatcherVerifyKey` — async dispatch threads the spawner's key over the detached runner's stdin start-options into the new
17
+ `dispatcher-key-registry.ts`; a root dispatcher host binds its own key; NEVER the manifest file, which is unauthenticated and worker-writable — w1a
18
+ NO-GO). `respond.challenge` issues a 32-byte single-use nonce bound to (connection, runId) — burned on the first append attempt regardless of outcome,
19
+ rate-bounded 16/connection, TTL 30s upper-bounded by the broker's 1s hello deadline; `respond.append` verifies an Ed25519 signature over the frozen
20
+ `respondAppendSignInput` export (`${nonce}${canonical}` — no separator, no runId in the canonical; run binding rides the nonce record; client signers
21
+ must use the export, never re-implement) and appends the row itself: `from:"leader"`, `kind:"response"`, questionId-tagged, `requestId = questionId`
22
+ idempotency, signed with the broker host keypair. Worker-token connections are rejected on both respond paths (role derives from token type —
23
+ defense-in-depth; the real bound is the private key they cannot hold).
24
+ - `respond.force` (broker, one-shot live park): token-less human-responsibility path, called directly over the broker socket (NOT wired into the respond
25
+ tool's `force` param — its semantics are unchanged). The gate is the park itself (fresh reload under the bounded busy-retry run lock must find
26
+ `status:"waiting"` with the exact `waiting.questionId`), exactly-one per question (durable requestId dedup + heap fast-fail; force after a CR append
27
+ returns `already-answered`), row `from:"leader-forced"` + `data.tag:"forced"`, append OUTSIDE the run lock (lock-ordering safety; exactly-once comes
28
+ from the idempotent append, not the lock). This is the path that finally DELIVERS a foreign-session force respond — the gap hướng C could not close.
29
+ - `deliverResponseRow` (`src/extension/team-tool/respond-delivery.ts`, wired into `handleRespond`'s ALIVE path): broker-first delivery — precheck =
30
+ socket derivable from `manifest.sessionId` AND this process IS the dispatcher (`manifest.dispatcherVerifyKey === getMailboxVerifyKeyPublic()`;
31
+ foreign/restarted responders skip straight to the fallback instead of burning an RPC that cannot verify), one short-lived pre-hello NDJSON connection
32
+ (challenge→append on one socket — the nonce is per-connection; 2.5s connect / 5s per-request deadlines; no retries — the fallback IS the retry), ANY
33
+ failure → local signed idempotent append (hướng C behavior + `respond.broker-delivery-fallback` warn). `handleRespond` now stages ALIVE rows inside the
34
+ run lock (atomic with the exactly-one-dispatch scan) and delivers them after it releases — the broker handler takes its own run lock, so the RPC window
35
+ must never sit under `withRunLockSync`. `requestId = questionId` keeps delivery exactly-once across broker + local + concurrent responds (a lost ack
36
+ frame dedupes against the broker's own row). New additive mailbox.ts export `signCoordinationChallenge` (raw Ed25519 over arbitrary challenge bytes
37
+ with the in-process coordination keypair — no sha256 wrap, unlike the row signers).
38
+
39
+ New event `respond.broker_append` (registered in contracts.ts): audit for every broker-appended response row — data {taskId, questionId, auth:
40
+ "challenge-response"|"forced", requestId, messageId, deduped} (+ tag/brokerSocket on the forced path); diagnostic only, never transitions run/task
41
+ state. Known shape note: broker wire rows carry `replyTo` but not `replyFrom`/`replyDeadline` (informational fields — the local fallback row keeps
42
+ them; documented in respond-delivery.ts). Residuals documented in trust-model v1.5: leader restart mid-run still open (needs a dispatcher re-bind
43
+ flow), broker restart mid-run mints a new heap keypair (documented, not fixed), force forge-channel consciously accepted (one live park, one-shot,
44
+ tagged, audited), CR closes the no-leader gap only while the dispatcher process lives.
45
+
3
46
  ## [0.11.55] — ask-park clobber fix + w1a dispatcher key distribution (2026-10-31)
4
47
 
5
48
  **Fixed (product, FINDING A — ask-park clobber).** Cross-process coalesce race on tasks.json: the HOST process's 50ms coalesced
@@ -9505,3 +9548,96 @@ the repo. Two genuine wins, plus an informational CI job that never blocks.
9505
9548
 
9506
9549
  ### Hardening (W10 batch A)
9507
9550
  - `check-slop-rules` no-double-cast baseline ratchet (41, 2026-10-22 — burn down only) + real-CLI e2e pin; W10-7 reconcileUsage missing-finalUsage guard; W10-8 READ-ONLY lane mutation one-line stderr visibility; W10-9 structuredOutput JSON round-trip pin; W10-2 verify-gate memo time-dependent-skip intent pin.
9551
+
9552
+ ### Leader post-review additions (live probe8/9, 2026-11-01)
9553
+
9554
+ - **Discovery fix**: the background runner now stamps the ACTUAL broker socket path onto
9555
+ `TeamRunManifest.brokerSocketPath` at stdin-creds accept; `deliverResponseRow` prefers it over
9556
+ `getBrokerSocketPath(manifest.sessionId)` derivation — live probe proved the derivation hashes the
9557
+ dispatcher-side synthetic session (7e4df036) while the run's broker listens on a different path
9558
+ (9529a08a/de784999), so the RPC path was unreachable in async production topology without the stamp.
9559
+ - **P3 comment fixes** (reviewer FIX_THEN_SHIP): CR sign-input contract doc (`${nonce}${canonical}`,
9560
+ no separator, no runId) + lost-ack dedup topology condition.
9561
+ - **KNOWN BLOCKER (Finding D — next wave)**: the run's broker socket file vanishes from the filesystem
9562
+ within ~seconds of runner start (listener fd stays valid for existing connections — park works — but
9563
+ fresh connects get ENOENT; live evidence: stamped path valid at park time, absent seconds later,
9564
+ runner alive+heartbeating, /proc/net/unix shows no bind for it while older sockets remain). Until
9565
+ Finding D is fixed, `deliverResponseRow`'s RPC path fails fast to the hướng-C local append in async
9566
+ runs — delivery remains correct via 0.11.55 semantics; `respond.force`/`respond.append` activate
9567
+ fully once the socket lifetime is fixed. Suspected vectors: sibling lifecycle stale-sweep /
9568
+ libuv close-unlink-by-name (socket-stop.ts documents the primitive) / sessionId inheritance
9569
+ collision between the runner and its children.
9570
+
9571
+ ### FINDING D closed + R6-5 waiting-preserve (2026-11-01, probes 10-13)
9572
+
9573
+ - **FINDING D root cause**: async runs handed every worker the DISPATCHER's broker
9574
+ socket; a `pi -p` dispatcher exits seconds after dispatch and session_shutdown
9575
+ unlinks that socket while the run lives on — every later broker op (parks,
9576
+ messages, respond RPC) hit ENOENT. Fix: the detached runner now HOSTS the run's
9577
+ broker (`runner-broker.ts`) — fresh `runner-<runId>` socket, per-task compound
9578
+ tokens minted in the runner heap (same one-pipe trust domain as the stdin
9579
+ pre-mints), dispatcher's public verify key still the respond.append CR anchor,
9580
+ construction mirrors the lifecycle host config-for-config, stopped in runCleanup.
9581
+ Soft-fail to the legacy dispatcher-socket issuer when hosting fails.
9582
+ - **R6-5 waiting-preserve** (FINDING A residual closed): the runner's
9583
+ persistHeartbeat ~1s whole-record persist carried a dispatch-time snapshot that
9584
+ never learned the broker parked the task — the first post-park heartbeat ERASED
9585
+ the park from tasks.json (probe10 "no-park" on a verifiably parked worker; stack
9586
+ pinned via temporary saveRunTasksCoalesced instrumentation, probe12 run
9587
+ team_20261101040510). mergeTaskUpdateWithDisk now preserves a disk park against
9588
+ park-unaware non-terminal snapshots (mirror of R6-1 terminal-preserve);
9589
+ legitimate clears (respond requeue / broker wait-resolve victim flip) write full
9590
+ arrays via saveRunTasks directly and are unaffected.
9591
+ - **Live-proven end-to-end (probe13, run team_20261101041053)**: leader dispatched
9592
+ async and exited → runner-hosted broker (manifest-stamped socket) → worker parked
9593
+ → park SURVIVED heartbeats (R6-5) → FOREIGN session (no token/key) responded via
9594
+ `respond.force` (`auth:"forced"`) → worker resumed with the answer and completed
9595
+ the task — the full hướng A acceptance that probes 8/9 could not reach.
9596
+
9597
+ ### HƯỚNG A follow-up: respond.force surfaced through the product respond tool (2026-11-01, probes 14-18)
9598
+
9599
+ - Probes 14-17 exposed a dead end: a RESTARTED/foreign responder (the heap-only
9600
+ dispatcher keypair died with the original `pi -p` process — which structurally
9601
+ exits after async dispatch) could only reach the LOCAL append, whose row is
9602
+ signed with a key the worker does not hold → dropped as noise → the ask timed
9603
+ out with NO product surface able to answer it. deliverResponseRow now has a
9604
+ three-tier chain: broker CR (dispatcher only) → **respond.force** (token-less
9605
+ one-shot for a LIVE park, broker-SIGNED row — probe13/18 live-proven, incl.
9606
+ `task.respond_delivered` worker ack) → local signed append (last-resort trail).
9607
+ Same-uid trust domain unchanged (any local process could already speak the
9608
+ protocol); this surfaces the existing capability through the product tool.
9609
+ - probe18 product-level proof: a foreign session's `team respond` on a live park
9610
+ of a dead-dispatcher run delivered via force end-to-end (respond.broker_append
9611
+ auth:forced → worker ack → task resumed).
9612
+
9613
+ ### steer.force — token-less live-worker steer for foreign/restarted sessions (2026-11-01, probes 19-20)
9614
+
9615
+ - probe19 exposed the steer twin of the respond dead end: a foreign/restarted session's
9616
+ `team steer` writes pendingSteers (next-spawn only) + a steering-file row signed with ITS
9617
+ key — outside the live worker's verify set → pollSteering drops it → in-context delivery
9618
+ NEVER happens (probe19: file row present, worker conversation received NONE).
9619
+ - New pre-hello broker method `steer.force` (mirror of respond.force's lane): the run's
9620
+ broker-host (detached runner — its keypair IS in every worker's verify set) performs the
9621
+ delivery itself: mailbox row (kind:"steer", interrupt) + BROKER-SIGNED steering line;
9622
+ live-task gate (terminal rejected), requestId dedup (R2-L-A4 mirror), worker-role
9623
+ connections rejected, `steer.broker_force` audit event.
9624
+ - `handleSteer` now auto-routes through steer.force (fire-and-forget) when the caller is not
9625
+ the trusted signer — same-uid trust domain unchanged, this only surfaces VERIFIED delivery
9626
+ through the product tool.
9627
+ - probe20 live-proven end-to-end: foreign session `team steer` mid-run → worker's conversation
9628
+ received the message verbatim ("exactly ONE injected steering message observed verbatim").
9629
+
9630
+ ### spawn-phase-trace — observability for log-silent spawn hangs (2026-11-01, incident team_20261101074334)
9631
+
9632
+ - Live incident: 02_execute's first worker spawn hung ~7 minutes between task.started
9633
+ (07:50:05) and the idle-retry respawn (07:57:05) with ZERO trace — no worker.spawned,
9634
+ no checkpoint childPid, no background.log line in the window (the runner's whole log is
9635
+ 17 lines). Forensics could not bisect WHERE it hung (candidates: worker-slot semaphore
9636
+ wait, model-registry resolution chain, child spawn env build — all log-silent).
9637
+ - `createSpawnPhaseTracer`: a 30s unref'd interval per spawn attempt printing
9638
+ `[spawn.trace] task=… attempt=… phase=… elapsedMs=…` to background.log — silent for
9639
+ healthy sub-30s spawns, a breadcrumb every 30s while stuck. Wired in child-executor
9640
+ (per-attempt, ended on onSpawn + attempt-finally).
9641
+ - `runWorker` slot-wait timing: acquire→fn-start >5s logs
9642
+ `[spawn.trace] worker-slot wait=…ms` — separates slot starvation from in-child phases
9643
+ in the next hang's trail.