From fe9bb409ede9ce70d91b2df84880a30a5a41f58e Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Wed, 12 Aug 2026 13:03:20 +0200 Subject: [PATCH 01/73] chore(harness): reopen the 0.0.6 runtime lane for #1457, #1459, #1548 Narrow reopen. Records the grouping decision from located surfaces rather than titles: three distinct files across two packages, no overlap, nothing technically inseparable, so three independent PRs. Routes evaluation through the automatic label-driven policy per D-4/D-5. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TKxrWGp5uxEHQ2NyZiZSMF --- .../supervisor.md | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/.llm/runs/release-0.0.6-features--orchestration/supervisor.md b/.llm/runs/release-0.0.6-features--orchestration/supervisor.md index 8a8fac4c7f..503699bb07 100644 --- a/.llm/runs/release-0.0.6-features--orchestration/supervisor.md +++ b/.llm/runs/release-0.0.6-features--orchestration/supervisor.md @@ -100,3 +100,51 @@ owns the canary and the stable cut; this lane does not publish. - `git fetch origin main` → `origin/main@01aa12b67` (identical; lane starts at tip) - `deno task agentic:runtime doctor` → `no_change (schema 1.0)`, components 18, **sessions 0** (no pre-existing Codex sessions to collide with) + +--- + +# Reopen — 2026-08-12, narrow scope + +The lane was closed with both original issues merged (#1405 `8ff1bcb8f`, #1398 `d7e2b67b2`, control +PR #1525 `0f0b6b6a3`). It is **reopened narrowly** for three newly triaged 0.0.6 public-runtime +blockers. Everything above this line is the closed original run and is not rewritten. + +| Field | Value | +| --- | --- | +| Control branch | `chore/release-0.0.6-runtime-reopen` | +| Baseline | `origin/main@0f0b6b6a3` | +| Scope | **#1457, #1459, #1548 only** — no other issue is adopted | +| Eval policy | **automatic**, per D-4/D-5: label-driven, never a manual OpenHands dispatch | + +## Grouping decision — three separate PRs + +Instructed to group only if technically inseparable. Checked by locating each defect's surface rather +than inferring from titles: + +| Issue | Surface | Shape | +| --- | --- | --- | +| #1457 chat proxy drops State-Protocol query params | `packages/fresh/src/runtime/ai/stream-proxy.ts` | small, fully specified | +| #1459 `DeferComponent` never hydrated | `packages/fresh/src/application/defer/{DeferPage,DeferIsland}.tsx`, `policy.ts` | substantive — hydration + client-bundle regression test | +| #1548 browser resolver cannot see Vite service refs | `packages/plugin-streams-core/src/application/stream-url-resolver.ts` | specified cause, real design choice | + +**No file overlap.** #1457 and #1459 both sit in `packages/fresh` but in unrelated subtrees +(`runtime/ai` vs `application/defer`), so they neither conflict nor share acceptance. #1548 is in a +different package entirely, and `stream-url-resolver.ts` was **not** touched by #1405 (that was the +producer supervisor, façade, and contract) — so there is no interaction with what this lane already +landed. + +Nothing is technically inseparable → **three independent PRs**, dispatchable in parallel. + +## Eval routing under the automatic policy + +- **#1457** — small and fully specified. Initial IMPL-EVAL fires automatically on draft → ready. + `impl-eval:skip` is **not** applied: the instruction is to use the automatic policy, and D-3's + waiver was the source of one prior misjudgement in this lane. Erring toward the automatic route. +- **#1459** — decision-heavy (how the island is registered and how a client-bundle regression test is + built). PLAN-EVAL warranted, triggered by the `openhands` + `status:plan-eval` label pair. +- **#1548** — the cause is known, but the fix is a genuine choice (package-side direct + `import.meta.env.VITE_*` read vs a Vite plugin transform), and it changes a published resolver. + PLAN-EVAL warranted. + +Reruns only by moving away from and back to the relevant `status:` label. One-shot +`eval:model:*` overrides applied per PR only where a specific model is wanted. From eb7d172ea8e4f1df853fa6fd57357218a1dc6a69 Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Wed, 12 Aug 2026 13:05:20 +0200 Subject: [PATCH 02/73] docs(harness): brief the #1457 chat proxy query-forwarding slice Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TKxrWGp5uxEHQ2NyZiZSMF --- .../slices/implement-1457.md | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 .llm/runs/release-0.0.6-features--orchestration/slices/implement-1457.md diff --git a/.llm/runs/release-0.0.6-features--orchestration/slices/implement-1457.md b/.llm/runs/release-0.0.6-features--orchestration/slices/implement-1457.md new file mode 100644 index 0000000000..83ea90c36f --- /dev/null +++ b/.llm/runs/release-0.0.6-features--orchestration/slices/implement-1457.md @@ -0,0 +1,140 @@ +use harness + +# Slice brief — #1457 chat stream proxy drops durable State-Protocol query parameters + +Implementation agent for one small, fully specified slice. **Codex · GPT-5.6 Sol · low** +(`light_implementation`). Do exactly this slice; do not refactor neighbours, and do not touch the +#1459 or #1548 surfaces (`application/defer/**`, `stream-url-resolver.ts`) — they are separate PRs in +this same lane. + +| Field | Value | +| --- | --- | +| Issue | #1457 (`priority:p1`) | +| Worktree | `/home/codex/repos/ns006-1457` | +| Branch | `fix/1457-chat-proxy-query-forwarding` | +| Base | `origin/main@f99cb4fbf` | +| Run dir | `.llm/runs/release-0.0.6-features--orchestration/` | + +## SKILL + +- `netscript-doctrine` — `packages/fresh` is framework code; this changes a **published helper's** + observable behaviour. Read before changing the option surface. +- `deno-fresh` — Fresh 2.x route/handler conventions. +- `netscript-harness` — slice/commit trail, drift recording. +- `netscript-tools` — validation wrappers and what counts as gate evidence. +- `netscript-pr` — draft PR body, closing keyword, phase comments, labels. + +## The defect, exactly + +`createNetScriptChatStreamProxy()` +(`packages/fresh/src/runtime/ai/stream-proxy.ts:159-211`) resolves the upstream URL at `:172`: + +```ts +const upstreamUrl = resolveChatSessionUrl(target, { streamPath }); +``` + +and builds the upstream request from it at `:206` — **without ever reading +`new URL(request.url).search`**. Headers are carefully forwarded (`:179-191`); the query string is +silently dropped. + +A durable chat subscriber calling +`/api/chat-stream?id=session-1&offset=42&live=sse&handle=h1` therefore reaches the durable-stream +service with **no** `offset`, `live`, or `handle`, so resume and live-polling semantics are lost. + +`resolveChatSessionUrl` (`packages/fresh/src/runtime/ai/create-chat-connection.ts:60-66`) delegates +to `buildStreamUrl(resolveChatStreamSubpath(...), baseUrl)`, so the resolved URL **may already carry +its own query**. That matters for the fix — see D2. + +## LOCKED decisions — implement these, do not re-decide + +The issue permits either "forward by default" or "a documented query-mapping hook". **Both**, in this +shape: + +- **D1 — forward the incoming query by default.** Every query parameter on the incoming request is + forwarded to the upstream URL, **except `id`**, which is application routing metadata and is + dropped. This is the zero-config behaviour a consumer gets with no new options, and it is what + removes the consumer-side adapter the issue names. +- **D2 — merge, never clobber.** Because `resolveChatSessionUrl` can return a URL that already has a + query, forwarding must **merge onto** the resolved URL's existing parameters rather than replacing + its search string. On a key collision the **resolved URL's own value wins** — the helper's + configured `streamPath` is more authoritative than client input, and letting a client override it + would be a request-forgery seam. Preserve repeated keys (`URLSearchParams` `append`, not `set`). +- **D3 — an optional documented mapping hook**, e.g. a `query` option receiving the incoming + `URLSearchParams` and returning the parameters to forward. When provided it **replaces** the D1 + default (the caller takes full control, including whether `id` survives). When absent, D1 applies. + This is additive: existing callers compile and behave identically **except** that the query is now + forwarded, which is the fix. +- **D4 — no change to headers, auth, body, streaming, abort, or response sanitization.** The + `accept-encoding: identity` line (`:191`) and the `duplex: 'half'` body handling (`:194-205`) are + load-bearing for other defects; leave them alone. + +If you conclude D1 and D2 conflict for a real case, **stop and report** rather than choosing. + +## Required tests + +The issue names them: `offset`, `live`, `handle`, and `cursor`. + +1. Each of `offset`, `live`, `handle`, `cursor` present on the incoming request **reaches the + upstream URL** with its value intact. Assert against the URL the injected `fetch` actually + receives — not against a helper's return value. +2. `id` is **dropped** from the upstream URL while the others survive (the exact case from the issue: + `?id=session-1&offset=42&live=sse&handle=h1`). +3. **Collision:** when the resolved upstream URL already carries a parameter the client also sends, + the resolved URL's value wins (D2). This pins the anti-forgery direction and would otherwise + regress silently. +4. **Repeated keys** are preserved rather than collapsed. +5. The `query` hook (D3), when supplied, replaces the default and can suppress or add parameters. +6. A no-query request still produces exactly the URL it produces today (no stray `?`). + +Each test must fail if its own behaviour regresses. Use the injectable `options.fetch` seam +(`:163`) to capture the upstream `Request`. + +## Gates — deliverables, not hopes + +```bash +deno run --allow-read --allow-run .llm/tools/run-deno-check.ts --root packages/fresh --ext ts,tsx +deno run --allow-read --allow-run .llm/tools/run-deno-lint.ts --root packages/fresh --ext ts,tsx +deno run --allow-read --allow-run .llm/tools/run-deno-fmt.ts --root packages/fresh --ext ts,tsx +deno task quality:gate +deno task --cwd packages/fresh test +deno task doc:lint --root packages/fresh --pretty +``` + +**Use the package-declared `deno task --cwd test`, never a bare `deno test `** — the bare +form omits `--allow-env` and exits 1 on `NotCapable`. That was a real defect in an earlier brief in +this lane; it is corrected here rather than repeated. + +`quality:gate` is mandatory for a `packages/**` slice. **Check whether its configured roots actually +cover `packages/fresh`** — they demonstrably omit several packages (tracked as **#1542**). If +`packages/fresh` is not covered, run an explicit target scan and say so; do not report the repo gate +as proof for this package. + +Do **not** run `deno task e2e:cli` — this slice does not touch scaffold output, and that gate is +expensive and serialised across the lane. + +**Known hazard:** `deno fmt` rewraps long lines and can silently undo a scripted string edit. After +the format wrapper, re-grep for the parameter names you introduced and confirm they are still there. + +## Commit trail + +1. Open a **draft PR against `main`** in the same session as your first commit. Title: + `fix(fresh): forward durable State-Protocol query parameters through the chat stream proxy`. + Body per `netscript-pr`: `Closes #1457` in `## Scope`, run-dir path, slice checklist, Definition of + Done, and a fenced `acceptance-evidence` block. **Use `box-index` entries**, not full-text `box:` + matching — this issue's expectations wrap across lines and the mirror matches a checkbox's first + line. Labels `type:fix`, `area:fresh`, `area:plugin-ai`, `area:streams`, `status:impl`, milestone + `0.0.6`. +2. Commit per slice, push by **explicit refspec** + (`git push origin HEAD:refs/heads/fix/1457-chat-proxy-query-forwarding`), and post a + `[PHASE: IMPL]` comment with the commit hash and **pasted real gate output**. +3. Keep your slice worklog current in the same commit. + +## Reporting contract + +Report: what changed and where; the exact test names and what each would catch; verbatim gate output; +and **anything you could not do, could not verify, or that surprised you**. A red gate reported is +useful; a red gate worked around silently is the failure mode this brief exists to prevent. + +You do **not** merge and you do **not** flip the PR to ready — flipping to ready triggers the +automatic IMPL-EVAL, and that trigger is the orchestrator's to fire. Merge authority is the +orchestrator's. From 887a28f1a050e34bb44805240f3c90e6ece8eed8 Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Wed, 12 Aug 2026 13:12:20 +0200 Subject: [PATCH 03/73] docs(harness): research and plan #1459 and #1548 #1548: the browser read is unreachable by static substitution for two reasons (import.meta passed by value, keys read by computed index), corroborated by this repo's own Vite plugin emitting define keys as literal import.meta.env.X. #1459 is larger than scoped: DeferIsland is not registered as an island at all, and a second defect sets f-client-nav false in exactly the partial-miss case. Its client-bundle regression criterion needs capability the repo lacks, so it moves to #1557 with a written reason rather than being ticked on weaker evidence. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TKxrWGp5uxEHQ2NyZiZSMF --- .../slices/codex-thread-ids.md | 20 +-- .../slices/plan-1459.md | 106 +++++++++++++++ .../slices/plan-1548.md | 110 ++++++++++++++++ .../slices/research-1459.md | 121 ++++++++++++++++++ .../slices/research-1548.md | 97 ++++++++++++++ 5 files changed, 444 insertions(+), 10 deletions(-) create mode 100644 .llm/runs/release-0.0.6-features--orchestration/slices/plan-1459.md create mode 100644 .llm/runs/release-0.0.6-features--orchestration/slices/plan-1548.md create mode 100644 .llm/runs/release-0.0.6-features--orchestration/slices/research-1459.md create mode 100644 .llm/runs/release-0.0.6-features--orchestration/slices/research-1548.md diff --git a/.llm/runs/release-0.0.6-features--orchestration/slices/codex-thread-ids.md b/.llm/runs/release-0.0.6-features--orchestration/slices/codex-thread-ids.md index eceedf2d35..a1172db4e2 100644 --- a/.llm/runs/release-0.0.6-features--orchestration/slices/codex-thread-ids.md +++ b/.llm/runs/release-0.0.6-features--orchestration/slices/codex-thread-ids.md @@ -1,16 +1,16 @@ -# ns006-1398 — Codex implementation thread -- **Thread / session id:** `019ff4ff-a633-7062-ae9c-21930930b5d6` -- **Rollout:** `/home/codex/.codex/sessions/2026/08/12/rollout-2026-08-12T10-03-42-019ff4ff-a633-7062-ae9c-21930930b5d6.jsonl` -- **Worktree:** `/home/codex/repos/ns006-1398` -- **Branch:** `fix/1398-publish-job-executions-to-durable-stream` @ `01aa12b67` (NO upstream by design). -- **Push rule:** explicit refspec only — `git push origin HEAD:refs/heads/fix/1398-publish-job-executions-to-durable-stream`. -- **Requested route:** provider=openai · model=gpt-5.6-sol · effort=medium -- **Observed route:** provider=openai · model=gpt-5.6-sol · effort=medium +# ns006-1457 — Codex implementation thread +- **Thread / session id:** `019ff5a5-f964-7d32-8a55-d10063b1c118` +- **Rollout:** `/home/codex/.codex/sessions/2026/08/12/rollout-2026-08-12T13-05-23-019ff5a5-f964-7d32-8a55-d10063b1c118.jsonl` +- **Worktree:** `/home/codex/repos/ns006-1457` +- **Branch:** `fix/1457-chat-proxy-query-forwarding` @ `f99cb4fbf` (NO upstream by design). +- **Push rule:** explicit refspec only — `git push origin HEAD:refs/heads/fix/1457-chat-proxy-query-forwarding`. +- **Requested route:** provider=openai · model=gpt-5.6-sol · effort=low +- **Observed route:** provider=openai · model=gpt-5.6-sol · effort=low - **Route verdict:** matched - **Runtime:** approval=never · sandbox=dangerFullAccess -- **Brief (staged):** `/home/codex/ns006-1398-brief.md` +- **Brief (staged):** `/home/codex/ns006-1457-brief.md` ## Steering (same thread — never a second send-message-v2 at this worktree) ```bash -codex exec resume 019ff4ff-a633-7062-ae9c-21930930b5d6 -- "" +codex exec resume 019ff5a5-f964-7d32-8a55-d10063b1c118 -- "" ``` _Written by `.llm/tools/agentic/codex/launch-codex-slice.ts`._ \ No newline at end of file diff --git a/.llm/runs/release-0.0.6-features--orchestration/slices/plan-1459.md b/.llm/runs/release-0.0.6-features--orchestration/slices/plan-1459.md new file mode 100644 index 0000000000..e110988870 --- /dev/null +++ b/.llm/runs/release-0.0.6-features--orchestration/slices/plan-1459.md @@ -0,0 +1,106 @@ +# Plan — #1459 `DeferComponent` is not hydrated, so partial-miss regions never refresh + +Lane: 0.0.6 runtime reopen. Control PR #1555. Research: `slices/research-1459.md`. +Branch: `fix/1459-defer-island-hydration`. **Goes to PLAN-EVAL before implementation** via the +`openhands` + `status:plan-eval` label pair. + +## What the research changed + +The issue frames this as "the component is not hydrated". The actual finding is narrower and harder: +**`DeferIsland.tsx` is not an island at all** — there is no island registration for it anywhere +(`DeferIsland.tsx:111` is a *named* export; the scaffold calls `fresh()` with no options at +`vite.config.ts.template:41`; `createNetScriptVitePlugin` has zero island logic across all 438 lines). +Islands in this repo reach the client by **directory convention** or by being **copied into the +consumer app** (`fresh-ui`), and the one JSR-shipped island that works — `QueryIsland` — works only +because the *consumer's own* island file imports it (`query-island.tsx:26-36`). + +And a **second, independent defect** sits in the same component: `DeferIsland.tsx:222` sets +`f-client-nav={!(isPartialRequest && !hasCachedData)}`, whose condition is **exactly** `partial-miss`. +So in precisely the failing case, `f-client-nav` is `false` while `f-partial` is set (`:221`). +Hydration alone may therefore still produce a full document navigation instead of the `/partials/**` +request. These two are **technically inseparable**: fixing hydration without this leaves the issue's +observable symptom unfixed. + +## Scope decision — split, and move one criterion with a written reason + +The issue's acceptance asks for a regression test that *"build[s] the Fresh client bundle, navigate[s] +into a page with a cache-miss deferred layer, assert[s] the partial endpoint is requested, and +assert[s] the named boundary swaps exactly once."* + +This repo has **no browser driver** (Playwright appears only in docs and in the CLI *scaffolding* for +consumer projects) and **no gate that builds or inspects a client bundle**. That criterion therefore +needs **net-new test capability**, which is its own piece of work. + +**Decision: it moves to a follow-up issue with a written reason (#1090 pattern), and this PR does not +tick it.** A p1 runtime blocker — a documented pattern that paints a skeleton which never resolves — +should not wait on net-new browser-test infrastructure. Per the honesty rule, the criterion travels +with a reason rather than being ticked on weaker evidence. + +What this PR **does** prove is stated below and is deliberately not dressed up as the missing test. + +## Locked decisions + +- **D1 — the island must reach the client without depending on a consumer-owned file.** Preference + order, to be settled by the implementer against verified feasibility: + 1. register the island from `createNetScriptVitePlugin` (the only option requiring no consumer + file) — **feasibility is unverified**; the research could not confirm `@fresh/plugin-vite` + supports registering an island from a JSR specifier, and proved only that *this repo passes no + such option*. **Verify this first.** + 2. if (1) is not supported, the `fresh-ui` **copy-mode** precedent (`registry.generated.ts:334-340`). + A consumer-side shim that a consumer can delete — silently reintroducing the bug — is the **least** + preferred and must be argued for if chosen. +- **D2 — fix `f-client-nav` (`DeferIsland.tsx:222`).** Its current condition disables client nav in + exactly the `partial-miss` case it needs to be enabled for. **Verify Fresh's actual + `f-client-nav`/`f-partial` form-interception semantics before changing it** — the research flagged + this as inference, not fact. If Fresh does intercept `f-partial` regardless of `f-client-nav`, say + so and leave the line alone with evidence. +- **D3 — address the partial-swap case.** The coordinator form renders as a **sibling outside** the + `` (`DeferPage.tsx:257-275`), so a region swap leaves the form's DOM in place while a + page-level swap re-renders an inert form. The acceptance explicitly requires the coordinator to work + *after a page partial swap*; a fix that only works on first paint does not satisfy it. +- **D4 — no change to `decideDeferClientAction`'s policy logic** (`policy.ts:177-208`). It is correct + and already unit-covered; it is simply never executed. If the fix requires changing it, stop and + report. +- **D5 — out of scope:** the `streams`/`shouldStream` sibling path (`runtime/mod.tsx:178`), the dead + `debug` prop (`DeferIsland.tsx:54`), and #1457/#1548's surfaces. + +## Tests — what this PR can honestly prove + +1. **Island marker in server output.** Reuse the existing JSX-tree harness + (`define-page/tests/search-params.test.tsx:93-99,115-134`) to assert the rendered tree carries + whatever marker the chosen D1 mechanism produces. Fails if registration regresses. +2. **`f-client-nav` condition** (D2) asserted directly for the four combinations of + `isPartialRequest` × `hasCachedData`, pinning that `partial-miss` enables client nav. This is the + guard that would have caught the second defect. +3. **`buildDeferFormState` / decision plumbing** extended so `partial-miss → submit` is exercised + with the real inputs `DeferPage` passes (`DeferPage.tsx:267-268`), not synthetic ones. +4. If D1 lands via the Vite plugin, a unit test that the plugin **emits the island registration**. + +**None of these prove the client bundle contains the island or that a browser issues the partial +request.** That is the moved criterion. Say so plainly in the PR body; do not let tests 1–4 be read as +satisfying it. + +## Gates + +Scoped check/lint/fmt over `packages/fresh`, `deno task quality:gate` (**verify it covers +`packages/fresh`; it demonstrably omits several packages — #1542**), `deno task --cwd packages/fresh +test`, `deno task doc:lint`. If D1 changes the CLI registry or scaffold template, add the +`packages/cli` scoped wrappers and expect `scaffold-static` to exercise it in CI. + +## Risks + +- **R1** — D1's preferred option has **unverified feasibility**. If it fails, the fallback changes the + distribution model (copy-mode), which is a materially different PR. That branch point is why this + plan goes to PLAN-EVAL. +- **R2** — the `f-client-nav` claim is inference. Acting on it without verifying Fresh's semantics + risks changing a correct line. +- **R3** — copy-mode (D1.2) means framework fixes stop reaching already-generated apps; that is a + doctrine-relevant trade, not just an implementation detail. + +## Acceptance mapping (#1459) + +| Criterion | This PR | Evidence | +| --- | --- | --- | +| Coordinator registered/hydrated as a real Fresh island, including after a partial swap | **yes** (D1 + D3) | server-output marker test; plugin-emission test | +| A cache miss triggers the configured partial request and the region settles | **partially** — mechanism fixed (D2), not observed in a browser | `f-client-nav` condition tests | +| Regression test builds the client bundle, navigates, asserts the partial request and a single swap | **no — moved to a follow-up with a written reason** | needs net-new browser/bundle capability this repo lacks | diff --git a/.llm/runs/release-0.0.6-features--orchestration/slices/plan-1548.md b/.llm/runs/release-0.0.6-features--orchestration/slices/plan-1548.md new file mode 100644 index 0000000000..b0ce3520a8 --- /dev/null +++ b/.llm/runs/release-0.0.6-features--orchestration/slices/plan-1548.md @@ -0,0 +1,110 @@ +# Plan — #1548 browser stream resolver cannot see Aspire VITE service references + +Lane: 0.0.6 runtime reopen. Control PR #1555. Research: `slices/research-1548.md`. +Branch: `fix/1548-vite-browser-stream-discovery`. **Goes to PLAN-EVAL before implementation** via the +`openhands` + `status:plan-eval` label pair (automatic policy, D-4/D-5). + +## Contract + +`createNetScriptStreamDB({ streamPath, schema })` with **no `baseUrl`** must resolve +`VITE_services__streams__http__0` (or `VITE_STREAMS_URL`) in a Fresh/Vite browser bundle and connect, +instead of throwing "Durable streams URL not found…". The consumer-side +`baseUrl: import.meta.env.VITE_… ?? …` workaround must become unnecessary. + +## Root cause (verified from repo code, not from claims about Vite) + +Two independent reasons the value cannot be statically substituted — +`packages/plugin-streams-core/src/application/stream-url-resolver.ts`: + +1. `import.meta` is passed **as a value** into `readImportMetaEnvironment` (`:60`), which reaches + `meta.env` through a parameter binding (`:77-78`). The literal expression + `import.meta.env.VITE_services__streams__http__0` never appears in source. +2. Even given a bag, both keys are read by **computed index** (`:64,:68`). + +**The decisive corroboration is internal, which is why this does not rest on an assertion about +Vite.** This repo's own Vite plugin injects env by emitting `define` entries keyed exactly as +`` `import.meta.env.${mapping.target}` `` (`packages/fresh/src/application/vite/vite.ts:195,311-335`). +That is textual static-expression substitution by construction — it cannot reach a value-passed +`import.meta` or a computed index. The failure is fully explained by this repo's own machinery. + +Aggravating factor: the browser branch is wrapped in a silent +`try { … } catch { return undefined }` (`:59,69-71`), so the diagnostic surfaces as a generic +"not found" rather than anything pointing at env reading. + +## Locked decisions + +- **D1 — read the two browser keys as literal static member expressions**, inline in the module: + `import.meta.env.VITE_services__streams__http__0` and `import.meta.env.VITE_STREAMS_URL`. No value + passing of `import.meta`, no computed index for these two keys. This is safe because the key + strings are **fully known at compile time** — `STREAMS_RESOURCE_NAME` is a constant + (`packages/plugin-streams-core/src/domain/constants.ts:5`). The generic + `serviceName`/`protocol`/`index` parameterisation is what made the keys computed; for the browser + path it buys nothing and costs substitutability. +- **D2 — split the pure lookup from the `import.meta` access**, mirroring the SDK precedent + `packages/sdk/src/discovery/browser-env.ts:35-54,65`, whose pure + `getBrowserServiceUrlFromEnv(env, …)` is unit-tested at + `packages/sdk/tests/discovery/env-ordering_test.ts:24,41,56`. The pure function takes an injected + env bag; only a thin inline shim touches `import.meta.env.*`. +- **D3 — no published-surface growth.** The pure function stays **internal**: not added to + `packages/plugin-streams-core/mod.ts`, which exports only `buildStreamUrl`, `getStreamsAuth`, + `getStreamsUrl` (`mod.ts:19`). Tests import it by src path, as this repo's tests already do. +- **D4 — resolution precedence is unchanged**: `DURABLE_STREAMS_URL` → `services__streams__http__0` + → browser `VITE_*` (full key before shorthand) → throw. This slice changes **how** the browser + branch reads, not **what** wins. +- **D5 — no Vite `transform` hook, and no scaffold-template change in this slice.** The transform + option would couple the framework to a bundler's module graph; the `envMappings` route + (`vite.ts:76-83,183-199`) only helps once the read is substitutable, so it is a possible follow-up, + not part of the fix. Adding it here would widen a p1 into CLI-template territory. +- **D6 — the silent `catch` (`:59,69-71`) may be narrowed so a genuine throw is distinguishable from + "absent", but must not change which value wins.** Explicitly bounded so it does not become a + diagnostics refactor. + +## The testability trap this plan has to solve + +A unit test cannot easily prove "Vite substituted the expression" without running a real Vite build. +So the tests must attack the defect from both sides: + +1. **Precedence tests on the pure lookup (D2)** with an injected bag — full key wins over shorthand; + shorthand used when the full key is absent; neither present → `undefined`. Mirrors + `env-ordering_test.ts`. +2. **A source-shape guard** asserting the module contains the **literal** substitutable expressions + and that the browser path does **not** pass `import.meta` across a function boundary or index + `env` by a computed key. This is deliberately a shape assertion: the defect *is* the shape, and + without this guard a future refactor back to a helper reintroduces the bug **silently and with all + behavioural tests still green** — exactly the regression class #1405's reason tests were built to + catch. +3. **A resolution test through `getStreamsUrl()`** proving the browser branch is reachable and + ordered correctly. + +The plan does **not** claim a unit test proves Vite substitution. If PLAN-EVAL judges the source-shape +guard insufficient, the fallback is a built-fixture test, which is materially more expensive and +should be argued for explicitly rather than assumed. + +## Slices + +- **S1** — D1 + D2 + D3 + D4 with tests 1–3. +- **S2** — D6 only if it stays trivial; otherwise dropped and recorded. + +## Gates + +Scoped check/lint/fmt over `packages/plugin-streams-core`, `deno task quality:gate` (**verify it +covers this package — it demonstrably omits several, tracked as #1542; if not covered, run an +explicit target scan and say so**), `deno task --cwd packages/plugin-streams-core test`, +`deno task doc:lint`. No `e2e:cli` — this slice does not touch scaffold output. + +## Risks + +- **R1** — the fix is only provable end-to-end in a real Vite build; the source-shape guard is a + proxy. Stated openly above rather than papered over. +- **R2** — `getStreamsAuth()` is Deno-only (`:138-141`) and returns `{}` in a browser. Out of scope + here, but if the browser path needs auth this fix alone will not deliver a working connection. + **Worth PLAN-EVAL's attention:** it may mean the issue's "connect directly to Streams" acceptance is + not fully satisfiable by URL resolution alone. +- **R3** — the AppHost actually injecting both VITE variables was **not verified** by research. + +## Acceptance mapping (#1548) + +| Expectation | Satisfied by | Evidence | +| --- | --- | --- | +| Zero-config browser factory resolves Aspire's canonical Vite reference | D1 + D2 + D4 | precedence tests + source-shape guard + resolution test | +| Consumer `baseUrl` workaround becomes unnecessary | D1 | the documented override remains supported, no longer required | diff --git a/.llm/runs/release-0.0.6-features--orchestration/slices/research-1459.md b/.llm/runs/release-0.0.6-features--orchestration/slices/research-1459.md new file mode 100644 index 0000000000..8379c20733 --- /dev/null +++ b/.llm/runs/release-0.0.6-features--orchestration/slices/research-1459.md @@ -0,0 +1,121 @@ +# Research — #1459 `DeferComponent` is not hydrated + +Delegated read-only sub-agent (Claude · Opus 5, `drift.md` D-1), bounded brief — 27 tool calls, +~3 min, within budget. Static analysis only; nothing built, run, or reproduced. + +## The reported defect is confirmed, and its cause is simpler than "hydration is broken" + +**`DeferIsland.tsx` is not an island at all.** There is no island registration for it anywhere in the +repo. + +- It is a **named** export, not a default export + (`packages/fresh/src/application/defer/DeferIsland.tsx:111`), re-exported as an ordinary module + symbol (`defer/mod.ts:12`, reachable as `@netscript/fresh/defer`). +- **The mechanism in this repo is directory convention**, resolved by the stock Fresh Vite plugin. + The scaffold calls `fresh()` with **no options** + (`packages/cli/src/kernel/assets/app/vite.config.ts.template:41`), and `createNetScriptVitePlugin` + contains **zero** island logic — no `islands` match across all 438 lines of + `packages/fresh/src/application/vite/vite.ts`. `define-fresh-app.ts:121-135` registers fs-routes + only and never passes a `loadIsland`. +- Every island that actually works in a scaffolded app lives in a **scanned directory**: the app + template's `(_islands)/` folders, and `@netscript/fresh-ui` islands, which are **copied into the + consumer app** (`packages/fresh-ui/registry/islands/`, registered as copy templates at + `registry.generated.ts:334-340`) rather than imported from the package. +- The contrast that proves the pattern: `QueryIsland` + (`packages/fresh/src/application/query/query-island.tsx:39`) ships from the same JSR package and + **does** reach the client — because its documented contract is that the **consumer's own** island + file imports it (`:26-36`), giving it a scanned entry point. `DeferComponent` has no such entry. + +So the component is reached **only** through a server-render path +(`DeferPage.tsx:8` ← `builders/define-page/runtime/mod.tsx:4`), and nothing in a scanned directory +transitively imports it. Both candidate causes hold and compound. + +## A second, independent defect inside the same issue + +`DeferIsland.tsx:222` sets: + +```tsx +f-client-nav={!(isPartialRequest && !hasCachedData)} +``` + +`isPartialRequest && !hasCachedData` **is exactly the `partial-miss` condition**. So in precisely the +case this issue is about, `f-client-nav` is `false` while `f-partial` is set (`:221`). If Fresh only +intercepts `f-partial` on forms under active client navigation, `requestSubmit()` would trigger a +**full document navigation**, not the `/partials/**` request the issue requires. + +**This is not verified** — Fresh core's form/partial interception was not read, so it is inference +from the attribute values. But it means **hydration alone may not fix the issue**, and any fix must +test this line rather than assume it. + +## Why no existing test catches this + +`decideDeferClientAction` (`policy.ts:177`, `partial-miss` branch at `:178-182`) has **exactly one +caller** — the client effect at `DeferIsland.tsx:188`. There is no server-side caller. In the shipped +product it is therefore **never executed at runtime on either side**; its unit coverage is the only +thing exercising it. + +The input plumbing is correct end-to-end (`runtime/mod.tsx:220` → `DeferPage.tsx:170,267-268`), which +is why this looks healthy from the server side. `DeferIsland.test.ts:1` imports only +`buildDeferFormState` and `sanitizeDeferSearchParams` and never renders the component. + +## Partial-swap behaviour + +From `DeferPage.tsx:257-275`: the coordinator form is rendered **as a sibling outside** the +`` — `
`. + +- A swap of the region's own partial replaces the content but **leaves the form's DOM node in + place**, so even a working island would not naturally re-run its effect (deps at `:201-214` are all + props). +- A **page-level** partial swap re-renders the whole `
` including a freshly server-rendered, + inert form — with no island marker, Fresh has nothing to hydrate into it. + +Nothing in the defer code addresses island persistence or re-hydration across `` swaps: no +`key`, no persistence attribute, no remount handling. The docs describe the transport and make no +persistence claim (`docs/site/web-layer/defer-streaming-ui.md:238-245`). + +## The regression test the issue asks for requires net-new capability + +The issue's acceptance says a test should *"build the Fresh client bundle, navigate into a page with +a cache-miss deferred layer, assert the partial endpoint is requested, and assert the named boundary +swaps exactly once."* + +What exists: + +- Pure-function unit tests (`DeferIsland.test.ts`) — never render the component. +- JSX-tree prop assertions (`define-page/tests/search-params.test.tsx:93-99,115-134`) — could assert + an island marker in **server** output, but cannot prove a client bundle. +- A live Fresh dev server over HTTP (`probe-project-boundary-dev.ts:43` with `FetchHttpAdapter`) — + the closest existing "drive a real app". + +What does **not** exist: + +- **No browser driver.** Playwright appears only in docs and in the CLI *scaffolding* Playwright + config for consumer projects (`init-agent.ts`) — not test infra this repo runs. +- **No gate builds or inspects a client bundle** anywhere in `packages/cli/e2e`. + +So the acceptance's regression test needs net-new capability at whichever level it targets. + +## Options (research made no recommendation) + +| Option | Trade-off | +| --- | --- | +| Consumer-side island shim from the scaffold (`routes/(_islands)/DeferIsland.tsx` re-exporting `DeferComponent`) | matches the `QueryIsland`/`fresh-ui` precedent, but every existing scaffolded app needs regeneration, and a consumer who deletes the file silently reintroduces the bug | +| Copy-mode distribution like `fresh-ui` (`registry.generated.ts`) | consistent with `ui:add`, but forks the island per project so framework fixes stop reaching existing apps | +| Declare the island from `createNetScriptVitePlugin` | the only option needing no consumer file — but **feasibility unconfirmed**, depends on a `@fresh/plugin-vite` capability not verified | +| Drop the island; resolve server-side on `ctx.isPartial && !component` | removes the hydration dependency and the `f-client-nav` hazard, but collapses the defer model — the policy branches (`policy.ts:184-208`) have no server equivalent | +| Inline `