From fcd4123f1ec797901643dcbb760f179341eccc78 Mon Sep 17 00:00:00 2001 From: Yuto Nishida Date: Sun, 20 Sep 2026 02:12:57 -0700 Subject: [PATCH] [papyrus] server: scope checkout state to the Basic kind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `checkoutState` read `ws.mode?.state ?? "none"` and was consumed as a universal "is the workstream ready" signal. But the checkout mode/state is a property of the Basic kind alone — the checkout-less `local-*` kinds have no such lifecycle. Rename it to `basicCheckoutState` and return `undefined` (not a fabricated "none") for a kind with no checkout, making the Basic-only scope explicit at every call site (the canvas node coloring and, for now, the session-spawn gate). Behavior is unchanged: `undefined !== "ready"` gates exactly as `"none"` did. Update the server↔silverwood contract + e2e tests to the new name and the undefined-for-checkout-less contract. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_0179izRQK57x2JpKTn6huvgL --- experimental/papyrus/server-tests/api.e2e.test.ts | 3 ++- .../papyrus/server-tests/silverwood-contract.test.ts | 5 +++-- experimental/papyrus/server/routes/api.ts | 12 ++++++------ experimental/papyrus/server/services/silverwood.ts | 11 ++++++----- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/experimental/papyrus/server-tests/api.e2e.test.ts b/experimental/papyrus/server-tests/api.e2e.test.ts index 84b8516..9134fe0 100644 --- a/experimental/papyrus/server-tests/api.e2e.test.ts +++ b/experimental/papyrus/server-tests/api.e2e.test.ts @@ -80,7 +80,8 @@ describe("papyrus routes → silverwood (in-process, skip-mode)", () => { jsonInit("POST", { name: "blank", path: ["local-blank"], args: [] }), ); expect(status).toBe(200); - expect(body.checkoutState).toBe("none"); + // Checkout state is Basic-only, so a checkout-less kind reports none (undefined, omitted). + expect(body.checkoutState).toBeUndefined(); const ns = await nodes(); expect(ns).toHaveLength(1); diff --git a/experimental/papyrus/server-tests/silverwood-contract.test.ts b/experimental/papyrus/server-tests/silverwood-contract.test.ts index 919de92..a098f12 100644 --- a/experimental/papyrus/server-tests/silverwood-contract.test.ts +++ b/experimental/papyrus/server-tests/silverwood-contract.test.ts @@ -26,7 +26,7 @@ describe("silverwood wrapper contract (skip-mode, no network)", () => { const ws = await newSkipWs("alpha"); expect(ws.name).toBe("alpha"); expect(ws.status).toBe("active"); - expect(sw.checkoutState(ws)).toBe("initialized-without-checkout"); + expect(sw.basicCheckoutState(ws)).toBe("initialized-without-checkout"); const got = await sw.get(ws.id); expect(got.id).toBe(ws.id); @@ -137,7 +137,8 @@ describe("silverwood wrapper contract (skip-mode, no network)", () => { const ws = await sw.create({ name: "blank", path: ["local-blank"], args: [] }); expect(ws.kind).toBe("local-blank"); expect(ws.mode).toBeUndefined(); - expect(sw.checkoutState(ws)).toBe("none"); + // Checkout state is Basic-only; a checkout-less kind has none (not a fabricated "none"). + expect(sw.basicCheckoutState(ws)).toBeUndefined(); expect(sw.checkoutLocation(ws)).toBeTruthy(); // Round-trips through get, and an empty blank removes cleanly (ground truth via CLI). diff --git a/experimental/papyrus/server/routes/api.ts b/experimental/papyrus/server/routes/api.ts index 38e6283..85d7b9c 100644 --- a/experimental/papyrus/server/routes/api.ts +++ b/experimental/papyrus/server/routes/api.ts @@ -86,7 +86,7 @@ apiRoutes.get("/session-schema", async (c) => { async function buildNode(ws: sw.Workstream) { const kv = ws.kv?.[sw.PAPYRUS_NS] || {}; const cwd = sw.checkoutLocation(ws) || ""; - const cstate = sw.checkoutState(ws); + const cstate = sw.basicCheckoutState(ws); // Live PTYs belonging to this workstream, from the registry. const runtimes = [...sessions].filter(([, s]) => s.workstreamId === ws.id); @@ -335,7 +335,7 @@ apiRoutes.post("/sessions", async (c) => { sessionId: ws.id, nodeId: ws.id, cwd, - checkoutState: sw.checkoutState(ws), + checkoutState: sw.basicCheckoutState(ws), }); }); @@ -361,8 +361,8 @@ apiRoutes.post("/sessions/:wsId/sessions/connect", async (c) => { return c.json({ error: e.message }, 404); } const cwd = sw.checkoutLocation(ws) || ""; - const checkoutState = sw.checkoutState(ws); - if (!cwd || (checkoutState !== "ready" && checkoutState !== "none")) { + const checkoutState = sw.basicCheckoutState(ws); + if (!cwd || (checkoutState !== "ready" && checkoutState !== undefined)) { return c.json({ error: "checkout not ready" }, 400); } @@ -438,8 +438,8 @@ apiRoutes.post("/sessions/:wsId/sessions", async (c) => { return c.json({ error: e.message }, 404); } const cwd = sw.checkoutLocation(ws) || ""; - const checkoutState = sw.checkoutState(ws); - if (!cwd || (checkoutState !== "ready" && checkoutState !== "none")) { + const checkoutState = sw.basicCheckoutState(ws); + if (!cwd || (checkoutState !== "ready" && checkoutState !== undefined)) { return c.json({ error: "checkout not ready" }, 400); } const sessionId = randomUUID(); diff --git a/experimental/papyrus/server/services/silverwood.ts b/experimental/papyrus/server/services/silverwood.ts index 1691375..aafd1b7 100644 --- a/experimental/papyrus/server/services/silverwood.ts +++ b/experimental/papyrus/server/services/silverwood.ts @@ -274,11 +274,12 @@ export function checkoutLocation(ws: Workstream): string | undefined { return ws.location?.within.path || undefined; } -/// The provisioning state to surface to the canvas (carried by the checkout mode). -export function checkoutState( - ws: Workstream, -): "initialized-without-checkout" | "ready" | "pending" | "failed" | "none" { - return ws.mode?.state ?? "none"; +/// The provisioning state of a **Basic** workstream's checkout, or `undefined` for a kind +/// that has none (the `local-*` kinds). This is a Basic-only property — the checkout +/// lifecycle — not a universal workstream state, so a checkout-less kind reports `undefined` +/// rather than a fabricated "none". Surfaced to color the canvas node (pending/failed). +export function basicCheckoutState(ws: Workstream): CheckoutMode["state"] | undefined { + return ws.kind === "basic" ? ws.mode?.state : undefined; } /// Read a papyrus KV value, JSON-decoded, or `undefined` if absent/garbage. -- 2.51.2