wp-durable · Extensions handover (Change 4, 2026-10-11)

Status: E1 Jev accepted on b08cf7aa; E2 Apify accepted on fc4eba90; Change 5 accepted on e19d853f; E4 subagent accepted on 28efb268; E3 Mac bridge accepted: spec 1.4 on 7626a6a9, bridge 1.4.0 with spec 1.5 and then bridge 1.4.1 (round 6) on dce3cd62 (the Server build now, with the long-poll cap). Change 4 is complete. Specs: CustomExtension/ (README, jev.md, apify.md, mac-bridge.md, subagent.md). Implementing session: wpDurable; accepting session: piinchrome-1c. The extensions are built in parallel and delivered in any order.

Deployments

Deployment Date Contents Acceptance
b08cf7aa-c515-4751-9473-775349c54249 2026-10-11 E1 Jev (main 9e69711); Apify and bridge placeholders E1 accepted 10/10 (piinchrome-1c)
fc4eba90-1ced-498d-ba5a-001027eef566 2026-10-11 E2 Apify spec 1.2 (main 4a00fd3), review fixes of the foundation and E1, GET // crash fix; bridge placeholder E2 accepted 13/14 (piinchrome-1c; the miss was the checker's expectation)
e19d853f-2ae1-49c5-a913-69b8ee4de23b 2026-10-11 Change 5 (main 22ac962): prompt sections only for offered tools, no-tools section, output cap as the provider's max-tokens Change 5 accepted 12/12 (piinchrome-1c)
28efb268-cbde-4f60-9647-ca66b633cd86 2026-10-11 E4 subagent spec 1.1 (main f18fb7a): two adversarial reviews closed; bridge placeholder checked by wpDurable (haiku foreground delegation, conversations 736 → 750); E4 accepted 13/13 (piinchrome-1c)
7626a6a9-5148-4e0f-8e6c-fa28d8a03372 2026-10-11 E3 Mac bridge spec 1.4 (main 8709e8f): rounds 1–4, three adversarial reviews closed; Server: /v1/bridge/connect, /v1/bridges, bridge:// cwd; Mac program version 1.3.0 (npm run bridge:install) checked by wpDurable (bridge present, 0 connected, /v1/bridges empty, the endpoint 401 without a token and with the client token, cwd: "/tmp" 400, healthz tools 8, logs clean); E3 accepted: Mac side, online 14/14, offline 2/2, back 3/3 (piinchrome-1c)
dce3cd62-c88b-490e-b80c-335bf020eb6e 2026-10-11 Long polls held at most 25 s, WP_DURABLE_LIMIT_WAIT_MS (main 9b43360; Server only, bridge program unchanged); deployed during a running bash job as the resume test checked by wpDurable (healthz ok, limits.waitMs 25000, the bridge reconnected, logs clean); resume test 8/8 and the final regression all green (piinchrome-1c): change2 19, m2 22, v2 26, claude 13, c5 12, e4 13, e1 10, e2 14 + long run, wait-cap 6/6, E3 phases

Variables

