From 0c8f55ec31b4022c5bcdcd565f7530b73d29ef6b Mon Sep 17 00:00:00 2001 From: Yuto Nishida Date: Fri, 23 Oct 2026 11:40:58 -0700 Subject: [PATCH] papyrus: run saved sessions through the current Silverwood CLI --- experimental/papyrus/server/routes/api.ts | 30 +++++++------------ .../papyrus/server/services/sessionManager.ts | 17 ++++------- .../papyrus/server/services/silverwood.ts | 9 ++++-- experimental/papyrus/server/types/index.ts | 2 +- 4 files changed, 25 insertions(+), 33 deletions(-) diff --git a/experimental/papyrus/server/routes/api.ts b/experimental/papyrus/server/routes/api.ts index cc74b17..5eb7fc0 100644 --- a/experimental/papyrus/server/routes/api.ts +++ b/experimental/papyrus/server/routes/api.ts @@ -249,11 +249,9 @@ async function knownSessionKinds(): Promise { return sessionSchemaCache; } -// Record a fresh durable session in silverwood BEFORE its PTY is spawned — `spawn from-id` -// reads the record to know how to run it, so the create is mandatory: this THROWS on failure -// and the caller must not spawn without a record. papyrus mints the id (for a claude session it -// is also the `claude --session-id`). A claude kind additionally acquires the advisory lock -// (best-effort — advisory, so it never blocks the spawn). Returns the lock state + the name used. +// Record before launching: `session run` resolves the saved kind and settings. +// Papyrus mints the id; resumable kinds acquire a best-effort advisory lock. +// Throws on creation failure; returns the lock state and display name. async function recordFreshSession( kind: string, wsId: string, @@ -339,10 +337,8 @@ apiRoutes.post("/sessions", async (c) => { }); }); -// Connect (reopen) an existing durable session: acquire its advisory lock (claude kinds -// only), then spawn its PTY via `spawn from-id` — silverwood runs it the way it records -// itself (a shell reopens fresh; a claude session resumes if a transcript exists, else -// starts fresh). `force` steals a claude lock held elsewhere. +// Reopen a durable session via `session run`, acquiring an advisory lock for resumable +// kinds. Silverwood decides first-run vs resume; `force` steals a lock held elsewhere. apiRoutes.post("/sessions/:wsId/sessions/connect", async (c) => { const wsId = c.req.param("wsId"); const { sessionId, force } = await c.req.json(); @@ -370,10 +366,8 @@ apiRoutes.post("/sessions/:wsId/sessions/connect", async (c) => { return c.json({ error: e.message }, 400); } - // silverwood is authoritative for the session's kind (the tab may be stale). An ephemeral - // kind (shell, disk-space) carries no lock; a claude kind acquires the advisory lock. Either - // way the PTY is a uniform `spawn from-id`, and silverwood decides how to run it (a shell - // reopens fresh; a claude session resumes if a transcript exists, else starts fresh). + // Read the authoritative kind (the tab may be stale) to decide whether to lock. + // Every saved kind launches via `session run`; silverwood decides how it runs. const durable = await sw .sessionLs(wsId) .catch(() => ({}) as Record); @@ -442,8 +436,8 @@ apiRoutes.post("/sessions/:wsId/sessions", async (c) => { } catch (e: any) { return c.json({ error: e.message }, 404); } - // `cwd` is only for the codex branch below (codex has no durable id yet, so it can't be - // pre-flighted via `spawn from-id`); non-codex kinds launch in the plan's authoritative cwd. + // Fresh Codex has no durable id to preflight via `session run` yet. + // Other kinds launch in the plan's authoritative cwd. const cwd = sw.checkoutLocation(ws) || ""; const sessionId = randomUUID(); if (kind === "codex") { @@ -469,10 +463,8 @@ apiRoutes.post("/sessions/:wsId/sessions", async (c) => { return c.json({ error: e.message }, 500); } - // silverwood decides spawnability per kind and returns the authoritative launch dir. The - // session must exist for `spawn from-id` to resolve, so this runs AFTER the record; on a - // non-spawnable workstream (e.g. a Basic checkout still provisioning) roll the record back - // so a failed attempt leaves no orphan tab, and surface silverwood's own error. + // Preflight `session run` after recording; silverwood decides spawnability and cwd. + // Roll back on failure so a non-spawnable workstream gets no orphan tab. let plan: sw.ShellPlan; try { plan = await sw.spawnPlan(wsId, sessionId); diff --git a/experimental/papyrus/server/services/sessionManager.ts b/experimental/papyrus/server/services/sessionManager.ts index 8a1106d..3d7d9ae 100644 --- a/experimental/papyrus/server/services/sessionManager.ts +++ b/experimental/papyrus/server/services/sessionManager.ts @@ -44,12 +44,12 @@ function releaseLock(sessionKey: string, session: Session): void { } } -// The minimal, explicit environment handed to `silverwood spawn`. NOT papyrus's +// The minimal, explicit environment handed to silverwood's session launcher. NOT papyrus's // inherited devshell env (`{ ...process.env }`, which is polluted with // IN_NIX_SHELL/DEVENV_*/an augmented PATH): silverwood scrubs and rebuilds the // real login env from this seed before it execs the agent, so none of papyrus's // env reaches claude. HOME/USER/SHELL seed that reconstruction; SSH_AUTH_SOCK is -// forwarded into it; SILVERWOOD_FOREST_PATH lets `spawn from-id` resolve the same +// forwarded into it; SILVERWOOD_FOREST_PATH lets `session run` resolve the same // forest papyrus's other silverwood calls do; CLAUDE_CONFIG_DIR lets it find Claude's // transcripts to decide first-run vs resume (all consumed by silverwood, then scrubbed). function seedEnv(): Record { @@ -70,14 +70,9 @@ function seedEnv(): Record { return env; } -/// Spawn a terminal for an existing session: a PTY running `silverwood spawn from-id -/// --workstream ` in the checkout dir, registered under `sessionKey`. The durable -/// session must already exist in silverwood (papyrus records it before spawning) — silverwood -/// reads its kind and runs it (deciding first-run vs resume for a claude kind from whether a -/// transcript exists), scrubs the environment, and `exec`s it, so this PTY tracks the shell's -/// lifetime directly: when it exits the PTY exits and we tear down (releasing the lock, dropping -/// the entry). `sessionKey` is the silverwood session id; `kind` is recorded on the -/// registry entry for reference but does not shape this resume command. +/// Run a recorded session in a PTY via `silverwood session run `. +/// Silverwood resolves its kind, first-run/resume mode, and environment, then execs it. +/// PTY exit tears down the runtime entry and releases its advisory lock. export function spawnTerminal(params: { sessionKey: string; workstreamId: string; @@ -86,7 +81,7 @@ export function spawnTerminal(params: { }): Session { const { sessionKey, workstreamId, cwd, kind = "claude-code" } = params; - const args = ["spawn", "from-id", sessionKey, "--workstream", workstreamId]; + const args = sw.sessionRunArgs(workstreamId, sessionKey); return spawnSilverwoodPty({ sessionKey, workstreamId, cwd, kind, args }); } diff --git a/experimental/papyrus/server/services/silverwood.ts b/experimental/papyrus/server/services/silverwood.ts index f8220fe..339d9ba 100644 --- a/experimental/papyrus/server/services/silverwood.ts +++ b/experimental/papyrus/server/services/silverwood.ts @@ -52,7 +52,7 @@ export interface AgentSession { lock?: { holder: string; acquired_at: string }; } -// How silverwood resolves a session into a runnable command (`silverwood spawn ... --json`). +// How silverwood resolves a session into a runnable command (`silverwood --json session run ...`). // silverwood owns *how* each kind runs and *where*; papyrus consumes `cwd` (the resolved // launch dir) and otherwise treats this as opaque. export interface ShellPlan { @@ -145,6 +145,11 @@ export async function sessionLs(id: string): Promise { - return run(["spawn", "from-id", sessionId, "--workstream", id]); + return run(sessionRunArgs(id, sessionId)); } // ---- writes (serialized per workstream — see below) ---- diff --git a/experimental/papyrus/server/types/index.ts b/experimental/papyrus/server/types/index.ts index 39d0a90..92dfba8 100644 --- a/experimental/papyrus/server/types/index.ts +++ b/experimental/papyrus/server/types/index.ts @@ -31,7 +31,7 @@ export interface Session { createdAt: string; // The silverwood session kind tag this PTY runs (from `session-schema`), recorded for // reference only for an existing durable session. A fresh Codex session uses its - // distinct create command because no `from-id` target exists yet. + // distinct create command because no durable `session run` target exists yet. kind?: string; } -- 2.54.0