Spool v2 walking skeleton: share → hub → web reader → resume in one flow#449
Merged
Conversation
…ion, views, session diffs Codex-drafted, Claude-reviewed with ownership fixes: codex output success detection (exit_code envelope + Success-prose, default reject), headerless apply_patch hunks, files[].events now the full client fetch list (call + result indices), sparse IndexedRecord support for the web reader.
…bstones, CLI tokens Content-addressed object sync per the PR1 scope contract: push declares a head and returns missing oids (per-user dedup, anti dedup-oracle); batch re-hashes every object server-side and appends an R2 pack; head folds the manifest to verify the root without touching bodies, then commits the ref. Read path: no-store meta (tombstone-immediate), immutable-cached view and NDJSON record ranges via ranged pack GETs. Router learns /session/*.
… session refs, note editor Codex-drafted against the same wire contract; share/resume land separately.
…stones, ranged pack reads Codex-drafted against the wire contract; fakes gain hub tables, db.batch, and ranged R2 gets. 21 tests incl. a full synthetic round-trip.
share: canonicalize provider JSONL at share time ($SPOOL_WS/$SPOOL_HOME rewrite), redact gate before publish, git-commit-style note flow, 3-step hub handshake with batched uploads, @n prefix shares, lineage recovered from birth records. resume (claude): fetch + client-side integrity check (per-record oids + root fold), materialize a brand-new provider session under ~/.claude/projects with one marked Spool birth record, hand off to native claude --resume (spawn only with --exec).
…d path Layer 1: author note vs machine-evidence zones (fallback chain note → last reply → first prompt), workspace card, lineage, copyable resume command. Layer 2: timeline rendered straight from the view index (zero record fetches at first paint; bodies load on expand) ↔ per-file lazy diff recomputed client-side with session-kit (hunk ↔ record two-way links). Layer 3: #r/<idx> deep links. Server-side OG summary tags via functions/session/[sid].ts. DESIGN.md tokens throughout; metadata is author-attributed on share pages.
…tions The protocol keystone: spool share's pipeline → push/batch/head Pages Functions over hermetic fakes → web-shaped reads (meta/view/records, root refold, client-side diff recompute) → spool resume materialization on a second temp HOME. Plus prefix-share @2 clamping and a route-table guard for /session/* on the spool.pro dispatcher.
…n for spool share spool list printed no session id, so there was nothing to feed spool share/show — the listing was a dead end. Rows now lead with #<uuid8>, and share expands unique prefixes (ambiguous ones fail loudly naming the candidates).
editNote keeps a saved note when the editor exits nonzero (vimrc noise, :cq habits) and aborts only when nothing was modified. HubClient wraps undici's bare 'fetch failed' with the hub URL and a start-the-dev-stack hint for loopback hosts.
Claude Code names project dirs after the resolved cwd — on macOS /tmp/x records as /private/tmp/x, so a session materialized under the unresolved path lands where claude --resume never looks. Resolve symlinks before computing the project dir (keeping the unresolved path only when the workspace doesn't exist yet); round-trip tests now pass the workspace through a symlink on purpose.
One command, no extra step: materialize then hand off to the native CLI in the workspace (design §3's '照常调用 provider 原生 resume'). --no-exec restores print-only; a missing claude binary degrades to the printed command instead of a stack trace.
The sid resumes against the reader's configured hub (spool.pro default); the URL form stays CLI-accepted for cross-hub cases.
…r, and transport layers
… package MessageList/MessageBubble/MarkdownContent/CodeBlock + findHighlightPlugin move out of the app renderer, data-driven via a minimal ConversationMessage shape (core's Message is structurally assignable). i18n becomes injectable labels; the app passes its t()/locale through. Styling ships both ways: the app compiles the shared sources with its own Tailwind pass (@source), while dist/styles.css is a prebuilt sheet for non-Tailwind consumers with dark: bound to html[data-theme]. App visuals unchanged — pure move.
…p conversation core's claude/codex parsers split at the I/O seam: the pure parsing body (and the spool-prelude strip) moves to browser-safe session-kit, core keeps thin fs wrappers with an unchanged surface — all 417 core tests guard the move. share-web's /session page now fetches the full record set with progress, parses it through the SAME parser, and renders it with session-view's MessageList — what you read on the web is what the desktop shows. Deep links and diff-hunk jumps resolve record indices to conversation messages (uuid first, timestamp fallback, gaps carry to the nearest preceding message). Custom timeline + record-render are gone.
The same pipeline spool share runs, driven from the desktop: prepare computes records/diffstat/redact findings locally and shows them before anything leaves the machine; publish runs the 3-step handshake through the app's existing sign-in session (the hub accepts session bearers — no separate login). CLI exposes its hub transport via the @spool-lab/cli/hub subpath; the standalone hub-client package extraction is deferred until the in-flight cli-auth work lands. Dialog follows the app's modal conventions; 7 locales.
Add pi as a fifth session source across the stack: a v3 JSONL parser for ~/.pi/agent/sessions (user/assistant text, toolCall names, model from assistant messages with model_change fallback, uuid recovery from the file name), source discovery with SPOOL_PI_DIR override, watcher roots, syncer dispatch with the claude-style two-segment nesting guard, sources seeding, status counts, CLI search/list filters, and a `pi --session <uuid>` resume hint on search results. Sharing and resume for pi stay gated off (claude + codex only) until canonicalization is validated against pi records. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
The skill now routes between two flows: sharing the current session to
the hub ("share to spool") and the existing history search. The share
flow targets the exact session via $CLAUDE_CODE_SESSION_ID after a
spool sync, has the agent author the note via -m (no $EDITOR in agent
contexts, stdin from /dev/null so the redact gate aborts instead of
hanging), and relays secret findings to the user for explicit consent
before retrying with --yes. Also covers @n prefix shares, withdraw,
the login hint, and adds pi to the search sources.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
The .spool is the publication artifact riding beside the raw records (design: records are the stream, the document is the publication). Wire: head gains a nullable spoolFileOid (migration 0004); the document is a plain content-addressed object served like the view with immutable caching. Desktop shares auto-build a sanitized document (redact-baked bodies AND title — caught by the new test) without importing share-kit's DOM-bound bundle into the main process; the CLI attaches one explicitly via --spool-file. The web session page adds Conversation ↔ Document tabs, rendering the document through share-kit's SnapshotReader plus a .spool download. share-kit gains the pure snapshotFromSpoolDocument transform.
materializeSession(provider, …) generalizes the claude path: codex rollouts get a fresh session_meta id, the date-partitioned sessions/YYYY/MM/DD/rollout-<stamp>-<uuid>.jsonl location, a response_item birth record, and a codex resume handoff. Session refs carry their provider so the command routes without a claude-only gate. Round-trip + materializer goldens cover both providers.
Real CLI share (codex fixture) through the real hub functions, resumed into a date-partitioned rollout with rewritten session_meta id, restored paths, and the birth record — closes the claude-only gap noted in the PR body.
The command table catches up with the design (§6): show <sid|url> prints the first-screen summary (note → intent/outcome fallback, files, card, resume hint); --log prints the record timeline from the view; --diff recomputes the per-file net change from event records only — the same session-kit path the web diff pane runs; @r<n> lands on one record. Local uuids keep the legacy transcript output byte-for-byte and gain the same three views straight from the provider file.
…point claude launches --resume <id> --fork-session and codex launches `codex fork <id>`, so the materialized file becomes an immutable anchor: continued work lands in the forked session, the anchor keeps folding to the shared integrity root, and re-running the printed command branches another fresh session off the share point. Spike-verified against the real CLIs: claude --fork-session -p recalls the materialized history with the anchor sha unchanged; codex fork discovers the hand-written rollout and stamps forked_from_id — codex's own lineage pointer — into the new session_meta.
Fork-first, per each provider's official docs: claude gains --fork-session, codex switches to `codex fork`, opencode gains --fork. Gemini keeps plain --resume — it has no fork affordance at all (its only branch story is the interactive /chat save + resume checkpoint pair). pi is new in the map as `pi --fork <uuid>`, live-verified: the uuid lookup is global but the fork is created under the CURRENT project, so the cd prefix decides both where the branch lands and where the agent works.
Web sign-in now runs the AuthKit authorization-code flow through the
provider registry (workos is the only provider); first sign-in links
onto pre-WorkOS Google accounts via the identities API instead of
forking them. Native surfaces get their own paths: a PKCE code
exchange at /api/auth/sign-in-with-code (desktop) and a self-hosted
browser-approval broker at /api/cli-auth/{start,approve,poll} (CLI)
that mints sph_ tokens off an approved web session. The Google
provider, id_token verification (jwks), and the desktop nonce-replay
machinery are deleted; /cli-auth ships as the approval page in
share-web.
Default path is now the cli-auth broker: start a device/user code pair, open the approval page (or print the URL — works over SSH), poll until the signed-in browser approves, save the sph_ token. --token keeps the paste path for CI. The interactive paste prompt is gone.
Follows the official WorkOS Electron pattern (public client, system browser, custom protocol callback — production environments reject http/localhost redirect URIs): the app mints a PKCE pair, opens the AuthKit authorize URL, receives the code on spool://auth/callback via the new deep-link dispatcher, and posts code+verifier to /api/auth/sign-in-with-code for a spool session. WorkOS tokens never touch the machine. The Google loopback server, its OAuth orchestrator, and the SPOOL_GOOGLE_* env plumbing are deleted; e2e sign-in rides the same wire contract against the mock backend. Dev-stack docs updated for the WorkOS sandbox setup.
Self-revoke for spool logout: the bearer IS the token to kill — hash lookup, row delete, hub-token-revoke audit. No bearer or an unknown token is 401 like everywhere else; a web session can't name a token so it can't revoke one. HUB_DEV_TOKEN lives in the env, not D1, so deleting it is an ok no-op and local-dev logout still works.
The inverse of login: best-effort DELETE /api/hub/v1/tokens (an unreachable hub warns but never wedges local sign-out; a 401 means the hub already invalidated it), then remove ~/.spool/hub-credentials.json. Operates on the stored file only — a lingering SPOOL_HUB_TOKEN env override is called out for the user to unset, never revoked.
login/logout/share/resume/withdraw were shipped undocumented, and the site reference was also missing projects, pin, and doctor from earlier releases. show gains its hub-ref, --log/--diff, and @r<n> forms.
Restructure the monorepo around an apps/ vs packages/ split and collapse the two public web properties into one app: - apps/web (@spool/web): landing + share-web merged into a single TanStack Start app on the void platform (voidPlugin + tanstackStart compose in one vite pipeline, vp toolchain kept). Marketing/docs/blog prerender to static HTML at build time (14 pages); markdown moves from @void/md to import.meta.glob + react-markdown. /s/* and /session/* use data-only SSR: route loaders fetch the tiny meta endpoints and render OG tags via head(), replacing the Pages Functions that injected meta into the static shell. The _headers file becomes a request middleware (src/start.ts) that mirrors its semantics with a per-request CSP nonce threaded to the router for the SSR hydration scripts. Fonts unify on self-hosted @fontsource (CSP font-src 'self'); theme key unifies on 'spool-theme' with legacy-key fallback; dev port stays 3002 (WorkOS redirect URI). - apps/: app, cli, web, backend move out of packages/; libraries (core, redact, session-kit, session-view, share-kit) stay in packages/. - @spool/share-backend renames to @spool/backend (apps/backend); cloud resource names (spool-share-backend, spool-share-db, buckets) are unchanged to avoid re-provisioning. - workers/spool-pro-router collapses from three origins to two: /api/* -> backend, everything else -> the merged web app. The per-page allowlist and ORIGIN_LANDING are gone. - CI: deploy-landing.yml becomes deploy-web.yml (builds workspace lib deps before void deploy, VOID_PROJECT stays spool-landing for origin continuity); e2e/release workflows, release.sh, share-dev.sh, docs, and cross-package path references updated. Also carries the in-flight v2 share-flow WIP from this branch (core worktree-identity/codex parser work, cli local-session-ref + command polish, session-view build-output test, session-reader prototype).
Share is the primary branch: description leads with share triggers, the share flow moves ahead of recall, and the opening reframes spool as session hand-off backed by the local cross-agent index. Recall keeps its full search -> zoom -> cited-use flow; a command reference table covers the rest of the CLI, verified against a live run of all 15 commands.
@spool-lab/core's parsers now import from @spool-lab/session-kit, but the app's build:deps chain never built it, so clean CI failed with TS2307 on claude.ts/codex.ts/spool-prelude.ts.
- Replace polynomial trailing-slash/-hyphen trims (/\/+$/, /-+$/) with linear loops in hub client/credentials and pi slug decoding. - Strip angle-bracket tags to a fixed point with a <>-exclusive char class instead of single-pass /<[^>]+>/g (incomplete sanitization + quadratic backtracking) in session-kit messages and share-kit claude parser. - Remove paired hidden-tag blocks in extractText via indexOf instead of lazy [\s\S]*? regexes. - Add regression tests incl. ReDoS timing probes for each fix.
- electron-vite bundles @spool-lab/cli/hub into the main process, so the cli package must be built before electron-vite build; add build:cli to the build:deps chain. - Replace SLASH_COMMAND_RECORD's lazy [\s\S]*? regex with an indexOf scan (stripSlashCommandRecords), clearing the remaining CodeQL polynomial-redos alert while preserving the optional-group semantics.
- The renderer bundles @spool-lab/session-view, so it must be built before electron-vite build; build:deps now covers every workspace package the app imports. - e2e launch helpers isolated claude/codex/gemini/opencode session dirs but not pi: an unset SPOOL_PI_DIR fell back to the real ~/.pi/agent/sessions, leaking real sessions into the test DB and breaking first/most-recent selectors across many specs.
- styles.css still pointed @source at ../../../session-view/src from the pre-restructure packages/app layout; Tailwind v4 silently ignores missing paths, so session-view's utility classes (table borders, cell max-width) never compiled into the app CSS. - Raise playwright globalTimeout to 900s and the e2e job timeout to 25m: 51 spec files cold-launch Electron serially under workers=1 on CI Linux, and 300s only ever covered ~2/3 of the suite.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
Two arcs in one branch. Arc 1 — the v2 walking skeleton (docs/spool-v2-design.zh-CN.md): the complete flow running end-to-end. Arc 2 — three-surface convergence (plans/spool-v2-convergence.zh-CN.md): desktop, CLI, and web collapse onto shared parse/render/transport layers.
Scope pinned by plans/spool-v2-pr1-scope.md (binding wire spec: OIDs, sequence chain, endpoints, view object, quotas).
Arc 1 — walking skeleton
packages/session-kit: browser-safe zero-dep core — JCS canonicalization with$SPOOL_WS/$SPOOL_HOMErewriting (machine-independent OIDs, no path leaks), sha256 OIDs, sequence hash chain with prefix roots, claude+codex edit-event extraction,deriveView,composeSessionDiffwith hunk↔record mapping./api/hub/v1/*): 3-step handshake —push(declare head, learn missing; per-user dedup kills the dedup oracle),objects/batch(server re-hashes everything; uncompressed R2 packs keep ranged GETs byte-addressable),head(folds the manifest to verify the root without touching bodies). Read path:no-storemeta (tombstone-immediate 410), immutable-cached view + NDJSON record ranges. Migrations0003_hub.sql+0004_hub_spool_file.sql, R2 bindingHUB, API tokens.spool login / share / resume / withdraw / show. Share canonicalizes at share time, runs a redact gate, git-commit-style note editor,@nprefix shares,--spool-fileattachment, lineage recovered from birth records, uuid-prefix resolution fromspool list's short ids. Resume verifies integrity client-side (per-record OIDs + root refold), materializes a fresh provider session under the workspace's REAL path (macOS/tmpsymlink trap fixed) — claude AND codex (date-partitioned rollouts, rewritten session_meta id) — appends one marked Spool birth record, and launches the native resume by default (--no-execto print).spool showcovers the design's command table: hub sids/URLs get the first-screen summary,--logthe timeline,--diffthe composed net change,@r<n>a single record./session/:sid): first screen with author-note vs machine-evidence zones, copyablespool resume <sid>command,#r/<idx>deep links, OG meta injection, router route.Arc 2 — convergence
packages/session-view: the desktop conversation UI (MessageList/MessageBubble/MarkdownContent/CodeBlock + findHighlightPlugin) extracted into a shared package. App re-imports with zero visual change (its own Tailwind pass via@source); non-Tailwind consumers get a prebuilt stylesheet withdark:bound tohtml[data-theme].requireHubUseraccepts it — zero new auth). CLI transport exposed via@spool-lab/cli/hub; the standalone hub-client package extraction is deferred until the in-flight cli-auth work lands..spooldocument (redact baked into bodies AND the title — caught by its own test); CLI attaches via--spool-file. The web page gains Conversation ↔ Document tabs (share-kitSnapshotReader+snapshotFromSpoolDocument) and a.spooldownload.Verified
Per-package: session-kit 24 · share-kit 71 · cli 114 · share-backend 293 (incl. full-stack round-trip: real CLI handlers ↔ real hub functions ↔ reader-side diff recompute ↔ resume on a second HOME) · share-web 106 · app 434. Root typecheck (18 tasks) + oxlint clean. Real-data validation: a 1126-record session shared, rendered, resumed, and withdrawn against a local hub.
Deliberately out (follow-ups)
Honest coverage gaps
claude --resumebinary; one manual round trip validated it on 2026-07-16, repeat before shipping.Ops notes
wrangler d1 migrations apply spool-share-db(0003 + 0004) · create R2 bucketspool-hub, dashboard bindingHUB·HUB_DEV_TOKENis local-dev only.Authorship: protocol/handshake/materialization/reader/convergence extraction by Claude; CLI plumbing, hub endpoint tests, and the session-kit first draft were Codex-drafted and landed after line-level review (per-commit notes). Parallel work by Xinyao (cli-auth device flow, WorkOS, pi source, reader prototypes) is intentionally NOT in these commits.