Skip to content
This repository was archived by the owner on Oct 2, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## [v0.5.6] - 2026-09-27

### Changed

- **The web app template runs on `@pipelex/sdk` 0.26.0**: the SDK's `RunFailedError` now carries a failed run's stored error report and its `ApiResponseError` a refused request's problem document, which the template's failure display reads. A project made from an earlier template keeps its own SDK range, and moving it onto 0.26.0 is a minor step whose breaking changes the SDK's changelog lists.

### Fixed

- **A failed durable run says why**: the web app template's failure display now reads a failed run's stored error report and shows the runtime's reason with the failing pipe, its advice as the next step, whether running it again can help, and a line to quote to support with the run id, the error type and when the run ended, where it used to show only "Run finished with status FAILED; no result available". A model provider's raw error text is kept out of everything the person can read, and a run that ended with no stored report keeps the old sentence.
- **A refused run says what to do**: when the runner refuses to run a method, at the start of a durable run or on a blocking run, the web app template's failure display now shows the runtime's next step in place of a generic hint, says whether running it again can help when the runtime says so, and lists the method's validation items under the technical details, with an unknown model's reference and the model deck's suggestions. An answer that carries none of this keeps its old wording.

## [v0.5.5] - 2026-09-25

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.5.5
0.5.6
2 changes: 1 addition & 1 deletion initializers/js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pipelex/create-method-app",
"version": "0.5.5",
"version": "0.5.6",
"description": "Start a Pipelex method app: npm create @pipelex/method-app@latest my-app -- --method <bundle | mt_… | address>",
"keywords": [
"pipelex",
Expand Down
1 change: 1 addition & 0 deletions webapp-js/.claude/skills/bootstrap/scripts/bootstrap.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ The server listens on loopback only, because anyone who can reach it runs method
- [\`docs/add-method.md\`](docs/add-method.md) β€” adding a method, and removing one.
- [\`docs/codegen.md\`](docs/codegen.md) β€” the generated types and the checks that keep them current.
- [\`docs/input-form.md\`](docs/input-form.md) β€” how the input form and the result view are rendered from a method's contract.
- [\`docs/errors.md\`](docs/errors.md) β€” what a person reads when something fails, and where a failed run's reason comes from.
- [\`CLAUDE.md\`](CLAUDE.md) β€” the project guide for coding agents.

## License
Expand Down
10 changes: 7 additions & 3 deletions webapp-js/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,12 @@ src/
ResultEnv.tsx # client component β€” the kernel's ResultEnvProvider with this app's resolvers, mounted in the layout
HydrationMark.tsx # client component β€” sets html[data-hydrated] for a browser script to wait on, mounted in the layout
CostReport.tsx # per-run token usage + cost breakdown, inside RunDetails' disclosure
ErrorDisplay.tsx # server component (renders classified PipelineError)
ErrorDisplay.tsx # server component (renders classified PipelineError: reason, next step, retry line, support line)
types/
pipelineError.ts # BadPipelineOutputError (tagged)
test/fixtures/contracts/ # recorded codegen output the shared code's tests run against
test/fixtures/runReports.ts # failed runs' stored error reports the failure display's tests run against
test/fixtures/refusals.ts # a refused run's problem document, as the SDK throws it
e2e/
home.spec.ts # offline β€” the page renders its title and either the empty state or a form
liveApi.ts # requireLiveApi() β€” the key guard every live spec calls
Expand Down Expand Up @@ -121,7 +123,7 @@ The slice is the same for every source kind; only `methods/<name>/` and the way
- **`src/hooks/`** β€” `useRun<TInput,TOutput>`, the unified client state machine (`idle β†’ running β†’ done|error`) that dispatches blocking vs durable by `mode`. Holds the durable poll loop, the staleness token, the elapsed ticker, the wall-clock ceiling, the `classifyTransportError` wrapping, and the **transient-failure budget** (a momentary 5xx/network blip on one poll tick β€” flagged `transient` by `pollDurableRun` β€” or a rejected poll await is retried up to `MAX_TRANSIENT_POLL_FAILURES`, surfacing `health: "retrying"` meanwhile, rather than abandoning a run that's still completing server-side). The running state's `health` field (`RunHealth | null`) names _why_ the poll loop is in a resilient state so `<RunStatus>` can show reassuring, cause-specific copy instead of one alarming "degraded" note: `"reconnecting"` when the **server** reported `degraded` (its status endpoint served a last-known DB status because Temporal was unreachable), `"retrying"` for a **client-side** poll blip, `null` when polling cleanly. A durable `start` that returns `lifecycle_unavailable` (the configured URL doesn't serve the durable run lifecycle) surfaces as an explicit error β€” `useRun` never silently downgrades durable to blocking. Forms never branch on mode β€” they just call `run(input)`. `useFileInputs` is the file seam β€” drop, size early-exit, ask the method's grant action, send the file to storage with the SDK's `uploadWithGrant` (from the browser-safe `@pipelex/sdk/upload`), write the stored reference back at the field's dotted path, and hold the id in the set the kernel reads as `uploadingIds` meanwhile β€” so a form with a file input composes it rather than restating it.
- **`src/components/`** β€” React components. `"use client"` only when the component uses hooks, event handlers, or browser APIs (`RunDetails` does; `RunStatus` is a pure render). `RunResult` is the one kernel composition on the output side β€” `RunInputsForm`'s twin β€” and every method's form renders it: the kernel's `<StuffViewer>` under the same `presentation="app"` the form uses, inside a labelled `<section>` so the result is a region a screen reader can name and jump to. There is no per-output-shape component, and that is the point: a result view stops being a design decision the app has to take once the method's own declaration of what it produces is a committed artifact. `ResultEnv` is the kernel's `ResultEnvProvider` carrying this app's two resolvers, mounted once in the root layout β€” display through `assetPath`, sharing through `resolveShareUrl` β€” so every file arm in every result view paints a stored reference through the assets route.
- **`src/types/`** β€” the **adapter layer over `src/generated/`**, not a place where shapes are declared. Each `parseXxx(results: RunResults)` hands `wireOutput(results)` to the binder generated from that method's own contract, and translates a thrown `ZodError` into the app's tagged error model; the type itself is re-exported from the generated `types.ts`. Hand-written validation belongs here only where it adds semantics the concept does not declare. Narrowers throw on mismatch; that's deliberate (system boundary).
- **`src/test/fixtures/contracts/`** β€” recorded `contracts.ts` files, real codegen output for methods this app does not ship, so the shared code's tests (`runInputs`, `resultField`, `RunResult`, `ResultEnv`) run against the shapes a real method produces. They are test data, never imported by app code.
- **`src/test/fixtures/contracts/`** β€” recorded `contracts.ts` files, real codegen output for methods this app does not ship, so the shared code's tests (`runInputs`, `resultField`, `RunResult`, `ResultEnv`) run against the shapes a real method produces. They are test data, never imported by app code. `src/test/fixtures/runReports.ts` holds failed runs' stored error reports, one recorded and two in the runtime's shapes, for the tests of the failed-run display (`errors`, `durableRun`, `ErrorDisplay`), and `src/test/fixtures/refusals.ts` a refused start's problem document for the refusal's.

## Generated types (`src/generated/`)

Expand Down Expand Up @@ -237,7 +239,9 @@ Conventions:
- **One client**: instantiate `PipelexApiClient` once via `getPipelexClient()`. Never `new PipelexApiClient()` directly in actions or components.
- **Narrow at the boundary, but never re-declare the shape**: the SDK returns loosely-typed output, so always pass the whole `RunResults` through a `parseXxx(results)` narrower in `src/types/`. That narrower hands `wireOutput(results)` to the generated binder and translates the thrown `ZodError` into a tagged subclass of `Error` (`BadPipelineOutputError`) via `describeSchemaFailure`. Do not `as` your way through, and do not hand-write the shape it validates β€” the method already declares it and `npm run codegen` projects it.
- **Return classified errors, don't throw across the server→client boundary**: the shared helpers return `{ ok: true, ... } | { ok: false, error: PipelineError }`. Throwing works in dev but Next.js production builds strip server-action error messages to opaque digests, which destroys the developer-facing error UX. `executeBlockingRun` / `startDurableRun` / `pollDurableRun` wrap the SDK call in `try/catch`, hand the caught value to `classifyPipelineError(err, env)`, and return the structured error. Render it client-side with `<ErrorDisplay>`.
- **Classification stays server-side, in the helpers.** `classifyPipelineError` `instanceof`-matches SDK error classes, which only exist server-side (they're stripped to opaque digests crossing the boundary) β€” so it runs inside the helpers, never on a poll/blocking result the client received. The durable `failed` poll constructs a `RunFailedError` from the result lookup and classifies it there too.
- **Classification stays server-side, in the helpers.** `classifyPipelineError` `instanceof`-matches SDK error classes, which only exist server-side (they're stripped to opaque digests crossing the boundary) β€” so it runs inside the helpers, never on a poll/blocking result the client received. The durable `failed` poll constructs a `RunFailedError` carrying the run's **stored error report** β€” the result lookup's `error`, else the status read's, which the hosted platform serves even where its result lookup does not yet β€” and classifies it there too, passing the status read's `finished_at`.
- **A failed run is shown from its report, never from the lookup's sentence.** `classifyRunFailed` puts the report's `title` in the headline, its `message` as what happened, its `user_action.detail` as the hint (except a `wait_and_retry` detail, which promises an automatic retry a failed run never gets, and an `unknown` one, the runtime's fallback, which points at fields the display never shows), its `retryable` verdict as `retry` (absent when the report has none, so nothing is claimed on a guess), and a `support` line with the run id, the `error_type` and when the run ended, which `<ErrorDisplay>` shows in place of the bare run id. **The provider's raw text never reaches the person**: the report's message quotes the provider SDK's text verbatim, which can be a raw error body or a whole HTML page, and the report names it as `provider_metadata.message`, so `visibleReportMessage` cuts it out and `provider_metadata` is left out of the technical details too. A run with no report β€” cancelled, terminated, timed out, finalized by the platform, or on a platform without the report β€” keeps the SDK's sentence. [`docs/errors.md`](docs/errors.md) is the reference.
- **A refused run is shown from its problem document.** A runner refusal at `/v1/start` or `/v1/execute` is an `ApiResponseError` whose members are the problem document's, and `withRefusalAdvice` gives the `bad_request` and `server_error` arms of `classifyResponse` its `userAction.detail` as the hint (a `wait_and_retry` or `unknown` detail excepted, as on a failed run) and its `retryable` verdict as `retry`; the message is already its `detail`. `validationLines` writes each validation item into the details ahead of the raw body, with an unknown model's `model_reference` and `suggestions`, which the SDK's `ValidationErrorItem` does not declare yet. An answer carrying none of these keeps today's classification, and the 401/403 and blocking-only arms are untouched.
- **Inputs are gated, never hand-guarded.** Every action starts with `gateRunInputs(CONTRACT, data)` over the method's committed contract β€” the same gate the browser ran for the Run button β€” and returns its `{ ok: false, error }` unchanged. Do not add a per-input `if (!x) return badRequest()` beside it. What legitimately sits _after_ the gate is a check the contract cannot express β€” the file-reference scheme check is the one example, and it runs over the _gated_ inputs. A file's type and size are checked earlier still, by the grant action, before the file is stored.
- **Add new error kinds in `src/lib/errors.ts`**: extend `PipelineErrorKind`, add an `instanceof` branch in `classifyPipelineError` (import the class from `@pipelex/sdk`), and cover it in `src/lib/errors.test.ts`. Keep `classifyPipelineError` pure β€” env passed in by caller, no `process.env` reads inside. The dual-mode kinds (`execute_timeout`, `run_still_running`, `run_failed`, `run_timeout`, `lifecycle_unavailable`) follow this pattern. Two exceptions build a `PipelineError` inline (no thrown error to classify): pre-flight validation (`file_too_large`, `unsupported_file_type`, `bad_request`) in a Server Action or in `useFileInputs`'s size check, and the client-side poll ceiling (`buildClientTimeoutError`, kind `run_timeout`) in `useRun`.
- **`lifecycle_unavailable` has two sources.** A 404 from a URL that doesn't serve the run-lifecycle routes arrives as the SDK's `RunLifecycleUnavailableError` (`instanceof` branch β†’ `classifyLifecycleUnavailable`); a `/start` against a deployment whose orchestrator is blocking-only (the in-process `direct` mode) arrives as a 400 `ApiResponseError` with `error_type: "StartRequiresAsyncOrchestration"`, matched by an `errorType` branch in `classifyResponse` β†’ `classifyStartRequiresAsync`. Both restate the runtime's vocabulary ("orchestration mode", "fire-and-forget") in this app's term β€” **durable execution** β€” and frame the configured URL as the problem, steering to `PIPELEX_BASE_URL`; the messages differ because the root causes differ.
Expand Down
1 change: 1 addition & 0 deletions webapp-js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ Next.js 16 (App Router), React 19, TypeScript 5 (strict), Tailwind CSS 4 (config
- [`docs/add-method.md`](docs/add-method.md) β€” adding a method, and removing one.
- [`docs/codegen.md`](docs/codegen.md) β€” the generated types and the checks that keep them current.
- [`docs/input-form.md`](docs/input-form.md) β€” how the input form and the result view are rendered from a method's contract.
- [`docs/errors.md`](docs/errors.md) β€” what a person reads when something fails, and where a failed run's reason comes from.
- [`docs/ci.md`](docs/ci.md) β€” what the pull-request checks prove, and how `make create` is proven against the live API.
- [`docs/chrome-lineage.md`](docs/chrome-lineage.md) β€” what this template took from the gallery, and what it changed.
- [`CLAUDE.md`](CLAUDE.md) β€” the project guide for coding agents.
Expand Down
Loading
Loading