diff --git a/package.json b/package.json index 132e55dbc0e..162450b4262 100644 --- a/package.json +++ b/package.json @@ -41,8 +41,8 @@ "check:tokens": "node scripts/check-forbidden-tokens.mjs", "check:token-gates": "node scripts/check-token-gates.mjs", "check:no-git-push": "node scripts/check-no-git-push.mjs", - "check:runner-phase4-spec": "node scripts/check-runner-phase4-spec.mjs", - "check:runner-phase5-spec": "node scripts/check-runner-phase5-spec.mjs", + "check:runner-operator-contract": "node scripts/check-runner-operator-contract.mjs", + "check:runner-status-authority": "node scripts/check-runner-status-authority.mjs", "test:check-no-git-push": "node --test scripts/check-no-git-push.test.mjs", "test:install-sh-docker": "./scripts/test-install-sh-docker.sh", "test:hermes-gateway-smoke": "node --test scripts/smoke/hermes-gateway-smoke.test.mjs", diff --git a/packages/paperclip-runner/.gitignore b/packages/paperclip-runner/.gitignore index dacf8c153b3..62876f7f760 100644 --- a/packages/paperclip-runner/.gitignore +++ b/packages/paperclip-runner/.gitignore @@ -1,9 +1,9 @@ /dist-browser/ -/dist-phase5/ +/dist-sdk/ /playwright-report/ /test-results/ -/dist-phase6/ -/dist-phase7/ +/dist-standalone/ +/dist-scenarios/ /dist-issue-thread/ # The repository root ignores scratch `check-*.mjs` files and only re-includes diff --git a/packages/paperclip-runner/README.md b/packages/paperclip-runner/README.md index 9edef4afc32..359d8486f9b 100644 --- a/packages/paperclip-runner/README.md +++ b/packages/paperclip-runner/README.md @@ -1,38 +1,19 @@ # Paperclip Native Runner -This workspace is the standalone development boundary for Paperclip's native -runner protocol, harness drivers, and normalized session backends. The -production runner direction is Rust; `runner/` is its Cargo workspace. The -TypeScript surface remains a control-plane/client reference implementation. -Phase 0 runs both implementations against one language-neutral fixture. Phase 1 -adds executable PRP schemas, a cross-language conformance corpus, deterministic -static replay, and a standalone browser reference page. Phase 2 adds the Rust -runner process, process-group supervision, a scripted fake harness over stdio -JSONL, a TypeScript mock core, and validated live browser replay. Phase 3 adds -the Rust outbound WebSocket client, private durable state, replay and restart -recovery, and safe CLI/browser diagnostics. Phase 4 adds a direct, skillless -Codex app-server driver, semantic completion tools, canonical event tracing, -and a real-model example against the mock core. These phases do not import or -start Paperclip's server, UI, CLI, or production database. The Phase 4b lower -layer adds browser-resolved provider requests, goal capability detection, -subagent lineage, control-race semantics, and a package-local demo server that -keeps Codex authentication server-side. The Phase 4b live console adds the -browser tracer over that boundary: a live transcript and composer, same-turn -steering, three distinct interrupt races, inline request cards, -capability-gated goal controls, parent/child lineage, reconnect, refresh, -replay, and a redacted protocol inspector. Phase 5 extracts that accepted -transport, reducer projection, hook, components, styles, and reference console -into versioned public subpaths, then proves the surface with a second minimal -consumer. -Phase 6 currently adds a package-local standalone adapter demo at the public -control-plane/session boundary. It proves PRP replay, reducer projection, -result finalization, a default legacy path, a feature-flagged native demo path, -and a kill switch without contacting or modifying a Paperclip instance. -Consumers import this demo contract through the declared -`@paperclipai/paperclip-runner/standalone` subpath instead of package source -files. +This package is the standalone development boundary for Paperclip's native +runner protocol, process supervision, durable transport, provider drivers, and +normalized session backends. Rust owns the production runner under `runner/`; +TypeScript provides the control-plane reference, browser SDK, scenario tools, +and conformance oracle. -## Phase 0–5 quick start +The package includes one coherent set of capabilities: PRP v1 validation and +replay, a supervised local runner with a scripted fake harness, durable +WebSocket delivery and recovery, a skillless Codex app-server driver, live +session and issue-thread surfaces, a public browser/React SDK, a standalone +adapter demo, and a deterministic mock control plane. None of these surfaces +imports or starts Paperclip's server, UI, CLI, or production database. + +## Quick start From the repository root: @@ -55,41 +36,41 @@ pnpm --filter @paperclipai/paperclip-runner verify:rootless The tracer's final line is stable: ```json -{"schemaVersion":"paperclip.runner.phase0.output.v1","runIdentity":{"runId":"run_phase0_0001","sessionId":"session_phase0_0001"},"result":{"status":"succeeded","summary":"Standalone Phase 0 fixture accepted."}} +{"schemaVersion":"paperclip.runner.conformance.output.v1","runIdentity":{"runId":"run_conformance_0001","sessionId":"session_conformance_0001"},"result":{"status":"succeeded","summary":"Standalone Conformance fixture accepted."}} ``` Run only the tracer with: ```sh -pnpm --filter @paperclipai/paperclip-runner trace:phase0 +pnpm --filter @paperclipai/paperclip-runner trace:conformance ``` -Replay the Phase 1 happy path, run a Phase 2 local session, or open the browser +Replay the Replay happy path, run a Local session, or open the browser devtool: ```sh -pnpm --filter @paperclipai/paperclip-runner replay:phase1 -pnpm --filter @paperclipai/paperclip-runner trace:phase2 -- --scenario happy-path -pnpm --filter @paperclipai/paperclip-runner trace:phase3 -- --fault lost-ack -pnpm --filter @paperclipai/paperclip-runner trace:phase4 -pnpm --filter @paperclipai/paperclip-runner demo:phase4b -- --host 127.0.0.1 --port 4174 +pnpm --filter @paperclipai/paperclip-runner replay:fixture +pnpm --filter @paperclipai/paperclip-runner trace:local-runner -- --scenario happy-path +pnpm --filter @paperclipai/paperclip-runner trace:durable-recovery -- --fault lost-ack +pnpm --filter @paperclipai/paperclip-runner trace:codex +pnpm --filter @paperclipai/paperclip-runner demo:live-console -- --host 127.0.0.1 --port 4174 -# Phase 4b: chat with a live session in the browser. -pnpm --filter @paperclipai/paperclip-runner console:phase4b +# Live console: chat with a live session in the browser. +pnpm --filter @paperclipai/paperclip-runner console:live-console pnpm --filter @paperclipai/paperclip-runner browser:dev --host 127.0.0.1 --port 4179 -# Phase 5: open the public-SDK reference console and mini consumer. -pnpm --filter @paperclipai/paperclip-runner console:phase5 +# SDK: open the public-SDK reference console and mini consumer. +pnpm --filter @paperclipai/paperclip-runner console:sdk -# Phase 6: run the standalone legacy/native/kill-switch tracer and page. -pnpm --filter @paperclipai/paperclip-runner trace:phase6 -pnpm --filter @paperclipai/paperclip-runner trace:phase6 -- --feature-flag enabled -pnpm --filter @paperclipai/paperclip-runner trace:phase6 -- --feature-flag enabled --kill-switch enabled -pnpm --filter @paperclipai/paperclip-runner demo:phase6 +# Standalone: run the standalone legacy/native/kill-switch tracer and page. +pnpm --filter @paperclipai/paperclip-runner trace:standalone +pnpm --filter @paperclipai/paperclip-runner trace:standalone -- --feature-flag enabled +pnpm --filter @paperclipai/paperclip-runner trace:standalone -- --feature-flag enabled --kill-switch enabled +pnpm --filter @paperclipai/paperclip-runner demo:standalone ``` -Phase 4b provider-backed routes are loopback-only and reject wildcard/LAN +Live console provider-backed routes are loopback-only and reject wildcard/LAN binds. Browser mutations require same-origin Fetch Metadata, matching Origin, and JSON content; see the protocol-server tutorial for direct `curl` examples. @@ -102,74 +83,73 @@ and JSON content; see the protocol-server tutorial for direct `curl` examples. | `test` | Run Rust/TypeScript fixture, supervisor, fake-driver, live/replay, and boundary tests. | | `check:forbidden-imports` | Reject TypeScript imports and Cargo path dependencies that cross into Paperclip core. | | `check:tracked-imports` | Reject tracked imports and `package.json` entry points that only resolve against untracked files, so a clean checkout of any commit builds. | -| `check:phase0-parity` | Require byte-for-byte equivalent Rust and TypeScript tracer output. | -| `check:phase1-goldens` | Require all reducer snapshots and cross-language summaries to match checked goldens. | -| `check:phase1-parity` | Run TypeScript and Rust against the same Phase 1 fixture summaries. | +| `check:numbered-milestones` | Reject numbered construction-milestone names in tracked package paths and source. | +| `check:conformance-parity` | Require byte-for-byte equivalent Rust and TypeScript tracer output. | +| `check:replay-goldens` | Require all reducer snapshots and cross-language summaries to match checked goldens. | +| `check:replay-parity` | Run TypeScript and Rust against the same Replay fixture summaries. | | `check:browser-tokens` | Reject component-local visual literals and require the standalone token layer. | | `docs:validate` | Validate local documentation links and the OKF v0.2 bundle. | -| `trace:phase0` | Run the Rust mock-core tracer, print the stable result, and exit. | -| `trace:phase0:typescript` | Run the TypeScript reference tracer directly. | -| `replay:phase1` | Validate and reduce a fixture to a final snapshot. | -| `trace:phase2` | Run one native local session through the Rust runner and fake harness. | -| `record:phase2` | Capture a validated happy-path live trace as a replay fixture. | -| `trace:phase3` | Run the Rust runner against the mock core with a selected recovery fault. | -| `record:phase3` | Regenerate the complete Phase 3 fault matrix and exact per-fault traces. | -| `trace:phase4` | Run the mock core with a real, local skillless Codex app-server session. | -| `record:phase4` | Run the safe Codex task and record its validated, normalized trace. | -| `demo:phase4b` | Start the package-local HTTP/SSE server with server-only Codex authentication. | -| `console:phase4b` | Start the standalone browser devtool with the Phase 4b live console on `127.0.0.1:4180`. | -| `record:phase4b` | Run a safe real Codex task through the demo server and record reconnect/replay evidence. | -| `console:phase5` | Start the public-SDK reference console and mini consumer on `127.0.0.1:4181`. | -| `test:phase5` | Run targeted browser-client, reducer-projection, and React component contract tests. | -| `test:browser:phase5` | Exercise both consumers with the fake driver, keyboard/a11y checks, reconnect/replay, measurements, and screenshots. | -| `record:phase5:codex` | Run both public consumers against a safe real Codex session and capture live screenshots. | -| `check:phase-07-contract` | Verify the generated capability, legacy MCP, and eval traceability contract. | +| `trace:conformance` | Run the Rust mock-core tracer, print the stable result, and exit. | +| `trace:conformance:typescript` | Run the TypeScript reference tracer directly. | +| `replay:fixture` | Validate and reduce a fixture to a final snapshot. | +| `trace:local-runner` | Run one native local session through the Rust runner and fake harness. | +| `record:local-runner` | Capture a validated happy-path live trace as a replay fixture. | +| `trace:durable-recovery` | Run the Rust runner against the mock core with a selected recovery fault. | +| `record:durable-recovery` | Regenerate the complete Durable recovery fault matrix and exact per-fault traces. | +| `trace:codex` | Run the mock core with a real, local skillless Codex app-server session. | +| `record:codex` | Run the safe Codex task and record its validated, normalized trace. | +| `demo:live-console` | Start the package-local HTTP/SSE server with server-only Codex authentication. | +| `console:live-console` | Start the standalone browser devtool with the Live console on `127.0.0.1:4180`. | +| `record:live-console` | Run a safe real Codex task through the demo server and record reconnect/replay evidence. | +| `console:sdk` | Start the public-SDK reference console and mini consumer on `127.0.0.1:4181`. | +| `test:sdk` | Run targeted browser-client, reducer-projection, and React component contract tests. | +| `test:browser:sdk` | Exercise both consumers with the fake driver, keyboard/a11y checks, reconnect/replay, measurements, and screenshots. | +| `record:sdk:codex` | Run both public consumers against a safe real Codex session and capture live screenshots. | +| `check:capability-contract` | Verify the generated capability, legacy MCP, and eval traceability contract. | | `check:semantic-contracts` | Verify the provider-neutral semantic tool contract is current. | -| `trace:phase7` | Run the real runnerd/Codex semantic loop against the mock control plane. | -| `demo:phase7` | Start the Phase 7 scenario explorer over the mock control plane on `127.0.0.1:4183`. | +| `trace:live-runner` | Run the real runnerd/Codex semantic loop against the mock control plane. | +| `demo:scenarios` | Start the Capability scenario explorer over the mock control plane on `127.0.0.1:4183`. | | `console:issue-thread` | Start the Paperclip-style issue thread on `127.0.0.1:4184`. | -| `test:phase7` | Run the scenario index, run-artifact, parity, explorer component, and route tests. | -| `test:browser:phase7` | Exercise both the scenario explorer and issue-thread browser contracts. | -| `record:phase7` | Record the twelve-route screenshot acceptance set at both viewports. | +| `test:scenarios` | Run the scenario index, run-artifact, parity, explorer component, and route tests. | +| `test:browser:scenarios` | Exercise both the scenario explorer and issue-thread browser contracts. | +| `record:capability` | Record the twelve-route screenshot acceptance set at both viewports. | | `browser:dev` | Start the standalone live/replay/recovery browser devtool. | | `test:browser` | Exercise static replay and live scenarios, then capture temporary screenshots under ignored test output. | -| `verify` | Run the complete deterministic Phase 0 through Phase 5 acceptance sequence. | +| `verify` | Run the complete deterministic Conformance through SDK acceptance sequence. | | `verify:rootless` | Extract Debian/Ubuntu browser libraries without root, then run `verify`. | ## Navigate - [Architecture and dependency boundary](docs/architecture.md) - [Tutorial index](docs/index.md) -- [Phase 0 hand-run tutorial](docs/tutorials/phase-00-standalone-tracer.md) -- [Phase 1 hand-run tutorial](docs/tutorials/phase-01-static-replay.md) -- [Phase 2 hand-run tutorial](docs/tutorials/phase-02-local-runner.md) -- [Phase 2 local protocol reference](docs/phase-02-local-protocol.md) -- [Phase 3 break-it-on-purpose tutorial](docs/tutorials/phase-03-break-recovery.md) -- [Phase 3 durable transport reference](docs/phase-03-durable-transport.md) -- [Phase 4 skillless Codex tutorial](docs/tutorials/phase-04-skillless-codex.md) -- [Phase 4 skillless Codex driver reference](docs/phase-04-skillless-codex-driver.md) -- [Phase 4b protocol/server tutorial](docs/tutorials/phase-04b-protocol-server.md) -- [Phase 4b protocol/server reference](docs/phase-04b-protocol-server.md) -- [Phase 4b live console tutorial](docs/tutorials/phase-04b-live-console.md) -- [Phase 4b live console reference](docs/phase-04b-live-console.md) -- [Phase 5 SDK console tutorial](docs/tutorials/phase-05-sdk-console.md) -- [Phase 5 browser SDK reference](docs/phase-05-sdk.md) -- [Phase 5 component decision record](docs/design/phase-5-component-decisions.md) -- [Phase 7 semantic catalog and authorization](docs/phase-07-semantic-catalog.md) -- [Phase 7 live runnerd/Codex loop](docs/phase-07-live-runnerd-codex.md) -- [Phase 7 issue-thread UI](docs/phase-07-issue-thread-ui.md) +- [Conformance hand-run tutorial](docs/tutorials/conformance-standalone-tracer.md) +- [Replay hand-run tutorial](docs/tutorials/replay.md) +- [Local runner hand-run tutorial](docs/tutorials/local-runner.md) +- [Local protocol reference](docs/local-runner.md) +- [Durable recovery break-it-on-purpose tutorial](docs/tutorials/durable-recovery.md) +- [Durable transport reference](docs/durable-recovery.md) +- [Codex skillless Codex tutorial](docs/tutorials/codex.md) +- [Codex skillless Codex driver reference](docs/codex-driver.md) +- [Live console protocol/server tutorial](docs/tutorials/live-console-protocol-server.md) +- [Live console protocol/server reference](docs/live-console-protocol-server.md) +- [Live console tutorial](docs/tutorials/live-console.md) +- [Live console reference](docs/live-console.md) +- [SDK console tutorial](docs/tutorials/sdk-console.md) +- [SDK browser SDK reference](docs/sdk.md) +- [SDK component decision record](docs/design/sdk-component-decisions.md) +- [Capability semantic catalog and authorization](docs/capability-semantic-catalog.md) +- [Capability live runnerd/Codex loop](docs/capability-live-runnerd-codex.md) +- [Capability issue-thread UI](docs/capability-issue-thread-ui.md) - [PRP compatibility/versioning policy](docs/protocol-compatibility.md) - [Cumulative end-to-end tutorial](docs/tutorials/end-to-end.md) -- [Journal guide](docs/journal.md) - [OKF knowledge bundle](knowledge/) -- [Implementation plan](spec/paperclip-native-runner-implementation-plan.md) -- [Normative spike specification](spec/paperclip-native-runner-spike-spec.md) +- [Normative native-runner contract](spec/native-runner-contract.md) -Phase 4 adds the package-local real-model reference driver, Phase 4b adds the -package-local browser console, and Phase 5 extracts a reusable public SDK plus +Codex adds the package-local real-model reference driver, Live console adds the +package-local browser console, and SDK extracts a reusable public SDK plus two standalone consumers. Production Paperclip integration remains deferred. -The Phase 5 reference console opens in direct chat mode. Enter a normal prompt, +The SDK reference console opens in direct chat mode. Enter a normal prompt, then open the protocol inspector to review events and reducer state. Expand a Terminal row and its nested **Debug details** disclosure to inspect every canonical event retained for that command. The header marker `🖇️ v0.1.2` diff --git a/packages/paperclip-runner/devtools/browser/live-console.spec.ts b/packages/paperclip-runner/devtools/browser/live-console.spec.ts index aa9b36e5aa0..23ff80c386a 100644 --- a/packages/paperclip-runner/devtools/browser/live-console.spec.ts +++ b/packages/paperclip-runner/devtools/browser/live-console.spec.ts @@ -1,17 +1,10 @@ -import { resolve } from "node:path"; -import { fileURLToPath } from "node:url"; - import { expect, test, type Page } from "@playwright/test"; -const evidenceRoot = resolve( - fileURLToPath(new URL("../../knowledge/evidence/phase-04b", import.meta.url)), -); - const DESKTOP = { width: 1440, height: 900 }; const MOBILE = { width: 390, height: 844 }; function shot(page: Page, name: string) { - return page.screenshot({ path: resolve(evidenceRoot, `${name}.png`), fullPage: true }); + return page.screenshot({ path: test.info().outputPath(`${name}.png`), fullPage: true }); } async function openConsole(page: Page) { @@ -27,7 +20,7 @@ async function runManifest(page: Page, id: string) { await expect(page.getByTestId("connection-status")).toHaveText("connected", { timeout: 15_000 }); } -test.describe("Phase 4b live console", () => { +test.describe("Live console", () => { test.beforeEach(async ({ page }) => { await page.setViewportSize(DESKTOP); }); @@ -46,7 +39,7 @@ test.describe("Phase 4b live console", () => { await expect(page.getByTestId("turn-completed")).toBeVisible({ timeout: 20_000 }); await expect(page.getByTestId("live-transcript")).toContainText("finished the task"); await expect(page.getByTestId("live-transcript")).toContainText( - "Summarize what the Phase 4b demo server proves.", + "Summarize what the Live console demo server proves.", ); await expect(page.getByTestId("tool-item")).toHaveAttribute("data-status", "completed"); await shot(page, "desktop-03-turn-completed"); @@ -364,7 +357,7 @@ test.describe("Phase 4b live console", () => { }); }); -test.describe("Phase 4b live console on a small screen", () => { +test.describe("Live console on a small screen", () => { test("keeps the transcript and composer usable at 390 by 844", async ({ page }) => { await page.setViewportSize(MOBILE); await openConsole(page); diff --git a/packages/paperclip-runner/devtools/browser/playwright.config.ts b/packages/paperclip-runner/devtools/browser/playwright.config.ts index 57a0bc28338..bcd43122dc0 100644 --- a/packages/paperclip-runner/devtools/browser/playwright.config.ts +++ b/packages/paperclip-runner/devtools/browser/playwright.config.ts @@ -16,11 +16,11 @@ export default defineConfig({ env: { // Keep streamed chunks slow enough that interrupt races are reachable // from a test, and fast enough that the suite stays quick. - PAPERCLIP_PHASE4B_CHUNK_DELAY_MS: "60", + PAPERCLIP_LIVE_CONSOLE_CHUNK_DELAY_MS: "60", // One server serves the whole suite, and every test leaves its session // behind when its page closes, so the interactive default of 16 is // exhausted partway through. - PAPERCLIP_PHASE4B_MAX_SESSIONS: "64", + PAPERCLIP_LIVE_CONSOLE_MAX_SESSIONS: "64", }, url: "http://127.0.0.1:4179", reuseExistingServer: false, diff --git a/packages/paperclip-runner/devtools/browser/src/App.tsx b/packages/paperclip-runner/devtools/browser/src/App.tsx index 695d93b022c..201bda968c2 100644 --- a/packages/paperclip-runner/devtools/browser/src/App.tsx +++ b/packages/paperclip-runner/devtools/browser/src/App.tsx @@ -1,24 +1,24 @@ import { useMemo, useRef, useState } from "react"; -import type { PrpEvent, PrpRequest } from "../../../src/protocol/phase1-contract"; +import type { PrpEvent, PrpRequest } from "../../../src/protocol/replay-contract"; import type { SessionSnapshot } from "../../../src/reducer/session-reducer"; import type { - Phase2RunMetadata, - Phase2RunTrace, - Phase2Scenario, -} from "../../../src/contracts/phase2"; + LocalRunnerRunMetadata, + LocalRunnerRunTrace, + LocalRunnerScenario, +} from "../../../src/contracts/local-runner"; import { - PHASE3_FAULTS, - type Phase3Fault, - type Phase3RunTrace, -} from "../../../src/contracts/phase3"; + DURABLE_RECOVERY_FAULTS, + type DurableRecoveryFault, + type DurableRecoveryRunTrace, +} from "../../../src/contracts/durable-recovery"; import { - applyPhase2LiveEvent, - createPhase2LiveSnapshot, - phase2SnapshotsMatch, - replayPhase2Events, -} from "../../../src/tracer/phase2-live"; -import { replayPhase1FixtureText } from "../../../src/tracer/phase1-replay"; + applyLocalRunnerLiveEvent, + createLocalRunnerLiveSnapshot, + localRunnerSnapshotsMatch, + replayLocalRunnerEvents, +} from "../../../src/tracer/local-runner-live"; +import { replayReplayFixtureText } from "../../../src/tracer/replay"; import { Badge } from "./components/ui/badge"; import { Button } from "./components/ui/button"; import { @@ -32,7 +32,7 @@ import { Textarea } from "./components/ui/textarea"; import { LiveConsole } from "./live/LiveConsole"; const fixtureFiles = import.meta.glob( - "../../../protocol/fixtures/phase-01/*.json", + "../../../protocol/fixtures/replay/*.json", { eager: true, query: "?raw", import: "default" }, ) as Record; @@ -46,7 +46,7 @@ const fixtureOrder = [ "unsupported-required-version", ]; -const liveScenarios: Array<{ key: Phase2Scenario; label: string }> = [ +const liveScenarios: Array<{ key: LocalRunnerScenario; label: string }> = [ { key: "happy-path", label: "Happy path" }, { key: "permission-input", label: "Permission and input" }, { key: "interrupted", label: "Interruption" }, @@ -57,7 +57,7 @@ const liveScenarios: Array<{ key: Phase2Scenario; label: string }> = [ type BrowserMode = "console" | "recovery" | "live" | "replay"; type LiveStatus = "idle" | "starting" | "running" | "terminal" | "error"; -const phase3FaultLabels: Record = { +const durableRecoveryFaultLabels: Record = { none: "Normal connection", "socket-drop": "Socket drop", "lost-ack": "Lost ACK", @@ -76,25 +76,25 @@ const modeCopy: Record< { eyebrow: string; title: string; description: string } > = { console: { - eyebrow: "Paperclip Runner Protocol · Phase 4b", + eyebrow: "Paperclip Runner Protocol · Live console", title: "Live Codex protocol console", description: "Chat with a live session, steer it, stop it, answer its requests, and inspect the canonical protocol behind every surface.", }, recovery: { - eyebrow: "Paperclip Runner Protocol · Phase 3", + eyebrow: "Paperclip Runner Protocol · Durable recovery", title: "Durable recovery diagnostics", description: "Break the outbound transport, recover the same session, and inspect durable state.", }, live: { - eyebrow: "Paperclip Runner Protocol · Phase 2", + eyebrow: "Paperclip Runner Protocol · Local runner", title: "Live runner diagnostics", description: "Run the local harness, stream validated events, and compare the live and replayed state.", }, replay: { - eyebrow: "Paperclip Runner Protocol · Phase 1", + eyebrow: "Paperclip Runner Protocol · Replay", title: "Static protocol replay", description: "Validate a protocol fixture, replay its events, and inspect the resulting session state.", @@ -107,10 +107,10 @@ interface BrowserFixture { source: string; } -type Phase2StreamRecord = +type LocalRunnerStreamRecord = | { kind: "event"; event: unknown } | { kind: "diagnostic"; message: string } - | { kind: "trace"; trace: Phase2RunTrace } + | { kind: "trace"; trace: LocalRunnerRunTrace } | { kind: "error"; message: string }; const browserFixtures = Object.entries(fixtureFiles) @@ -139,7 +139,7 @@ function humanizeProtocolLabel(value: string): string { return words.length === 0 ? words : `${words[0]?.toUpperCase()}${words.slice(1)}`; } -function recoveryOutcomeTone(outcome: Phase3RunTrace["diagnostics"]["recovery"]["outcome"]) { +function recoveryOutcomeTone(outcome: DurableRecoveryRunTrace["diagnostics"]["recovery"]["outcome"]) { if (outcome === "recovered") { return "success" as const; } @@ -150,8 +150,8 @@ function recoveryOutcomeTone(outcome: Phase3RunTrace["diagnostics"]["recovery"][ } function RecoveryDiagnostics() { - const [fault, setFault] = useState("lost-ack"); - const [trace, setTrace] = useState(null); + const [fault, setFault] = useState("lost-ack"); + const [trace, setTrace] = useState(null); const [status, setStatus] = useState<"idle" | "running" | "complete" | "error">("idle"); const [error, setError] = useState(null); @@ -159,12 +159,12 @@ function RecoveryDiagnostics() { setStatus("running"); setError(null); try { - const response = await fetch("/api/phase3/recovery", { + const response = await fetch("/api/durableRecovery/recovery", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ fault }), }); - const result = (await response.json()) as Phase3RunTrace & { error?: string }; + const result = (await response.json()) as DurableRecoveryRunTrace & { error?: string }; if (!response.ok) throw new Error(result.error ?? "The recovery trace failed."); setTrace(result); setStatus("complete"); @@ -189,16 +189,16 @@ function RecoveryDiagnostics() { id="recovery-fault" value={fault} disabled={status === "running"} - onChange={(event) => setFault(event.target.value as Phase3Fault)} + onChange={(event) => setFault(event.target.value as DurableRecoveryFault)} > - {PHASE3_FAULTS.map((candidate) => ( + {DURABLE_RECOVERY_FAULTS.map((candidate) => ( ))}
Recovery status @@ -223,22 +223,22 @@ function RecoveryDiagnostics() {

Run the trace to collect recovery diagnostics.

) : ( <> -
+
Connection
-
+
{humanizeProtocolLabel(trace.diagnostics.connection.state)}
Connections
-
+
{trace.diagnostics.connection.connectionCount}
Reconnects
-
+
{trace.diagnostics.connection.reconnectCount}
@@ -260,7 +260,7 @@ function RecoveryDiagnostics() {
Runner restarts
-
+
{trace.diagnostics.recovery.runnerRestarts}
@@ -270,7 +270,7 @@ function RecoveryDiagnostics() {
Fresh bootstraps
-
+
{trace.diagnostics.recovery.freshBootstraps}
@@ -280,7 +280,7 @@ function RecoveryDiagnostics() {
Storage
-
+
{trace.diagnostics.outbox.bytes} bytes of {trace.diagnostics.outbox.maxBytes}{" "} max @@ -299,7 +299,7 @@ function RecoveryDiagnostics() {
{humanizeProtocolLabel(trace.diagnostics.recovery.outcome)} @@ -481,15 +481,15 @@ function pendingRuntimeRequest(events: readonly PrpEvent[]): PrpRequest | null { } function LiveRunner() { - const [scenario, setScenario] = useState("happy-path"); + const [scenario, setScenario] = useState("happy-path"); const [status, setStatus] = useState("idle"); const [runId, setRunId] = useState(null); const [snapshot, setSnapshot] = useState(null); const [events, setEvents] = useState([]); const [error, setError] = useState(null); const [diagnostic, setDiagnostic] = useState(null); - const [input, setInput] = useState("phase-02-live-trace"); - const [trace, setTrace] = useState(null); + const [input, setInput] = useState("local-runner-live-trace"); + const [trace, setTrace] = useState(null); const [parity, setParity] = useState(null); const snapshotRef = useRef(null); const eventsRef = useRef([]); @@ -509,26 +509,26 @@ function LiveRunner() { snapshotRef.current = null; try { - const startResponse = await fetch("/api/phase2/runs", { + const startResponse = await fetch("/api/localRunner/runs", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ scenario }), }); const started = (await startResponse.json()) as { id?: string; - metadata?: Phase2RunMetadata; + metadata?: LocalRunnerRunMetadata; error?: string; }; if (!startResponse.ok || started.id === undefined || started.metadata === undefined) { throw new Error(started.error ?? "The local runner did not start."); } setRunId(started.id); - const initial = createPhase2LiveSnapshot(started.metadata); + const initial = createLocalRunnerLiveSnapshot(started.metadata); setSnapshot(initial); snapshotRef.current = initial; setStatus("running"); - const streamResponse = await fetch(`/api/phase2/runs/${started.id}/events`); + const streamResponse = await fetch(`/api/localRunner/runs/${started.id}/events`); if (!streamResponse.ok || streamResponse.body === null) { throw new Error("The live event stream did not open."); } @@ -542,12 +542,12 @@ function LiveRunner() { buffered = lines.pop() ?? ""; for (const line of lines) { if (line.trim().length > 0) { - handleRecord(JSON.parse(line) as Phase2StreamRecord, started.metadata); + handleRecord(JSON.parse(line) as LocalRunnerStreamRecord, started.metadata); } } if (part.done) { if (buffered.trim().length > 0) { - handleRecord(JSON.parse(buffered) as Phase2StreamRecord, started.metadata); + handleRecord(JSON.parse(buffered) as LocalRunnerStreamRecord, started.metadata); } break; } @@ -558,13 +558,13 @@ function LiveRunner() { } } - function handleRecord(record: Phase2StreamRecord, metadata: Phase2RunMetadata) { + function handleRecord(record: LocalRunnerStreamRecord, metadata: LocalRunnerRunMetadata) { if (record.kind === "event") { const current = snapshotRef.current; if (current === null) { return; } - const applied = applyPhase2LiveEvent(current, record.event); + const applied = applyLocalRunnerLiveEvent(current, record.event); if (!applied.ok) { setStatus("error"); setError(applied.issues.join("; ")); @@ -586,11 +586,11 @@ function LiveRunner() { return; } setTrace(record.trace); - const replay = replayPhase2Events(metadata, record.trace.events); + const replay = replayLocalRunnerEvents(metadata, record.trace.events); setParity( snapshotRef.current === null ? false - : phase2SnapshotsMatch(snapshotRef.current, replay), + : localRunnerSnapshotsMatch(snapshotRef.current, replay), ); setStatus("terminal"); } @@ -599,7 +599,7 @@ function LiveRunner() { if (runId === null) { return; } - const response = await fetch(`/api/phase2/runs/${runId}/${action}`, { + const response = await fetch(`/api/localRunner/runs/${runId}/${action}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body ?? {}), @@ -626,7 +626,7 @@ function LiveRunner() { id="live-scenario" value={scenario} disabled={status === "starting" || status === "running"} - onChange={(event) => setScenario(event.target.value as Phase2Scenario)} + onChange={(event) => setScenario(event.target.value as LocalRunnerScenario)} > {liveScenarios.map((candidate) => (