diff --git a/docs/EVENT-AWAIT.md b/docs/EVENT-AWAIT.md index 63721c291..bb9a01f9e 100644 --- a/docs/EVENT-AWAIT.md +++ b/docs/EVENT-AWAIT.md @@ -4,6 +4,37 @@ proposed, 2026-09-17. Governs the resident verb `on` when it is used inside a running flow body rather than as the flow's entry condition.* +### Implementation boundary (2026-09-19) + +The local SDK/daemon path implements preparation, explicit router activation, +durable root suspension, wake replay, and CLI resume. `subscription.park` is a +lease-holder-only protocol call with `run_id`, `step_id`, `attempt`, +`idempotency_key`, `subscription_id`, and `phase` (`activation` or `event_wait`). +It journals a step-scoped `wait.event` and releases the worker lease; it does +not record a crash or charge a retry. `run.resume` checks the subscription's +journaled readiness before dispatching the root again. Resuming an unchanged +wait returns the same CLI suspension report without running the body. + +Each authored `next()` supplies a stable call `sequence` to `subscription.next`. +Completed wakes replay by that ordinal, including after acknowledgment; a body +re-executing from the beginning must not receive its second wake at its first +call. Replaying a closed handle does not reopen provider ingress. + +Subscription handles are run-scoped call ordinals (`activity-1`, `activity-2`, +...). Authors must keep `f.on()` call order stable on replay, just as child +step call order must be stable. These ids are not derived from source locations: +reordering declarations or branching on unjournaled external state does not +preserve an activity's identity. The pinned body and replayed results provide +the supported deterministic ordering. + +Cloud's durable binding registry, ingress fencing, suspended-result handling, +and wake scheduling remain integration work. The CLI integration probe uses a +local router adapter, not a deployed provider webhook. It lives at +`packages/sdk/tests/fixtures/event-await-cli-probe.mjs`; the associated test +exercises repeated resume, daemon SIGKILL/restart, two ordered wakes, duplicate +delivery, and memoized child effects. This is not evidence for Cloud routing, +provider authorization, or epoch-compaction acceptance below. + ## 1. The problem A flow that opens a pull request is not finished when the PR exists. CI fails, @@ -172,28 +203,37 @@ body-level `on` without both, with `unbounded_subscription`. ## 5. Kernel (additive) -No new step kind and no new verb. The kernel vocabulary stays closed -(decision 13). +No new step kind or surface verb. The daemon adds the internal +`subscription.activate` handoff verb; the kernel's step vocabulary stays +closed (decision 13). -1. **`subscription.opened`** — `subscription_id` (deterministic from run id, - step id and declaration), `event_types`, `pattern` (the recursive-subset +1. **`subscription.prepared`** — `subscription_id` (the run-scoped `f.on()` call + ordinal, whose order must remain stable on replay), `event_types`, `pattern` (the recursive-subset match already used by `TriggerSpec.pattern`), `stream` - (`subscription/`), `deadline_at_ms`, `include_self`, and - the immutable provider binding: integration installation, canonical - resource scope, authorization snapshot, router binding generation, and - durable ingress offset. Opening is a two-party handshake: Cloud first - records the fenced binding at that ingress offset, then the journal appends - `subscription.opened`; `f.on()` is not visible to the body until both have - completed. Recovery first honors any durable `closing: overflow` fence for - that generation: it completes the close and its active wait, never restores - or replays the binding. Absent that fence, recovery removes a prepared - binding that has no matching journal entry, and otherwise restores the same - generation and replays ingress after its offset before acknowledging the - body. This closes the journal-to-router race without delivering frames that - predate opening. -2. **`subscription.closed`** — `subscription_id`, `completionReason` + (`subscription/`), `settle_ms`, `idle_ms`, + `deadline_at_ms`, and `include_self`. It is an immutable request, never an + open cursor and never eligible for ingress. The daemon returns a `suspended` + outcome at this boundary, carrying this exact prepared snapshot to Cloud + (`eventTypes`, canonical `pattern`, bounds, and `includeSelf`); Cloud never + parses the sandbox SQLite journal. Cloud assigns the binding generation and + ingress cursor after persisting its registry row, so those receipts are not + author-controlled suspension fields. No resident daemon thread waits for a + Cloud binding. +2. **`subscription.opened`** — the prepared request plus immutable provider + binding: integration installation, canonical resource scope, authorization + snapshot, router binding generation, and durable ingress offset. Cloud must + first persist its binding and ingress cursor, then invoke the idempotent + `subscription.activate` daemon verb, which appends this entry. Only an + active entry is visible when the body is resumed. Recovery honors any + durable `closing: overflow` fence for that generation; it completes the + close and its active wait, never restores or replays the binding. Cloud + removes a prepared registry record that lacks a matching active journal + entry, and otherwise restores the same generation and replays ingress + strictly after its cursor. This closes the journal-to-router race without + delivering frames that predate opening. +3. **`subscription.closed`** — `subscription_id`, `completionReason` (`closed` \| `run_completed` \| `canceled` \| `deadline` \| `overflow`). -3. **Buffered delivery** — matching events become `stream.appended` on the +4. **Buffered delivery** — matching events become `stream.appended` on the subscription's stream, carrying the provider delivery id as the idempotency key. A stream holds at most **1,000 unread frames or 1 MiB of unread encoded frame bytes**, measured after this subscription consumer's acknowledged @@ -213,7 +253,7 @@ No new step kind and no new verb. The kernel vocabulary stays closed the fenced binding; it never restores that generation as open. The would-exceed frame is unappended. Events for a closed or unknown subscription are refused, not buffered. -4. **`wait.event` extension** — alongside `event_key`, a wait may name +5. **`wait.event` extension** — alongside `event_key`, a wait may name `stream`, `from_offset`, `settle_ms`, `idle_at_ms`, and `deadline_at_ms`. It completes with `event_received` and `result: { from_offset, next_offset }` once the stream has entries at or past `from_offset` and `settle_ms` has @@ -224,10 +264,10 @@ No new step kind and no new verb. The kernel vocabulary stays closed exist; idle completes with `result: { timeout: "idle" }` only when no buffered entries won the serialized race. Adding result fields keeps `wait.completed`'s reason enum unchanged. -5. **Timeouts are enforced.** The scheduler arms `timeout_at_ms`, +6. **Timeouts are enforced.** The scheduler arms `timeout_at_ms`, `idle_at_ms`, and `deadline_at_ms` as durable timers for every open wait, including `wait.human`. This closes the gap in §2 for existing waits too. -6. **Epoch summary** carries open subscriptions with their stream offsets and +7. **Epoch summary** carries open subscriptions with their stream offsets and deadlines alongside `open_waits`. ## 6. Router contract (Cloud) @@ -235,12 +275,13 @@ No new step kind and no new verb. The kernel vocabulary stays closed The event router is Cloud's, not the kernel's (decision 15: the kernel is tenant-unaware). -- A `subscription.opened` entry is projected to the router as a fenced binding - of `(run_id, subscription_id, generation, ingress_offset)` to its event - types, pattern, provider installation, and canonical resource scope. It is - removed on `subscription.closed`. The open handshake records the binding and - ingress offset before the body can observe the subscription; recovery - replays ingress strictly after that offset before acknowledging the binding. +- A `subscription.prepared` entry tells Cloud to create a fenced binding of + `(run_id, subscription_id, generation, ingress_offset)` to its event types, + pattern, provider installation, and canonical resource scope. Cloud writes + that binding and cursor durably, calls `subscription.activate`, then resumes + the body; it removes the binding on `subscription.closed`. A prepared record + alone never accepts ingress. Recovery replays ingress strictly after the + activated cursor before acknowledging the body. - The router matches incoming `EventFrameV1` frames against open bindings, first proving the frame came through the bound installation and is within the bound resource scope. It then applies the self-actor filter and calls @@ -249,6 +290,35 @@ tenant-unaware). sleeping cell wakes the cell. - The router never decides whether the flow is done. It only delivers. +### Targeted router protocol + +Cloud delivers through `subscription.deliver`, supplying `run_id`, +`subscription_id`, the exact immutable `router_binding`, a stable `delivery_id`, +and the authorized `frame`. This targets only that subscription. The local +`event.emit` adapter broadcasts to matching subscriptions and is not the Cloud +router boundary. The kernel refuses unknown, prepared, closed, and stale-binding +recipients. Duplicates return `{ appended: false, reason: "duplicate" }`; an +append that triggers the unread limit returns `reason: "overflow"` after the +journaled overflow close. A successful append returns `{ appended: true }`. +Provider installation, resource scope, event matching, and self-actor checks +remain Cloud's responsibility before this call; the receipt is a fence, not an +authorization credential. + +An activation retry must carry the same ingress cursor and complete binding +receipt as the original journal entry. Changing either is +`subscription_binding_mismatch`, never a replacement of the active binding. +`subscription.fence_overflow` carries the same receipt and completes Cloud's +persisted overflow fence idempotently under the per-run sequencer. + +`subscription.inspect` takes `run_id` and returns a read-only `subscriptions` +projection: identity, prepared/active/closed state, closing reason, binding and +activation cursor, unread frame/byte counts, settle duration, and absolute idle +and deadline instants. Idle is taken from the durable wait or the last journaled +wake, never the time of this query. This gives Cloud a protocol surface for +scheduling and cleanup without reading sandbox SQLite files. A delivery response +alone does not authorize Cloud to advance its durable ingress acknowledgment: +the containing journal must first cross the durable publication barrier. + ## 7. Acceptance The crash-injection suite is the gate (AGENTS.md standard 5). A conforming diff --git a/docs/SURFACE.md b/docs/SURFACE.md index cb0865562..3c642a75a 100644 --- a/docs/SURFACE.md +++ b/docs/SURFACE.md @@ -1449,6 +1449,7 @@ The exit codes are part of the surface contract: | `1` | The run failed with a declared `completionReason`, or a transport, runtime, or daemon protocol error left the outcome unknown. A `step_failed` run names the failing step and its per-step `completionReason`, plus the exit code and output tails the journal recorded for it. An authored `done("step_failed")` exits `1` as well, and says so without naming a step, because no step failed — the body declared the verdict. With a `detail`, that detail replaces the generic sentence and is reported as `completionDetail`. | | `2` | The command was refused before a journal write: invalid input, failed preflight, unreachable daemon, a `run_not_found` resume target, or a `--local-agent` the named run cannot honour (`local_agent_unavailable`, below). | | `3` | The run parked. `PARKED [run_parked]` names the step and its `llm` or `agent` type, and distinguishes an unavailable worker from a `needs_human` recovery wait. An authored body parked on `f.human` reports the question, who it is for, and the `flows answer` invocation that records the decision (see *Human gates* below). | +| `4` | An authored body suspended at `f.on(...).next()` for subscription activation or event delivery. JSON reports `status: "suspended"` and the durable `suspension` boundary. The root releases its worker lease; unchanged waits can be resumed without consuming crash retries. This local SDK/daemon contract still requires Cloud router integration before hosted use; see [EVENT-AWAIT.md](EVENT-AWAIT.md). | Without an attached worker, reaching an `llm` or `agent` step returns a durable parked outcome. For authored TypeScript, `--local-agent` attaches both local diff --git a/docs/evidence/event-await-implementation/README.md b/docs/evidence/event-await-implementation/README.md new file mode 100644 index 000000000..321298334 --- /dev/null +++ b/docs/evidence/event-await-implementation/README.md @@ -0,0 +1,300 @@ +# Event-await verification, 2026-09-19 + +This replaces the earlier implementation notes and inconsistent test transcript. +The current protocol and its Cloud integration boundary are specified in +[EVENT-AWAIT.md](../../EVENT-AWAIT.md). + +The handshake is `subscription.open` → `subscription.prepared` → Cloud persists +its binding/cursor → `subscription.activate` → `subscription.opened`. +`subscription.park` releases the root lease on a durable wait. A local router +adapter supplies events for the CLI probe; it does not establish a deployed +Cloud provider path. Cloud binding/authorization, wake scheduling, and epoch +compaction acceptance remain unverified. No complete acceptance claim is made. + +## Kernel parking and replay + +```text +cwd: /tmp/flows-pr-followup/pr441/kernel +$ cargo test --locked -p relayflowd --test event_activity_parking + Compiling relayflowd v0.1.0 (/tmp/flows-pr-followup/pr441/kernel/relayflowd) + Finished `test` profile [unoptimized + debuginfo] target(s) in 0.48s + Running tests/event_activity_parking.rs (target/debug/deps/event_activity_parking-d8e0d6eeb684776f) + +running 3 tests +test replay_keeps_each_acknowledged_batch_addressable_by_body_call_ordinal ... ok +test activation_and_delivery_racing_the_lease_handoff_are_not_lost ... ok +test parked_attempt_survives_restart_and_only_a_ready_subscription_redispatches_it ... ok + +test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.06s + + +exit status: 0 + +``` + +## SDK types and targeted integration/runtime tests + +```text +cwd: /tmp/flows-pr-followup/pr441/packages/sdk +$ sh -c 'export PATH=/tmp/flows-pr-cleanup/toolchain/node_modules/node/bin:/tmp/flows-pr-cleanup/toolchain/node_modules/.bin:$PATH RELAYFLOWD_BIN=/tmp/flows-pr-followup/pr441/kernel/target/debug/relayflowd; npm run typecheck && npm run typecheck:tests && npx vitest run tests/authored-activity.test.ts tests/activity-preflight.test.ts tests/live-event-activities.test.ts tests/event-await-cli.test.ts tests/authored-root.test.ts tests/authored-human.test.ts tests/authored-node-runtime.test.ts tests/journal-client.test.ts' + +> @relayflows/sdk@2.0.22 typecheck +> tsc --noEmit && tsc -p tsconfig.type-tests.json + + +> @relayflows/sdk@2.0.22 typecheck:tests +> tsc -p tsconfig.tests.json + + + RUN v2.1.9 /tmp/flows-pr-followup/pr441/packages/sdk + + ✓ tests/journal-client.test.ts (15 tests) 90ms + ✓ tests/activity-preflight.test.ts (1 test) 9ms + ✓ tests/authored-activity.test.ts (13 tests) 93ms + ✓ tests/authored-human.test.ts (13 tests) 115ms + ✓ tests/authored-root.test.ts (12 tests) 174ms + ✓ tests/live-event-activities.test.ts (2 tests) 186ms + ✓ tests/event-await-cli.test.ts (1 test) 9603ms + ✓ parks, restarts, and replays two event wakes through the actual CLI 9602ms + ✓ tests/authored-node-runtime.test.ts (14 tests) 73430ms + ✓ Bun 1.4.0 standalone → native Node authored lifecycle > serializes the immutable prepared binding facts through the Node and CLI boundary 1284ms + ✓ Bun 1.4.0 standalone → native Node authored lifecycle > awaits agent plus three run steps and resumes without repeating effects 1899ms + ✓ Bun 1.4.0 standalone → native Node authored lifecycle > accepts a predicate-gated flow: the `.gate` child is journaled, verified, and not counted as an authored step 1918ms + ✓ Bun 1.4.0 standalone → native Node authored lifecycle > parks an f.human across the IPC boundary, answers it, and resumes the Node body with the answer 2976ms + ✓ Bun 1.4.0 standalone → native Node authored lifecycle > stops on parent SIGKILL and replays completed children before success 2388ms + ✓ Bun 1.4.0 standalone → native Node authored lifecycle > stops on parent SIGTERM and replays completed children before success 2502ms + ✓ Bun 1.4.0 standalone → native Node authored lifecycle > stops on parent blocked-SIGKILL and replays completed children before success 2469ms + ✓ Bun 1.4.0 standalone → native Node authored lifecycle > stops on parent SIGKILL and replays completed children before declined 2294ms + ✓ Bun 1.4.0 standalone → native Node authored lifecycle > refuses unawaited rather than reporting terminal success 13458ms + ✓ Bun 1.4.0 standalone → native Node authored lifecycle > refuses manual then rather than reporting terminal success 14316ms + ✓ Bun 1.4.0 standalone → native Node authored lifecycle > loads captured graph bytes before preserving the unsupported-use refusal 13186ms + ✓ Bun 1.4.0 standalone → native Node authored lifecycle > rejects a forged result frame without durable completion 12851ms + ✓ Bun 1.4.0 standalone → native Node authored lifecycle > refuses missing Node before body effects or root admission 412ms + ✓ Bun 1.4.0 standalone → native Node authored lifecycle > refuses an old Node candidate before body effects 462ms + + Test Files 8 passed (8) + Tests 71 passed (71) + Start at 20:41:12 + Duration 74.60s (transform 1.12s, setup 0ms, collect 6.05s, tests 83.70s, environment 2ms, prepare 545ms) + + +exit status: 0 + +``` + +## Actual CLI restart and replay probe + +```text +cwd: /tmp/flows-pr-followup/pr441 +$ env RELAYFLOWD_BIN=/tmp/flows-pr-followup/pr441/kernel/target/debug/relayflowd /tmp/flows-pr-cleanup/toolchain/node_modules/node/bin/node packages/sdk/tests/fixtures/event-await-cli-probe.mjs /tmp/flows-pr-followup/pr441 +{"args":["run","await.flow.ts","--input","{}"],"status":4,"stdout":"{\"ok\":false,\"command\":\"run\",\"resolutions\":[],\"diagnostics\":[{\"severity\":\"warning\",\"kind\":\"subscription_suspended\",\"message\":\"Flow \\\"event-await-cli\\\" suspended for activation.\"}],\"path\":\"await.flow.ts\",\"runId\":\"01M2YEDANJN68QR2M0HYGBQSC5\",\"socketPath\":\"/run/user/1000/relayflowd-048d675f69db.sock\",\"status\":\"suspended\",\"suspension\":{\"kind\":\"activation\",\"subscriptionId\":\"activity-1\",\"eventTypes\":[\"e2e_event\"],\"stream\":\"subscription/activity-1\",\"settleMs\":0,\"idleMs\":3600000,\"deadlineAtMs\":1789962028741,\"includeSelf\":false},\"completedSteps\":0}\n","stderr":"WARNING [subscription_suspended] Flow \"event-await-cli\" suspended for activation.\n"} +{"args":["resume","01M2YEDANJN68QR2M0HYGBQSC5"],"status":4,"stdout":"{\"ok\":false,\"command\":\"resume\",\"resolutions\":[],\"diagnostics\":[{\"severity\":\"warning\",\"kind\":\"subscription_suspended\",\"message\":\"Flow \\\"event-await-cli\\\" suspended for activation.\"}],\"runId\":\"01M2YEDANJN68QR2M0HYGBQSC5\",\"socketPath\":\"/run/user/1000/relayflowd-048d675f69db.sock\",\"status\":\"suspended\",\"suspension\":{\"kind\":\"activation\",\"subscriptionId\":\"activity-1\",\"stream\":\"subscription/activity-1\",\"deadlineAtMs\":1789962028741,\"eventTypes\":[\"e2e_event\"],\"settleMs\":0,\"idleMs\":3600000,\"includeSelf\":false},\"completedSteps\":0}\n","stderr":"WARNING [subscription_suspended] Flow \"event-await-cli\" suspended for activation.\n"} +{"args":["resume","01M2YEDANJN68QR2M0HYGBQSC5"],"status":4,"stdout":"{\"ok\":false,\"command\":\"resume\",\"resolutions\":[],\"diagnostics\":[{\"severity\":\"warning\",\"kind\":\"subscription_suspended\",\"message\":\"Flow \\\"event-await-cli\\\" suspended for activation.\"}],\"runId\":\"01M2YEDANJN68QR2M0HYGBQSC5\",\"socketPath\":\"/run/user/1000/relayflowd-048d675f69db.sock\",\"status\":\"suspended\",\"suspension\":{\"kind\":\"activation\",\"subscriptionId\":\"activity-1\",\"stream\":\"subscription/activity-1\",\"deadlineAtMs\":1789962028741,\"eventTypes\":[\"e2e_event\"],\"settleMs\":0,\"idleMs\":3600000,\"includeSelf\":false},\"completedSteps\":0}\n","stderr":"WARNING [subscription_suspended] Flow \"event-await-cli\" suspended for activation.\n"} +{"args":["resume","01M2YEDANJN68QR2M0HYGBQSC5"],"status":4,"stdout":"{\"ok\":false,\"command\":\"resume\",\"resolutions\":[],\"diagnostics\":[{\"severity\":\"warning\",\"kind\":\"subscription_suspended\",\"message\":\"Flow \\\"event-await-cli\\\" suspended for activation.\"}],\"runId\":\"01M2YEDANJN68QR2M0HYGBQSC5\",\"socketPath\":\"/run/user/1000/relayflowd-048d675f69db.sock\",\"status\":\"suspended\",\"suspension\":{\"kind\":\"activation\",\"subscriptionId\":\"activity-1\",\"stream\":\"subscription/activity-1\",\"deadlineAtMs\":1789962028741,\"eventTypes\":[\"e2e_event\"],\"settleMs\":0,\"idleMs\":3600000,\"includeSelf\":false},\"completedSteps\":0}\n","stderr":"WARNING [subscription_suspended] Flow \"event-await-cli\" suspended for activation.\n"} +{"args":["resume","01M2YEDANJN68QR2M0HYGBQSC5"],"status":4,"stdout":"{\"ok\":false,\"command\":\"resume\",\"resolutions\":[],\"diagnostics\":[{\"severity\":\"warning\",\"kind\":\"subscription_suspended\",\"message\":\"Flow \\\"event-await-cli\\\" suspended for activation.\"}],\"runId\":\"01M2YEDANJN68QR2M0HYGBQSC5\",\"socketPath\":\"/run/user/1000/relayflowd-048d675f69db.sock\",\"status\":\"suspended\",\"suspension\":{\"kind\":\"activation\",\"subscriptionId\":\"activity-1\",\"stream\":\"subscription/activity-1\",\"deadlineAtMs\":1789962028741,\"eventTypes\":[\"e2e_event\"],\"settleMs\":0,\"idleMs\":3600000,\"includeSelf\":false},\"completedSteps\":0}\n","stderr":"WARNING [subscription_suspended] Flow \"event-await-cli\" suspended for activation.\n"} +{"args":["resume","01M2YEDANJN68QR2M0HYGBQSC5"],"status":4,"stdout":"{\"ok\":false,\"command\":\"resume\",\"resolutions\":[],\"diagnostics\":[{\"severity\":\"warning\",\"kind\":\"subscription_suspended\",\"message\":\"Flow \\\"event-await-cli\\\" suspended for activation.\"}],\"runId\":\"01M2YEDANJN68QR2M0HYGBQSC5\",\"socketPath\":\"/run/user/1000/relayflowd-048d675f69db.sock\",\"status\":\"suspended\",\"suspension\":{\"kind\":\"activation\",\"subscriptionId\":\"activity-1\",\"stream\":\"subscription/activity-1\",\"deadlineAtMs\":1789962028741,\"eventTypes\":[\"e2e_event\"],\"settleMs\":0,\"idleMs\":3600000,\"includeSelf\":false},\"completedSteps\":0}\n","stderr":"WARNING [subscription_suspended] Flow \"event-await-cli\" suspended for activation.\n"} +{"args":["resume","01M2YEDANJN68QR2M0HYGBQSC5"],"status":4,"stdout":"{\"ok\":false,\"command\":\"resume\",\"resolutions\":[],\"diagnostics\":[{\"severity\":\"warning\",\"kind\":\"subscription_suspended\",\"message\":\"Flow \\\"event-await-cli\\\" suspended for activation.\"}],\"runId\":\"01M2YEDANJN68QR2M0HYGBQSC5\",\"socketPath\":\"/run/user/1000/relayflowd-048d675f69db.sock\",\"status\":\"suspended\",\"suspension\":{\"kind\":\"activation\",\"subscriptionId\":\"activity-1\",\"stream\":\"subscription/activity-1\",\"deadlineAtMs\":1789962028741,\"eventTypes\":[\"e2e_event\"],\"settleMs\":0,\"idleMs\":3600000,\"includeSelf\":false},\"completedSteps\":0}\n","stderr":"WARNING [subscription_suspended] Flow \"event-await-cli\" suspended for activation.\n"} +{"args":["resume","01M2YEDANJN68QR2M0HYGBQSC5"],"status":4,"stdout":"{\"ok\":false,\"command\":\"resume\",\"resolutions\":[],\"diagnostics\":[{\"severity\":\"warning\",\"kind\":\"subscription_suspended\",\"message\":\"Flow \\\"event-await-cli\\\" suspended for activation.\"}],\"runId\":\"01M2YEDANJN68QR2M0HYGBQSC5\",\"socketPath\":\"/run/user/1000/relayflowd-048d675f69db.sock\",\"status\":\"suspended\",\"suspension\":{\"kind\":\"activation\",\"subscriptionId\":\"activity-1\",\"stream\":\"subscription/activity-1\",\"deadlineAtMs\":1789962028741,\"eventTypes\":[\"e2e_event\"],\"settleMs\":0,\"idleMs\":3600000,\"includeSelf\":false},\"completedSteps\":0}\n","stderr":"WARNING [subscription_suspended] Flow \"event-await-cli\" suspended for activation.\n"} +{"args":["resume","01M2YEDANJN68QR2M0HYGBQSC5"],"status":4,"stdout":"{\"ok\":false,\"command\":\"resume\",\"resolutions\":[],\"diagnostics\":[{\"severity\":\"warning\",\"kind\":\"subscription_suspended\",\"message\":\"Flow \\\"event-await-cli\\\" suspended for activation.\"}],\"runId\":\"01M2YEDANJN68QR2M0HYGBQSC5\",\"socketPath\":\"/run/user/1000/relayflowd-048d675f69db.sock\",\"status\":\"suspended\",\"suspension\":{\"kind\":\"activation\",\"subscriptionId\":\"activity-1\",\"stream\":\"subscription/activity-1\",\"deadlineAtMs\":1789962028741,\"eventTypes\":[\"e2e_event\"],\"settleMs\":0,\"idleMs\":3600000,\"includeSelf\":false},\"completedSteps\":0}\n","stderr":"WARNING [subscription_suspended] Flow \"event-await-cli\" suspended for activation.\n"} +{"args":["resume","01M2YEDANJN68QR2M0HYGBQSC5"],"status":4,"stdout":"{\"ok\":false,\"command\":\"resume\",\"resolutions\":[],\"diagnostics\":[{\"severity\":\"warning\",\"kind\":\"subscription_suspended\",\"message\":\"Flow \\\"event-await-cli\\\" suspended for activation.\"}],\"runId\":\"01M2YEDANJN68QR2M0HYGBQSC5\",\"socketPath\":\"/run/user/1000/relayflowd-048d675f69db.sock\",\"status\":\"suspended\",\"suspension\":{\"kind\":\"activation\",\"subscriptionId\":\"activity-1\",\"stream\":\"subscription/activity-1\",\"deadlineAtMs\":1789962028741,\"eventTypes\":[\"e2e_event\"],\"settleMs\":0,\"idleMs\":3600000,\"includeSelf\":false},\"completedSteps\":0}\n","stderr":"WARNING [subscription_suspended] Flow \"event-await-cli\" suspended for activation.\n"} +{"args":["resume","01M2YEDANJN68QR2M0HYGBQSC5"],"status":4,"stdout":"{\"ok\":false,\"command\":\"resume\",\"resolutions\":[],\"diagnostics\":[{\"severity\":\"warning\",\"kind\":\"subscription_suspended\",\"message\":\"Flow \\\"event-await-cli\\\" suspended for activation.\"}],\"runId\":\"01M2YEDANJN68QR2M0HYGBQSC5\",\"socketPath\":\"/run/user/1000/relayflowd-048d675f69db.sock\",\"status\":\"suspended\",\"suspension\":{\"kind\":\"activation\",\"subscriptionId\":\"activity-1\",\"stream\":\"subscription/activity-1\",\"deadlineAtMs\":1789962028741,\"eventTypes\":[\"e2e_event\"],\"settleMs\":0,\"idleMs\":3600000,\"includeSelf\":false},\"completedSteps\":0}\n","stderr":"WARNING [subscription_suspended] Flow \"event-await-cli\" suspended for activation.\n"} +{"args":["resume","01M2YEDANJN68QR2M0HYGBQSC5"],"status":4,"stdout":"{\"ok\":false,\"command\":\"resume\",\"resolutions\":[],\"diagnostics\":[{\"severity\":\"warning\",\"kind\":\"subscription_suspended\",\"message\":\"Flow \\\"event-await-cli\\\" suspended for event_wait.\"}],\"runId\":\"01M2YEDANJN68QR2M0HYGBQSC5\",\"socketPath\":\"/run/user/1000/relayflowd-048d675f69db.sock\",\"status\":\"suspended\",\"suspension\":{\"kind\":\"event_wait\",\"subscriptionId\":\"activity-1\",\"stream\":\"subscription/activity-1\",\"deadlineAtMs\":1789962028741},\"completedSteps\":0}\n","stderr":"WARNING [subscription_suspended] Flow \"event-await-cli\" suspended for event_wait.\n"} +{"args":["resume","01M2YEDANJN68QR2M0HYGBQSC5"],"status":4,"stdout":"{\"ok\":false,\"command\":\"resume\",\"resolutions\":[],\"diagnostics\":[{\"severity\":\"warning\",\"kind\":\"subscription_suspended\",\"message\":\"Flow \\\"event-await-cli\\\" suspended for event_wait.\"}],\"runId\":\"01M2YEDANJN68QR2M0HYGBQSC5\",\"socketPath\":\"/run/user/1000/relayflowd-048d675f69db.sock\",\"status\":\"suspended\",\"suspension\":{\"kind\":\"event_wait\",\"subscriptionId\":\"activity-1\",\"stream\":\"subscription/activity-1\",\"deadlineAtMs\":1789962028741},\"completedSteps\":0}\n","stderr":"WARNING [subscription_suspended] Flow \"event-await-cli\" suspended for event_wait.\n"} +{"args":["resume","01M2YEDANJN68QR2M0HYGBQSC5"],"status":4,"stdout":"{\"ok\":false,\"command\":\"resume\",\"resolutions\":[],\"diagnostics\":[{\"severity\":\"warning\",\"kind\":\"subscription_suspended\",\"message\":\"Flow \\\"event-await-cli\\\" suspended for event_wait.\"}],\"runId\":\"01M2YEDANJN68QR2M0HYGBQSC5\",\"socketPath\":\"/run/user/1000/relayflowd-048d675f69db.sock\",\"status\":\"suspended\",\"suspension\":{\"kind\":\"event_wait\",\"subscriptionId\":\"activity-1\",\"stream\":\"subscription/activity-1\",\"deadlineAtMs\":1789962028741},\"completedSteps\":0}\n","stderr":"WARNING [subscription_suspended] Flow \"event-await-cli\" suspended for event_wait.\n"} +{"args":["resume","01M2YEDANJN68QR2M0HYGBQSC5"],"status":0,"stdout":"{\"ok\":true,\"command\":\"resume\",\"resolutions\":[],\"diagnostics\":[],\"runId\":\"01M2YEDANJN68QR2M0HYGBQSC5\",\"socketPath\":\"/run/user/1000/relayflowd-048d675f69db.sock\",\"completedSteps\":4,\"status\":\"completed\",\"completionReason\":\"success\"}\n","stderr":""} +{"args":["resume","01M2YEDANJN68QR2M0HYGBQSC5"],"status":0,"stdout":"{\"ok\":true,\"command\":\"resume\",\"resolutions\":[],\"diagnostics\":[],\"runId\":\"01M2YEDANJN68QR2M0HYGBQSC5\",\"socketPath\":\"/run/user/1000/relayflowd-048d675f69db.sock\",\"completedSteps\":4,\"status\":\"completed\",\"completionReason\":\"success\"}\n","stderr":""} +E2E_PASS: repeated park, SIGKILL/restart, two wakes replayed in order, deduped delivery, exactly-once child effects, zero crash retries + +exit status: 0 + +``` + +## Review regressions: event isolation and close reasons + +```text +cwd: /tmp/flows-pr-followup/pr441/kernel +$ cargo test --locked -p relayflowd --test event_activity_parking --test event_activities + Compiling relayflowd v0.1.0 (/tmp/flows-pr-followup/pr441/kernel/relayflowd) + Finished `test` profile [unoptimized + debuginfo] target(s) in 1.60s + Running tests/event_activities.rs (target/debug/deps/event_activities-ed1739dfa94f36b5) + +running 11 tests +test prepared_open_response_replays_the_immutable_binding_snapshot ... ok +test exact_deadline_tie_wins_and_reports_unread_range ... ok +test cancel_closes_an_open_activity_before_the_terminal_run_record ... ok +test overflow_of_a_parked_next_returns_overflow_after_recovery ... ok +test accepted_append_is_buffered_deduplicated_and_survives_a_restart_before_next ... ok +test idle_wait_is_durable_and_fires_without_an_event ... ok +test immediate_event_wakes_have_durable_distinct_wait_boundaries ... ok +test normal_wake_is_not_acknowledged_until_the_following_next ... ok +test prepared_binding_stays_invisible_across_a_crash_until_activation_then_next_suspends ... ok +test remaining_event_await_acceptance_cases_use_the_real_journal ... ok +test overflow_closes_before_the_1001st_unread_frame_and_recovery_never_reopens_it ... ok + +test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 9.73s + + Running tests/event_activity_parking.rs (target/debug/deps/event_activity_parking-d8e0d6eeb684776f) + +running 4 tests +test replay_keeps_each_acknowledged_batch_addressable_by_body_call_ordinal ... ok +test intentional_close_cancels_the_pending_pull_without_claiming_a_timeout ... ok +test activation_and_delivery_racing_the_lease_handoff_are_not_lost ... ok +test parked_attempt_survives_restart_and_only_a_ready_subscription_redispatches_it ... ok + +test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.06s + + +exit status: 0 +``` + +## Build provenance and cleanup regression + +The original targeted SDK run followed this build. The later cleanup regression +command also rebuilds the SDK before testing it. + +```text +cwd: /tmp/flows-pr-followup/pr441/kernel +$ sh -c 'cargo build --locked -p relayflowd && npm run build --prefix ../packages/sdk' + Compiling relayflowd v0.1.0 (/tmp/flows-pr-followup/pr441/kernel/relayflowd) + Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.34s + +> @relayflows/sdk@2.0.22 build +> tsc && node scripts/make-cli-executable.mjs + + +exit status: 0 +``` + +```text +cwd: /tmp/flows-pr-followup/pr441/kernel +$ cargo build --locked -p relayflowd + Compiling relayflowd v0.1.0 (/tmp/flows-pr-followup/pr441/kernel/relayflowd) + Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.16s + +exit status: 0 +``` + +```text +cwd: /tmp/flows-pr-followup/pr441/packages/sdk +$ sh -c 'npm run build && npx vitest run tests/authored-activity.test.ts' + +> @relayflows/sdk@2.0.22 build +> tsc && node scripts/make-cli-executable.mjs + + + RUN v2.1.9 /tmp/flows-pr-followup/pr441/packages/sdk + + ✓ tests/authored-activity.test.ts (15 tests) 79ms + + Test Files 1 passed (1) + Tests 15 passed (15) + Start at 20:56:03 + Duration 1.13s (transform 467ms, setup 0ms, collect 837ms, tests 79ms, environment 0ms, prepare 78ms) + + +exit status: 0 +``` + +## Corrupt journal rejection + +```text +cwd: /tmp/flows-pr-followup/pr441/kernel +$ cargo test --locked -p relayflowd --test event_activity_corruption --test event_activity_parking --test event_activities + Compiling relayflowd v0.1.0 (/tmp/flows-pr-followup/pr441/kernel/relayflowd) + Finished `test` profile [unoptimized + debuginfo] target(s) in 0.34s + Running tests/event_activities.rs (target/debug/deps/event_activities-ed1739dfa94f36b5) + +running 11 tests +test prepared_open_response_replays_the_immutable_binding_snapshot ... ok +test cancel_closes_an_open_activity_before_the_terminal_run_record ... ok +test exact_deadline_tie_wins_and_reports_unread_range ... ok +test overflow_of_a_parked_next_returns_overflow_after_recovery ... ok +test accepted_append_is_buffered_deduplicated_and_survives_a_restart_before_next ... ok +test idle_wait_is_durable_and_fires_without_an_event ... ok +test prepared_binding_stays_invisible_across_a_crash_until_activation_then_next_suspends ... ok +test normal_wake_is_not_acknowledged_until_the_following_next ... ok +test immediate_event_wakes_have_durable_distinct_wait_boundaries ... ok +test remaining_event_await_acceptance_cases_use_the_real_journal ... ok +test overflow_closes_before_the_1001st_unread_frame_and_recovery_never_reopens_it ... ok + +test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 9.78s + + Running tests/event_activity_corruption.rs (target/debug/deps/event_activity_corruption-e16548e9ab56b153) + +running 3 tests +test a_completed_event_range_cannot_replay_with_missing_frames ... ok +test malformed_stream_frames_fail_replay_and_future_append ... ok +test malformed_deadline_range_is_an_error_while_null_is_an_empty_range ... ok + +test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s + + Running tests/event_activity_parking.rs (target/debug/deps/event_activity_parking-d8e0d6eeb684776f) + +running 4 tests +test intentional_close_cancels_the_pending_pull_without_claiming_a_timeout ... ok +test replay_keeps_each_acknowledged_batch_addressable_by_body_call_ordinal ... ok +test activation_and_delivery_racing_the_lease_handoff_are_not_lost ... ok +test parked_attempt_survives_restart_and_only_a_ready_subscription_redispatches_it ... ok + +test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.06s + + +exit status: 0 +``` + +## CLI startup and unchanged watcher checks + +The activity checker now loads only for authored TypeScript checks. These are +one-shot import measurements, not a statistical benchmark. The watcher test +limits and assertions are unchanged. + +```text +cwd: /tmp/flows-pr-followup/pr441/packages/sdk +$ /tmp/flows-pr-cleanup/toolchain/node_modules/node/bin/node --input-type=module -e 'const start = performance.now(); await import("./dist/cli.js"); console.log(JSON.stringify({cliImportMs: performance.now() - start, rssBytes: process.memoryUsage().rss}));' +{"cliImportMs":480.42877,"rssBytes":172797952} + +exit status: 0 +``` + +```text +cwd: /tmp/flows-pr-followup/pr441/packages/sdk +$ /tmp/flows-pr-cleanup/toolchain/node_modules/node/bin/node --input-type=module -e 'const start = performance.now(); await import("./dist/cli.js"); console.log(JSON.stringify({cliImportMs: performance.now() - start, rssBytes: process.memoryUsage().rss}));' +{"cliImportMs":261.441739,"rssBytes":121061376} + +exit status: 0 +``` + +```text +cwd: /tmp/flows-pr-followup/pr441/packages/sdk +$ sh -c 'export PATH=/tmp/flows-pr-cleanup/toolchain/node_modules/node/bin:/tmp/flows-pr-cleanup/toolchain/node_modules/.bin:$PATH; npm run build && npm run typecheck && npx vitest run tests/cli-watch.test.ts tests/activity-preflight.test.ts' + +> @relayflows/sdk@2.0.22 build +> tsc && node scripts/make-cli-executable.mjs + + +> @relayflows/sdk@2.0.22 typecheck +> tsc --noEmit && tsc -p tsconfig.type-tests.json + + + RUN v2.1.9 /tmp/flows-pr-followup/pr441/packages/sdk + + ✓ tests/activity-preflight.test.ts (1 test) 9ms + ✓ tests/cli-watch.test.ts (10 tests) 12411ms + ✓ flows check --watch > rechecks syntax errors, clears once, and returns the last refusal on Ctrl-C 958ms + ✓ flows check --watch > streams JSON lines without ANSI, recovers after atomic saves, and exits zero after repair 1339ms + ✓ flows check --watch > coalesces 20 concurrent saves into at most two rechecks 1516ms + ✓ flows check --watch > watches transitive relative use imports, cycles, and nearest config changes 1815ms + ✓ flows check --watch > refreshes the import graph and notices missing imports being created 1860ms + ✓ flows check --watch > reloads authored TypeScript instead of reusing the first imported definition 1418ms + ✓ flows check --watch > detects a nearer config appearing and falls back after it is deleted 1368ms + ✓ flows check --watch > keeps watching after the target is deleted and recreated 1361ms + ✓ flows check --watch > queues changes during a slow check without overlapping checks 772ms + + Test Files 2 passed (2) + Tests 11 passed (11) + Start at 21:23:28 + Duration 13.83s (transform 765ms, setup 0ms, collect 1.98s, tests 12.42s, environment 0ms, prepare 104ms) + + +exit status: 0 +``` diff --git a/evidence/dev-declared-streams-2026-09-20/README.md b/evidence/dev-declared-streams-2026-09-20/README.md new file mode 100644 index 000000000..105b481c6 --- /dev/null +++ b/evidence/dev-declared-streams-2026-09-20/README.md @@ -0,0 +1,27 @@ +# Dev #455 named-stream worker failure and fix + +Dev Cloud `3362eb8d43cb2882ddbb65309f02527624d6fb77` admitted runtime `0e549c37a74cfd61e34936e3d664f2619d463d02`. One scoped #455 fixture submission created Cloud run `46041b32-e6c8-488d-9c42-226bd356bf0d`, engine run `01M2ZWVRDBM0757P0578C4BT7S`. + +The actual hosted journal proves the deterministic sync step succeeded with `SYNC_MATERIALIZED=ok` and `SYNC_MODE=snapshot`. The flow then parked before its first agent attempt because the local worker held only a random stream, while the flow declared `flows-drive-cloud`. Cloud already supplied `--local-agent`; the missing flag was not the cause. No agent work package, final report, or diff was produced. Cloud recorded failed / `needs_human`; cancellation returned HTTP 409 `Run already failed`. That is not a cleanup-success claim. No durable state was deleted. + +The SDK now registers the ordinary agent steps' declared stream names, using offset zero because this worker has consumed no stream messages. Resume reads names from the immutable journaled spec. Workspace revision pins are never invented; kernel matching and offset fencing remain authoritative. Communication-owned stream registration remains with communication workers. Authored TypeScript behavior is unchanged. This change does not address the separate declarative LLM worker gap. + +## Captured commands and output + +Each `.log` contains its literal command and full output. Commands ran with `TMPDIR=/home/khaliqgant/.cache/dev-pr-proof` exported; the built daemon was explicitly selected with `RELAYFLOWD_BIN=/tmp/flows-pr-followup/pr441/kernel/target/debug/relayflowd`. SDK dependencies were shared read-only through the existing PR441 node_modules symlink, while this checkout has its own dist build. The untracked symlink is not committed. + +- `stream-baseline-build.log`: original implementation build success. +- `stream-regression-red-with-resume.log`: new named-stream run/resume tests reproduced no eligible worker, 6 failed / 2 passed. This is baseline regression evidence, not mutation verification. +- `stream-fix-build.log`: fixed SDK build success. +- `stream-regression-green.log`: intermediate 4-failure test run retained. Agent execution had succeeded; the new spec-immutability assertion incorrectly expected `workspace: []` where the authored spec omitted workspace. Corrected that new expectation to preserve the original shape. +- `stream-regression-green-final.log`: final real-daemon wrapper, named-stream run/resume, workspace refusal, communication worker and stream-selection suites: 5 files / 36 tests passed. The command also named a nonexistent `communication-spec.test.ts`; Vitest ran only the five listed files. No sixth suite is claimed. +- `stream-types.log`: test TypeScript compilation success. +- `stream-diff-check.log`: git diff check success before evidence was added. +- `cloud-dev-drive-launch.log`, `cloud-dev-drive-status.log`, `cloud-dev-drive-logs.log`, `cloud-dev-drive-patch.log`: actual hosted submission and failure evidence. +- `drive-journal.log`: read-only snapshot SQL, with run.spawned intentionally projected to names/types/surfaces; all other entries printed in full. +- `drive-export.log`: successful durable snapshot export digest. Export and SQLite retained locally in `/home/khaliqgant/.cache/dev-pr-proof/drive-export.bin`, `drive-snapshot.tar.gz`, `drive-run.sqlite3`. +- `drive-cancel.log`: HTTP409, not successful resource cleanup. + +An earlier build in `/tmp/flows-dev-stream-pins` failed because the user's tmpfs quota was exhausted, and even the evidence write failed. Its tool output reported TS5033 writes and OSError122. The checkout was copied/repaired to the home cache before the captured successful baseline build; no gate or dependency constraint was changed. + +This is local regression proof against a real daemon with a deterministic stub CLI. A fresh hosted #455 attempt on the new published artifact remains required for acceptance. diff --git a/evidence/dev-declared-streams-2026-09-20/cloud-dev-drive-launch.log b/evidence/dev-declared-streams-2026-09-20/cloud-dev-drive-launch.log new file mode 100644 index 000000000..487ef88fc --- /dev/null +++ b/evidence/dev-declared-streams-2026-09-20/cloud-dev-drive-launch.log @@ -0,0 +1,22 @@ +$ python3 /tmp/cloud-dev-run-cli.py cloud run workflows/drive-cloud-v2.yaml --relayflow-version v2 --sync-code --json + +Agent Relay collects usage telemetry to improve the product. +Run `agent-relay telemetry disable` to opt out. +Learn more: https://agentrelay.com/telemetry + +Validating workflow... +Preparing run... + Prepared in 5.1s +Creating tarball... + Tarball: 2729KB in 0.2s +Uploading to workflow storage... + Uploaded in 2.4s +Launching workflow... + Launched in 1.9s +{ + "runId": "46041b32-e6c8-488d-9c42-226bd356bf0d", + "status": "pending", + "launchJobId": "9cd2c631-4681-4fdd-9a60-1144bdcbcc70" +} + +exit=0 diff --git a/evidence/dev-declared-streams-2026-09-20/cloud-dev-drive-logs.log b/evidence/dev-declared-streams-2026-09-20/cloud-dev-drive-logs.log new file mode 100644 index 000000000..1254b3737 --- /dev/null +++ b/evidence/dev-declared-streams-2026-09-20/cloud-dev-drive-logs.log @@ -0,0 +1,12 @@ + +# 2026-09-20T17:12:11.930122+00:00 +$ python3 /tmp/dev-read.py /api/v1/workflows/runs/46041b32-e6c8-488d-9c42-226bd356bf0d/logs +{"path": "/api/v1/workflows/runs/46041b32-e6c8-488d-9c42-226bd356bf0d/logs", "http": 200, "response": {"content": "[bootstrap] Downloading code from S3 (code.tar.gz)...\n[bootstrap] Code extracted to /project/workflows/runs/b022def4-1fd6-4e7d-ba88-c30dd0a2879e\n[bootstrap] Mounted setup-token env for anthropic\n[bootstrap] Mounted credentials for openai at /home/daytona/.codex/auth.json\n[bootstrap] Installing ai-hist...\n\nadded 97 packages in 1s\n\n34 packages are looking for funding\n run `npm fund` for details\n[bootstrap] ai-hist installed; no relayhistory assertion was provided for this run\n[bootstrap] Setting up git baseline in /project/workflows/runs/b022def4-1fd6-4e7d-ba88-c30dd0a2879e...\n[bootstrap] Creating file manifest for baseline...\n[bootstrap] Baseline committed with 1328 tracked files (clean tree).\n[bootstrap] Seeding relayfile workspace with initial code (attempt 1/3)...\n[bootstrap] Relayfile seed complete in 28022ms\n[bootstrap] Started relayfile-mount daemon\n[bootstrap] Relayflow v2 helper mount root: /project\n", "offset": 921, "totalSize": 921, "done": false}} + +exit=0 + +# 2026-09-20T17:12:52.575309+00:00 +$ python3 /tmp/dev-read.py /api/v1/workflows/runs/46041b32-e6c8-488d-9c42-226bd356bf0d/logs +{"path": "/api/v1/workflows/runs/46041b32-e6c8-488d-9c42-226bd356bf0d/logs", "http": 200, "response": {"content": "[bootstrap] Downloading code from S3 (code.tar.gz)...\n[bootstrap] Code extracted to /project/workflows/runs/b022def4-1fd6-4e7d-ba88-c30dd0a2879e\n[bootstrap] Mounted setup-token env for anthropic\n[bootstrap] Mounted credentials for openai at /home/daytona/.codex/auth.json\n[bootstrap] Installing ai-hist...\n\nadded 97 packages in 1s\n\n34 packages are looking for funding\n run `npm fund` for details\n[bootstrap] ai-hist installed; no relayhistory assertion was provided for this run\n[bootstrap] Setting up git baseline in /project/workflows/runs/b022def4-1fd6-4e7d-ba88-c30dd0a2879e...\n[bootstrap] Creating file manifest for baseline...\n[bootstrap] Baseline committed with 1328 tracked files (clean tree).\n[bootstrap] Seeding relayfile workspace with initial code (attempt 1/3)...\n[bootstrap] Relayfile seed complete in 28022ms\n[bootstrap] Started relayfile-mount daemon\n[bootstrap] Relayflow v2 helper mount root: /project\n[bootstrap] Starting workflow execution (per-step-sandbox)\n[bootstrap] Relayflow v2 runtime verified {\n sourceCommit: '0e549c37a74cfd61e34936e3d664f2619d463d02',\n protocolVersion: '0'\n}\n[bootstrap] Flow parked, needs human recovery: Relayflow v2 run parked and needs human recovery: Run \"01M2ZWVRDBM0757P0578C4BT7S\" parked at step \"assess-1\" (agent): no worker is attached for step type \"agent\".\n", "offset": 1319, "totalSize": 1319, "done": true}} + +exit=0 diff --git a/evidence/dev-declared-streams-2026-09-20/cloud-dev-drive-patch.log b/evidence/dev-declared-streams-2026-09-20/cloud-dev-drive-patch.log new file mode 100644 index 000000000..2ba4b1ad1 --- /dev/null +++ b/evidence/dev-declared-streams-2026-09-20/cloud-dev-drive-patch.log @@ -0,0 +1,6 @@ + +# 2026-09-20T17:14:12.803842+00:00 +$ python3 /tmp/dev-read.py /api/v1/workflows/runs/46041b32-e6c8-488d-9c42-226bd356bf0d/patch +{"path": "/api/v1/workflows/runs/46041b32-e6c8-488d-9c42-226bd356bf0d/patch", "http": 200, "response": {"patch": "", "hasChanges": false}} + +exit=0 diff --git a/evidence/dev-declared-streams-2026-09-20/cloud-dev-drive-status.log b/evidence/dev-declared-streams-2026-09-20/cloud-dev-drive-status.log new file mode 100644 index 000000000..2d7ed8481 --- /dev/null +++ b/evidence/dev-declared-streams-2026-09-20/cloud-dev-drive-status.log @@ -0,0 +1,230 @@ + +# 2026-09-20T17:09:18.234786+00:00 +$ python3 /tmp/cloud-dev-run-cli.py cloud status 46041b32-e6c8-488d-9c42-226bd356bf0d --json +{ + "runId": "46041b32-e6c8-488d-9c42-226bd356bf0d", + "sandboxId": "6bf08460-eb79-4d5c-8e4d-e6a08c90a4df", + "dispatchType": "sandbox", + "relayflowVersion": "v2", + "relayflowV2Authority": { + "source": { + "sha256": "0685c1462380e547379b315d18bb14a87ea504fbb44365fb78651bbc37cb6213", + "fileType": "yaml", + "byteLength": 28888, + "executionSha256": "5b248ca09e4619b6a75421ed28089cb96e63060662c1c3fc8c2a4663489dab49" + }, + "artifact": { + "key": "system/relayflow-v2/6ebfed9b28e28052276afea10604c11401eae4994640171899891279333657ec.tar.gz", + "sha256": "6ebfed9b28e28052276afea10604c11401eae4994640171899891279333657ec", + "sourceCommit": "0e549c37a74cfd61e34936e3d664f2619d463d02", + "protocolVersion": "0", + "manifestSchemaVersion": 1 + }, + "consumerEpoch": "relayflow-v2-2026-09-02.1", + "schemaVersion": 1, + "relayfileMount": { + "paths": [ + "/workflows/runs/b022def4-1fd6-4e7d-ba88-c30dd0a2879e" + ], + "scope": { + "key": "b022def4-1fd6-4e7d-ba88-c30dd0a2879e", + "kind": "run" + } + } + }, + "userId": "1eb0dd9f-509f-4025-8343-dfabd82b6b5c", + "workspaceId": "494ab549-4ace-4202-9127-6d15d946d580", + "workflow": "{\"version\":\"0.1.0\",\"name\":\"flows-drive-cloud\",\"description\":\"The Lead's tick, shaped for a cloud sandbox with the laptop closed.\\nA cloud sandbox has no git remote and no GitHub token, so this flow\\nnever delivers: it runs 1 full work-package cycles back to back\\nin ONE sandbox, committing each to the sandbox branch. Recover the work\\nwith `agent-relay cloud sync `. Nothing reaches main without a\\nhuman. GENERATED from workflows/drive.yaml by ops/gen-drive-cloud-v2.py.\\n\",\"budget\":{\"maxWallclockMs\":3600000},\"steps\":[{\"type\":\"deterministic\",\"command\":\"# Materialize the repo; never assume it. This step assumed a clone\\n# with an `origin` remote and so every cloud tick died here with\\n# `fatal: 'origin' does not appear to be a git repository` (runs\\n# 9fc8d996, ff35187a, 06505b94, 4cf36ea7, b33c2c9a).\\n#\\n# A cloud workflow sandbox does NOT get a clone. The platform's own\\n# materialization is the code sync: the CLI tars the `git ls-files`\\n# set and the bootstrap extracts it into the code mount, then runs\\n# `git init` over it. Files yes, `.git` history and remotes no.\\n# A checkout with a remote only exists on a host that already has one\\n# (laptop, fleet node). Both shapes are supported below; neither is\\n# assumed, and an unmaterialized sandbox fails closed and typed\\n# rather than failing later as an unexplained tool error.\\nset -eu\\necho \\\"SYNC_WORKDIR=$(pwd)\\\"\\n\\nmissing=\\\"\\\"\\nfor required in AGENTS.md docs/RFC-0001-everything-is-a-relayflow.md ops/DIRECTIVES.md kernel packages/sdk; do\\n [ -e \\\"$required\\\" ] || missing=\\\"$missing $required\\\"\\ndone\\nif [ -n \\\"$missing\\\" ]; then\\n echo \\\"SYNC_FAIL_NOT_MATERIALIZED: Cloud did not materialize the scheduled code snapshot.\\\" >&2\\n echo \\\" missing:$missing\\\" >&2\\n echo \\\" cwd: $(pwd)\\\" >&2\\n echo \\\" Recreate the schedule with a current agent-relay CLI so Cloud stores its code snapshot.\\\" >&2\\n exit 78\\nfi\\necho \\\"SYNC_MATERIALIZED=ok\\\"\\n\\n# Fail fast on a stale tree. A per-step sandbox can be seeded from an\\n# older orchestrator archive, and five consecutive runs burned ~20\\n# minutes each producing diffs that reverted merged work — a stale\\n# tree diffed against fresh main looks like a wholesale revert. The\\n# guards at delivery caught them, but only after the cost was paid.\\n#\\n# ops/FORBIDDEN_PATHS lists paths that must NOT exist. Their presence\\n# here means this sandbox is not the tree we uploaded, and nothing\\n# built on it can be trusted.\\nif [ -f ops/FORBIDDEN_PATHS ]; then\\n stale=\\\"\\\"\\n while IFS= read -r forbidden; do\\n case \\\"$forbidden\\\" in ''|\\\\#*) continue ;; esac\\n [ -e \\\"$forbidden\\\" ] && stale=\\\"$stale $forbidden\\\"\\n done < ops/FORBIDDEN_PATHS\\n if [ -n \\\"$stale\\\" ]; then\\n echo \\\"SYNC_FAIL_STALE_TREE: this sandbox contains paths that do not exist on the base:\\\" >&2\\n for p in $stale; do echo \\\" $p\\\" >&2; done\\n echo \\\" The workspace was seeded from an older archive, so it is not the tree\\\" >&2\\n echo \\\" that was uploaded. A diff computed from it reverts merged work.\\\" >&2\\n echo \\\" Failing now rather than spending a full cycle to produce an unusable diff.\\\" >&2\\n exit 75\\n fi\\nfi\\n\\n# The checkout must be rooted HERE: `git rev-parse --git-dir` also succeeds\\n# inside a parent repository, which would send fetch/checkout into the\\n# parent (scheduled steps run under /project/workflows/schedules/), and\\n# a leftover `.git` gitfile would make `git init` fail.\\nif [ \\\"$(git rev-parse --show-toplevel 2>/dev/null)\\\" != \\\"$(pwd -P)\\\" ]; then\\n if [ -f .git ]; then mv .git \\\".git.stale.$$\\\"; fi\\n git init -q\\nfi\\ngit config user.email \\\"lead@relayflows.local\\\"\\ngit config user.name \\\"Relayflow Lead\\\"\\n\\nif git remote get-url origin >/dev/null 2>&1; then\\n # Real checkout (laptop / fleet node): take the true origin/main.\\n echo \\\"SYNC_MODE=remote\\\"\\n git fetch --quiet origin\\n git checkout --quiet -B main origin/main\\n base=$(git rev-parse --short origin/main)\\nelse\\n # Sandbox snapshot: there is no remote to fetch and nothing to\\n # rebase onto. The snapshot IS the base. Commit it so the tick has\\n # a parent to diff against — `git diff main` in the review step\\n # needs a `main` that exists.\\n echo \\\"SYNC_MODE=snapshot\\\"\\n if ! git rev-parse --verify --quiet HEAD >/dev/null 2>&1; then\\n git add -A\\n git commit --quiet -m \\\"snapshot base for this tick\\\" || true\\n fi\\n git branch --quiet -f main HEAD 2>/dev/null || git checkout --quiet -b main\\n base=$(git rev-parse --short HEAD)\\nfi\\n\\ngit checkout --quiet -B \\\"flow/drive-${base}-$(date +%m%d%H%M)\\\"\\necho \\\"SYNC_BASE=$base\\\"\\necho \\\"SYNC_BRANCH=$(git rev-parse --abbrev-ref HEAD)\\\"\\necho SYNCED\\n\",\"id\":\"sync\"},{\"type\":\"agent\",\"dependsOn\":[\"sync\"],\"verification\":{\"type\":\"output_contains\",\"value\":\"ASSESS_DONE\"},\"id\":\"assess-1\",\"instruction\":\"The Relayflow Lead. Assesses state, plans one work package, reports honestly.\\n\\nYou are the Relayflow Lead (charter/LEAD.md). Assess the repo.\\n\\nYOUR SCOPE IS THE TASK YOU WERE GIVEN. Two launchers exist and they\\ndeliver it differently: ops/launch-gate.sh commits an ops/TARGET.md\\nnaming one gate, while the autodrive loop passes the task directly\\nand writes NO TARGET.md. If ops/TARGET.md is absent that is normal —\\nit is not missing context and there is nothing to go looking for.\\n\\nEither way: QUOTE the scope into ops/NEXT.md, never cite the path.\\nTARGET.md lives only in the throwaway launch worktree and is NOT in\\nthe delivered diff, so a reviewer sees a reference to a file that\\ndoes not exist. Review flagged that on PR #19 and again on #35, #40\\nand #48 — it is now enforced in verify, which REFUSES a NEXT.md that\\ncites a path not present in the tree. Anything you rely on must\\nappear in the package itself.\\n\\nAnd when you state that something passes, paste the literal command\\nand its output. \\\"Three tests pass\\\" with no captured output is not a\\nclaim a reviewer can check, and it was also flagged on PR #19. This\\nis AGENTS.md's central standard, applied to your own reporting. It is the operator's scoping decision and it overrides your\\nown judgement about priority — several runs execute in parallel, each\\npinned to a different gate, and a run that wanders outside its target\\nwill collide with a sibling. Stay inside it or, if the target is\\ngenuinely unreachable, say so in ops/NEEDS_HUMAN.md rather than\\nsilently choosing different work.\\nThen read ops/STATE.md — it is ground truth about gates and open\\nPRs for an environment with no git history, and it names the known\\nsandbox faults that are NOT reasons to block. Then read\\nops/DIRECTIVES.md — standing human directives outrank the backlog;\\nif one is unsatisfied, it IS the work package.\\nThen read docs/bootstrap-report.md and ops/DRIVE-LOG.md if they exist,\\n`git log --oneline -15`, `gh pr list --state open` and open PR review\\nstate, kernel/ and packages/sdk/ test status. Then write ops/NEXT.md: the\\nSINGLE highest-priority work package toward the current gate\\n(gate 1 until its done-when in RFC-0001 §3 holds), with: objective,\\nfiles in scope, definition of done (must include passing commands),\\nand what is explicitly OUT of scope for this tick. If an open PR is\\nawaiting fixes from review, the work package is fixing it — never\\nstart new work over unfinished work. If work is blocked on a human\\ndecision, write ops/NEEDS_HUMAN.md stating the exact question and the\\noptions — and then STILL end with ASSESS_DONE.\\n\\nCOMMIT YOUR WORK PACKAGE BEFORE YOU FINISH:\\n git add -A && git commit -m \\\"assess: work package for this tick\\\"\\nEach step runs in its OWN sandbox and files reach the next step only\\nthrough the executor's propagation, which is lossy: on runs a2089144\\nand 2560e02d your predecessor wrote ops/NEXT.md, said so truthfully,\\nand the file never arrived — one of those runs finished with a\\nzero-file patch. Committing puts the package in git history rather\\nthan leaving it as a loose working-tree file. If the commit fails,\\nsay so in your output rather than finishing silently. The assess-gate step\\nbelow reads that file and parks the run with a typed outcome.\\nALWAYS end with ASSESS_DONE, blocked or not: this gate cannot tell a\\ndifferent final token from a crashed agent, so on run 54ebd998 the\\nLead correctly reported BLOCKED_NEEDS_HUMAN three times and was\\nscored as failing three times. Saying you are blocked is a result,\\nnot a failure — but it must be said in the file, not the token.\\n\",\"cli\":\"claude\",\"surfaces\":{\"streams\":[{\"stream\":\"flows-drive-cloud\"}]}},{\"type\":\"deterministic\",\"dependsOn\":[\"assess-1\"],\"command\":\"# A typed park, not a crash. The assess step cannot express \\\"blocked\\\"\\n# in its final token (its gate only recognises ASSESS_DONE), so the\\n# Lead writes ops/NEEDS_HUMAN.md instead and this step reads it.\\nset -u\\n# An escalation is trusted only when TWO INDEPENDENT SIGNALS AGREE:\\n# the file exists AND this tick is what wrote it.\\n#\\n# Existence alone is not a signal. ops/NEEDS_HUMAN.md was committed to\\n# main on 2026-09-06 (082c62aa) and nothing in this repo has ever\\n# deleted it — no `rm`, no `git rm`, and `git log --diff-filter=D`\\n# over that path is empty. ops/launch-gate.sh builds each run's\\n# worktree from origin/main and does not strip it, so every tick from\\n# 2026-09-12 onward escalated here before doing any work, on a\\n# question a human had already answered. PRs #417, #420, #422, #424,\\n# #426, #427 and #428 are seven consecutive cloud runs whose entire\\n# diff is this file and ops/NEXT.md, re-litigating the same conflict.\\n# None merged. A full cloud run was burned on each.\\n#\\n# A stale file must never be able to masquerade as a live escalation.\\n# \\\"This tick\\\" is the same test the ops/NEXT.md freshness check below\\n# uses — a commit in `main..HEAD` — widened by the uncommitted case,\\n# because propagation between per-step sandboxes is lossy and assess\\n# may write the file and fail to commit it. Losing a live escalation\\n# is the worse error of the two, so an unproven-fresh file that is\\n# dirty in the working tree still parks the run.\\n#\\n# The default is to TRUST the escalation. Only a positive, SUCCESSFUL\\n# answer from git may downgrade it to stale, because \\\"git printed\\n# nothing\\\" and \\\"git could not answer\\\" look identical otherwise — and\\n# a sandbox is exactly where git cannot answer. SYNC_MODE=snapshot\\n# runs `git init` over an extracted tarball, so a step that runs\\n# before main exists, a missing .git, or any git failure would\\n# silently classify a LIVE escalation as stale and walk the builder\\n# straight past a human decision. That inverts the tradeoff above,\\n# so an unprovable escalation parks the run.\\nif [ -f ops/NEEDS_HUMAN.md ]; then\\n escalation=unprovable\\n if git rev-parse --git-dir >/dev/null 2>&1 \\\\\\n && git rev-parse --verify --quiet main >/dev/null 2>&1; then\\n tick_log=$(git log --oneline main..HEAD -- ops/NEEDS_HUMAN.md 2>/dev/null)\\n if [ $? -ne 0 ]; then\\n escalation=unprovable\\n elif [ -n \\\"$tick_log\\\" ]; then\\n escalation=this_tick_committed\\n else\\n tick_dirty=$(git status --porcelain -- ops/NEEDS_HUMAN.md 2>/dev/null)\\n if [ $? -ne 0 ]; then\\n escalation=unprovable\\n elif [ -n \\\"$tick_dirty\\\" ]; then\\n escalation=this_tick_uncommitted\\n else\\n escalation=stale\\n fi\\n fi\\n fi\\n if [ \\\"$escalation\\\" = stale ]; then\\n echo \\\"ASSESS_STALE_NEEDS_HUMAN_IGNORED: ops/NEEDS_HUMAN.md exists but this tick did not write it.\\\"\\n echo \\\" It is the committed record of an escalation that has already been answered,\\\"\\n echo \\\" not a live one, so it does not park this run. Delete it from main once its\\\"\\n echo \\\" question is resolved — a resolved escalation left in the tree is a lie the\\\"\\n echo \\\" next assessor has to spend a run disproving.\\\"\\n else\\n if [ \\\"$escalation\\\" = unprovable ]; then\\n echo \\\"ASSESS_ESCALATION_FRESHNESS_UNPROVABLE: git could not say whether this tick\\\"\\n echo \\\" wrote ops/NEEDS_HUMAN.md (no repo, no main, or git failed). Failing safe and\\\"\\n echo \\\" treating it as live: ignoring a real escalation is the worse of the two errors.\\\"\\n fi\\n echo \\\"ASSESS_BLOCKED_NEEDS_HUMAN: the Lead escalated a decision it cannot make ($escalation).\\\"\\n echo \\\"--- ops/NEEDS_HUMAN.md ---\\\"\\n cat ops/NEEDS_HUMAN.md\\n exit 75\\n fi\\nfi\\nif [ ! -f ops/NEXT.md ]; then\\n echo \\\"ASSESS_FAIL: no ops/NEXT.md — an assessment that named no work package did not assess\\\"\\n exit 1\\nfi\\n# The assessment must have WRITTEN this tick's package, not merely\\n# left the previous one in place. On run 457a6102 assess reported\\n# \\\"The work package is written to ops/NEXT.md\\\" and the very next step\\n# read the OLD file — the logs carry the reason:\\n# \\\"relayfile flush failed after the command succeeded (exit 1);\\n# a later agent step may see stale files\\\"\\n# The builder then correctly refused to invent scope, but only after\\n# a whole build step had been spent. Catch it here instead: if\\n# ops/NEXT.md is identical to the base, the assessment did not land,\\n# whoever is at fault.\\n# Look for the package in the working tree OR in a commit made this\\n# tick. Propagation between per-step sandboxes is lossy, so a package\\n# that exists only as a loose file may not arrive; one committed by\\n# the assess step travels in git history instead.\\nif git log --oneline main..HEAD -- ops/NEXT.md 2>/dev/null | grep -q .; then\\n echo \\\"ASSESS_PACKAGE_COMMITTED: found ops/NEXT.md change in this tick's history\\\"\\nelif git diff --quiet main -- ops/NEXT.md 2>/dev/null; then\\n # Warn, do not fail. This was fatal, and it killed four runs in six\\n # while the loop produced nothing — a worse outcome than the risk\\n # it guarded against.\\n #\\n # The risk it guarded was \\\"the builder gets scope nobody wrote this\\n # tick\\\". But scope does not actually come from ops/NEXT.md: it comes\\n # from ops/TARGET.md, which the launcher COMMITS into the uploaded\\n # tree, so it is present in every per-step sandbox and cannot be\\n # lost to the propagation fault. NEXT.md refines the target; it does\\n # not define it.\\n echo \\\"ASSESS_WARN_STALE_NEXT: ops/NEXT.md did not change from the base commit.\\\"\\n echo \\\" The assess step's package did not survive the step boundary (a known\\\"\\n echo \\\" platform fault: per-step sandboxes lose both loose files and git objects).\\\"\\n echo \\\" Proceeding, because ops/TARGET.md is committed in the tree and carries this\\\"\\n echo \\\" run's scope. The builder is not working blind — it is working from the\\\"\\n echo \\\" target rather than from a refinement of it.\\\"\\n if [ -f ops/TARGET.md ]; then\\n echo \\\"--- ops/TARGET.md (the scope that did survive) ---\\\"\\n head -8 ops/TARGET.md\\n else\\n echo \\\"ASSESS_FAIL_NO_SCOPE: neither a fresh ops/NEXT.md nor an ops/TARGET.md.\\\"\\n echo \\\" With no scope from either source the builder WOULD be working blind.\\\"\\n exit 1\\n fi\\nfi\\n# A package with no definition of done cannot be verified, and the\\n# builder cannot honestly report BUILD_DONE against it.\\n# A package must be verifiable, but do not dictate its wording. This\\n# check demanded the literal phrase \\\"definition of done\\\" and so\\n# rejected a CORRECT assessment three times on run 30475b25 — one\\n# that reported gate 2's primitives already complete and proposed\\n# moving to gate 3, and was right on both counts. A gate that\\n# rejects true reports is as bad as one that accepts false ones.\\n#\\n# Accept either shape: a runnable command (that is what \\\"verifiable\\\"\\n# actually means), or an explicit statement that this tick has no\\n# buildable package.\\nif grep -qiE \\\"definition of done|definition-of-done|done when|done-when|acceptance criteria\\\" ops/NEXT.md \\\\\\n || grep -qE \\\"(cargo|npm|node|sh|pytest) [a-z]\\\" ops/NEXT.md \\\\\\n || grep -qiE \\\"no buildable work|nothing to build|assessment only|gate .* is (green|complete)\\\" ops/NEXT.md; then\\n :\\nelse\\n echo \\\"ASSESS_FAIL_NO_DOD: ops/NEXT.md names neither a runnable command nor a\\\"\\n echo \\\" statement that this tick has no buildable package. A work package that\\\"\\n echo \\\" cannot be verified cannot be built against.\\\"\\n exit 1\\nfi\\necho \\\"ASSESS_GATE_PASS ($(grep -m1 -oE 'WP-[0-9]+[^|]*' ops/NEXT.md || echo 'work package'))\\\"\\n\",\"timeoutMs\":120000,\"id\":\"assess-gate-1\"},{\"type\":\"agent\",\"dependsOn\":[\"assess-gate-1\"],\"maxIterations\":3,\"verification\":{\"type\":\"output_contains\",\"value\":\"BUILD_DONE\"},\"id\":\"build-1\",\"instruction\":\"Implements the work package. Rust for kernel/, TypeScript for packages/sdk/.\\n\\nRead ops/NEXT.md, AGENTS.md, and the relevant parts of\\ndocs/RFC-0001-everything-is-a-relayflow.md. Implement exactly that\\nwork package — nothing more. Run the definition-of-done commands\\nyourself and iterate until they pass. Keep files small and\\nsingle-purpose. End with BUILD_DONE only when the definition of done\\npasses locally; paste the passing output.\\n\",\"cli\":\"codex\",\"surfaces\":{\"streams\":[{\"stream\":\"flows-drive-cloud\"}]}},{\"type\":\"deterministic\",\"dependsOn\":[\"build-1\"],\"command\":\"# CLOUD VARIANT (generated): a FAILED verify is recorded and the\\n# run continues. Nothing is delivered from a sandbox, so a failure\\n# here cannot ship; the next cycle's assess treats it as the work\\n# package. On a delivering environment verify stays fatal.\\n# A gate that cannot fail is not a gate. Never pipe a test command\\n# into tail inside the status check: the pipeline's status is tail's.\\nset -u\\nran=0; ok=0\\n\\n# Bound every long-running command, not just the suites. Run\\n# 6d045b23 sat in verify for 29+ minutes: its suites were bounded but\\n# `cargo build` and `npm ci` were not, so a cold sandbox installing a\\n# toolchain and compiling from scratch had no ceiling at all.\\n# Bounding half the step is not bounding the step.\\n#\\n# timeoutMs is NOT enforced by the platform —\\n# observed three times on 2026-08-28 (verify-1 at 31min against a\\n# 20min bound, review-1 at 36min against 30min, plus an unbounded\\n# toolchain install). And the kernel suite now contains a test that\\n# intermittently hangs under sandbox timing:\\n# an_entry_appended_during_watch_registration_is_delivered_exactly_once\\n# ran past 60s in cloud while passing locally in 0.54s. Without a\\n# bound here, one hanging test consumes the entire run budget.\\nrun_bounded() {\\n _label=\\\"$1\\\"; shift\\n if command -v timeout >/dev/null 2>&1; then\\n timeout \\\"${VERIFY_SUITE_TIMEOUT:-900}\\\" \\\"$@\\\"\\n elif command -v gtimeout >/dev/null 2>&1; then\\n gtimeout \\\"${VERIFY_SUITE_TIMEOUT:-900}\\\" \\\"$@\\\"\\n else\\n echo \\\"VERIFY_WARN: no timeout(1); $_label runs unbounded\\\" >&2\\n \\\"$@\\\"\\n fi\\n}\\n\\nif [ -d kernel ]; then\\n # Invoke through `sh`: in a cloud sandbox this script was present\\n # but not executable (observed on run 4cf36ea7). Git tracks it as\\n # mode 100755, so the exec bit is lost somewhere in materialization\\n # — which stage is NOT established, so no mechanism is claimed here.\\n # `sh