pi-crew 0.11.55 → 0.11.57
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 +136 -0
- package/dist/index.mjs +530 -529
- package/package.json +1 -1
- package/src/extension/registration/lifecycle-handlers.ts +10 -1
- package/src/extension/team-tool/respond-delivery.ts +429 -0
- package/src/extension/team-tool/respond.ts +135 -50
- package/src/extension/team-tool.ts +41 -1
- package/src/runtime/async-runner.ts +10 -1
- package/src/runtime/background-runner.ts +93 -17
- package/src/runtime/broker/crew-broker.ts +146 -1
- package/src/runtime/broker/dispatcher-key-registry.ts +39 -0
- package/src/runtime/broker/protocol/broker-respond.ts +365 -0
- package/src/runtime/broker/protocol/broker-steer.ts +151 -0
- package/src/runtime/broker/protocol/connection-state.ts +10 -0
- package/src/runtime/broker/protocol/request-parsers.ts +137 -1
- package/src/runtime/broker/runner-broker.ts +189 -0
- package/src/runtime/broker/stdin-handshake.ts +39 -4
- package/src/runtime/run-worker.ts +17 -2
- package/src/runtime/spawn-phase-trace.ts +53 -0
- package/src/runtime/task-runner/child-executor.ts +14 -0
- package/src/runtime/task-runner/state-helpers.ts +32 -0
- package/src/state/contracts.ts +15 -0
- package/src/state/coordination/mailbox.ts +15 -0
- package/src/state/types.ts +11 -0
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.
|