Variable Kind Used by
JEV_ACCESS_KEY secret (from .secrets/jev-access-key) jev_decide
APIFY_ACCESS_KEY secret (from .secrets/apify-access-key) apify_run
WP_BRIDGE_TOKEN secret (from .secrets/wp-bridge-token) the bridge's WebSocket connection
WP_DURABLE_JEV_BASE_URL optional Jev Worker (default https://x402jev.stevenevans669.workers.dev)
WP_DURABLE_APIFY_BASE_URL optional Apify Worker (default https://x402apify.stevenevans669.workers.dev)
WP_DURABLE_APIFY_DAILY_USD optional Apify daily budget in USD (default 2.00)
WP_DURABLE_LIMIT_SUBAGENT_PARALLEL optional live subagent children per parent (default 4)
WP_DURABLE_LIMIT_SUBAGENT_DEPTH optional subagent depth; only 1 is accepted in this version

The three secrets follow Server v2 §6: read once at start, removed from process.env, masked by redact(), never in the bash environment, a tool result, a log line, a response body, or a work directory.

Shared changes (foundation)

Jev (E1)

Spec CustomExtension/jev.md 1.0. Module src/extensions/jev/ (index.ts the extension and tool, validate.ts the Worker's request rules). Tool jev_decide, extension jev, prompt section jev.

Limits: jevTimeoutMs 35000 (WP_DURABLE_LIMIT_JEV_TIMEOUT_MS; longer than the Worker's own 30 s upstream timeout, so its 504 arrives and is retried once); body 1 MiB (the Worker's, fixed); answers over 64 KB as compact JSON, tool output limits 2 MiB and 200000 lines (the Engine never cuts an answer).

Review fixes after acceptance (adversarial review of the foundation and E1, shipped with E2): state, instructions, criteria, and model are untyped in the tool schema, because the Harness converted values the Worker refuses (null to "null", 5 to "5") before the Server's check; the check now sees the raw values. Large answers are no longer cut by the Engine's default 50 KB / 2000-line limit. Timeout default 35 s (was 30 s).

Tests: test/unit/jev.test.ts (39: every Worker rule, request shape, every status, retries and retry-after, 401 state, key redaction in results and logs, usage, timeout, abort), test/e2e/jev.test.ts with the local fake test/e2e/fake-jev.ts (8: listing and section, three batched questions in one request with usage and the recorded <jev> section, 429 then success, 422, a refused call that sends nothing, tool restriction, no key in responses, entries or logs, key missing → not offered). Live check before deploy (local Server, real Worker): two questions, sunny confidence 1, 350 input tokens, 520 ms.

Acceptance (piinchrome-1c, production conversation 512, 10/10): status and tools with the section text and replay safe; the weather choice → sunny, confidence 1, usage under pi.usage.tools.jev_decide (331 in / 41 out, $0.0000139); five batched questions in one call all answered; a one-option choice refused by the Server's own check with the Worker's message; no token or key in the 13 entries; docs pages fine.

Known limitations: the 401 state is per process (a restart shows present until the next call); a timeout of our own (no answer in 30 s) is not retried.

Apify (E2)

Built to CustomExtension/apify.md 1.1. Module src/extensions/apify/:

Booking order of one call: Server-side check (a refusal sends nothing and books nothing) → one commit books the cap on wp.spend → the Worker request with that cap → a second commit gives the cap back when the answer proves that no run started (400, 401, 413, 429, 422 upstream_rejected, 502 upstream_refused, 503 upstream_busy).

Review fixes (adversarial review of E2): a 422 upstream_rejected is refunded only for Apify's invalid-input (a run-failed 422 follows a charged run); wp.spend is stored whole on every change; a refund after 00:00 UTC is remembered under the document's day; the default cap follows the Worker's announced bound; output limits so the Engine never cuts a shown result; a throw while saving keeps the items and usage; secrets masked before the summary is cut; the Worker's error code masked in logs and details; WP_DURABLE_LIMIT_APIFY_CLIENT_SLACK_MS (25 s).

Spec 1.1: an empty dataset (200 { items: [] }, for example when rag-web-browser's search step times out) is a non-error result starting apify_run: 0 items — the Actor finished without data; …, with the booking kept. A reported usage.totalUsd replaces the cap in wp.spend and in the tool's cost (details.chargedUsd); inert until the Worker sends it.

Limits and variables:

Name Default Override
Items JSON shown inline 64 KB WP_DURABLE_LIMIT_APIFY_INLINE_BYTES
Items shown when saved to a file 16 KB WP_DURABLE_LIMIT_APIFY_SUMMARY_BYTES
Daily budget 2.00 USD WP_DURABLE_APIFY_DAILY_USD
Worker https://x402apify.stevenevans669.workers.dev WP_DURABLE_APIFY_BASE_URL
Client wait beyond the run's timeout 25 s WP_DURABLE_LIMIT_APIFY_CLIENT_SLACK_MS
Largest answer read 64 MB WP_DURABLE_LIMIT_APIFY_ANSWER_BYTES
Actor list refresh 1 h none

Tests: test/unit/apify.test.ts 56 (validation per rule, budget arithmetic, request shaping against a local HTTP server, every error status with its refund decision, large-result spill, key redaction); test/e2e/apify.test.ts 10 with the local fake Worker test/e2e/fake-apify.ts (three items with the cap sent and booked and the usage recorded; an unknown actor refused without a request; about 2 MB of items saved under downloads/; 429 with retry-after and the cap given back; bookings across a restart; the budget spent by one of two parallel calls, then the tool not offered, also after a restart; a kill during a run: interrupted, not sent again, cap kept; no key: not offered, state missing).

Deviations from the spec (also in the spec's decision log):

Known limitations:

Acceptance (piinchrome-1c, production conversation 599, 13/14; the miss was the checker's own expectation): status with three actors and the 2 USD budget; replay unsafe, section without cheerio; a one-page crawl of 76 KB saved to downloads/apify-612.json and read with bash; a rag-web-browser search with 1 item; an unknown actor and a pageFunction input refused with nothing sent or booked; bookings 0.05 + 0.05 + 0.10 + 0.10 = 0.30 USD; a 74 s website-content-crawler run (12 pages, playwright:firefox) from Railway completed with 15 items; usage recorded; no token or key in the entries.

Subagent (E4)

Built to CustomExtension/subagent.md 1.1 from pi's reference code at tag v1.1.0 (MIT, the release that matches the installed pi-durable 1.1.0): packages/coding-agent/src/experimental/durable/subagent.ts (foreground), packages/coding-agent/src/experimental/vacation/vacation.ts (background: a task that owns the child and reports), and packages/durable/test/examples/22-subagent-foreground.ts, 23-subagent-background.ts. Module src/extensions/subagent/ (derive.ts: the call's checks, the child's agent change, the live-children count, request ids and report texts; index.ts: the tool, the reporter task wp.subagent-reporter, the section).

Limits: subagentParallel 4 (WP_DURABLE_LIMIT_SUBAGENT_PARALLEL; the section names the configured number); subagentDepth 1 (WP_DURABLE_LIMIT_SUBAGENT_DEPTH, any other value stops the start).

Test support: faux test mode accepts POST /v1/test/faux/responses with { rules: [{ when, ...step }], count? }: each response answers with the first rule whose when occurs in the request's last message, so a parent and its children can share the one faux queue in any order.

Tests: test/unit/subagent.test.ts 22 (call checks, child derivation, live-children count, request ids, the spec 1.1 report frame with a forged header kept inside the fence and a closing marker in any letter case escaped, the section's last sentence, depth, and the exactly-once windows: a rerun of a background call finds its wp.subagent-calls entry, a rerun of a foreground call finds its child before any check, a rerun of the reporter's phases sends the same request ids). test/e2e/subagent.test.ts 13 (foreground with children holding the child's own usage and parent, two parallel delegations one on another model, derivation from a restricted parent and an override, the fifth concurrent call refused, parent abort aborting the foreground child, refusals that create nothing, the shared work directory with the child's own WP_CONVERSATION_ID and the 409s, a background child using the parent's work directory before the parent reads what it wrote, a background child going on through a parent abort with its report arriving once, a queued report withdrawn by a parent abort and not posted again, kill and restart mid-delegation finding the same child and submission, background report exactly once across a restart, no subagent in a child's prompt). test/e2e/tools.test.ts now expects six tools without keys.

Acceptance (piinchrome-1c, production, main f18fb7a, 13/13): parent 765 made two subagent calls in one turn, answered in 8 s with "A=391; B=101, 103, 107"; children 780 (gpt-6-sol) and 791 (haiku) carry parent 765 and show in the parent's children with usage; a child has no subagent tool. A background delegation returned "delegated" in 4 s; the report arrived once after 16 s with the spec 1.1 frame, no duplicate after 20 s, and the parent answered it. Aborting parent 839 aborted its foreground child 853 (tasks 849, 852); both idle, the parent's submission unanswered. No token in the entries.

Mac bridge (E3)

Spec: CustomExtension/mac-bridge.md (1.4). Server side src/extensions/bridge/; Mac program bridge/ (README bridge/README.md; npm run bridge:install, then node ~/.wp-bridge/bin/dist/wp-bridge.js --root <name>=<path>); image bridge/Dockerfile (wp-bridge-tools). Round 2 (2026-10-11) applied spec 1.1 and fixed the 32 findings of the adversarial review of c1dfd13. Round 3 (2026-10-11) applied spec 1.2 (decision A: the immutable flag) and 1.3 (git credentials count for the strict check), and fixed the 22 findings of the second review (51508da). Round 4 (2026-10-11) applied spec 1.4 (the bridge never runs with its code in a root) and fixed the 28 findings (14 distinct, one critical) of the third review (a2dd35d, .scratch/review-e3r3.json). Round 5 (2026-10-11) applied spec 1.5 (what tools on the Mac run or obey) and fixed the findings of the fourth review (one high: links in bridge/dist); the Mac program only.

Round 6 (bridge 1.4.1; improvements from the round-5 acceptance; no Server change)

Accepted by piinchrome-1c, all green: Mac-side checks 13/13, host-run probe 15/15 (both stop and recover cycles recovered through the 5 s rescan and the warm-up container; no stall), online 14/14, offline 2/2, back 3/3.

Acceptance of round 5 (piinchrome-1c, bridge 1.4.0 on dce3cd62): Mac-side checks 12/13 (the miss: .scratch/ copies of mise.toml counted, the rule working), host-run probe 14/15 (the miss: one Colima stall), online 14/14, offline 2/2, back 3/3; launcher start from / with only HOME, LANG, LOGNAME, TERM, TMPDIR, USER and WP_BRIDGE_LAUNCHER.

Round 5 (spec 1.5 and the fourth review; bridge 1.4.0; no Server change)

Round 4 (spec 1.4 and the third review)

What was built

Limits

Limit Default Override
Read through a bridge 4 MiB (at most 5 MiB: one frame) WP_DURABLE_LIMIT_BRIDGE_READ_BYTES
Write through a bridge 64 MiB (in 4 MiB parts) WP_DURABLE_LIMIT_BRIDGE_WRITE_BYTES
Wait for a bridge that may be coming back 30 s, within 60 s of a start or a disconnect none (faux tests only: WP_DURABLE_TEST_BRIDGE_WAIT_MS)
Upload idle time (Mac side) 5 min from the last part none

Tests

Acceptance (piinchrome-1c, production 7626a6a9, the bridge run from ~/.wp-bridge/bin in a cmux tab as Stevens-MacBook-Air-2): Mac side (bridge:install self-contained, the start from inside the root refused with bridge:install named, PATH and programs printed, bridge/exclude.txt immutable while running and cleared at Ctrl-C); online 14/14 (list, read, git and python in the container, write → the file on the Mac → rm, .secrets hidden, no network, .git read-only, timeout, no token in 41 entries; container exec 0.1 s); offline 2/2; back 3/3. Observation (low): the first start's uncommitted-changes check did not finish within 60 s (a cold first container on Colima; it passed in about 1 s at the next start).

Known limitations