Skip to content

Spool v2 walking skeleton: share → hub → web reader → resume in one flow#449

Merged
xinyao27 merged 49 commits into
mainfrom
feat/v2-share-flow
Jul 17, 2026
Merged

Spool v2 walking skeleton: share → hub → web reader → resume in one flow#449
xinyao27 merged 49 commits into
mainfrom
feat/v2-share-flow

Conversation

@xinyao27

@xinyao27 xinyao27 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

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.

provider session ─ spool share / desktop one-click ─→ hub (/api/hub/v1) ─→ spool.pro/session/<sid>
                                                                       └─→ spool resume <sid>

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_HOME rewriting (machine-independent OIDs, no path leaks), sha256 OIDs, sequence hash chain with prefix roots, claude+codex edit-event extraction, deriveView, composeSessionDiff with hunk↔record mapping.
  • Hub (share-backend /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-store meta (tombstone-immediate 410), immutable-cached view + NDJSON record ranges. Migrations 0003_hub.sql + 0004_hub_spool_file.sql, R2 binding HUB, API tokens.
  • CLI: spool login / share / resume / withdraw / show. Share canonicalizes at share time, runs a redact gate, git-commit-style note editor, @n prefix shares, --spool-file attachment, lineage recovered from birth records, uuid-prefix resolution from spool 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 /tmp symlink 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-exec to print). spool show covers the design's command table: hub sids/URLs get the first-screen summary, --log the timeline, --diff the composed net change, @r<n> a single record.
  • Web (/session/:sid): first screen with author-note vs machine-evidence zones, copyable spool resume <sid> command, #r/<idx> deep links, OG meta injection, router route.

Arc 2 — convergence

  • P1 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 with dark: bound to html[data-theme].
  • P2 one parsing brain: core's claude/codex parsers split at the I/O seam; the pure body lives in session-kit, core keeps thin fs wrappers (all 417 core tests guard the move). The web session page fetches all records with progress, parses through the SAME parser, and renders with the SAME components the desktop uses. Deep links / diff hunks resolve record indices → conversation messages.
  • P3 desktop one-click share: "Share to spool.pro" in the session-detail menu. Prepare computes records/diffstat/redact findings locally before anything leaves the machine; publish runs the CLI's exact handshake through the app's existing OAuth session bearer (requireHubUser accepts 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.
  • P4 .spool attachment + Document view: every desktop share auto-attaches a sanitized .spool document (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-kit SnapshotReader + snapshotFromSpoolDocument) and a .spool download.

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)

  • hub-client package extraction (waiting on the cli-auth work), Ed25519 head verification, gemini/opencode, OG image, Trace/blame/why, true list virtualization of huge documents.
  • Withdraw ≠ delete: R2 objects retained by design; account deletion must purge hub objects before GA.

Honest coverage gaps

  • Reader UI is logic-tested (route/NDJSON continuation/fallbacks/mapping) — no Playwright e2e of the page DOM.
  • CI never drives the real claude --resume binary; one manual round trip validated it on 2026-07-16, repeat before shipping.
  • Codex rollout formats vary in the wild; fidelity tests cover known variants, and the full-stack round trip now exercises a codex share → resume leg.

Ops notes

  • wrangler d1 migrations apply spool-share-db (0003 + 0004) · create R2 bucket spool-hub, dashboard binding HUB · HUB_DEV_TOKEN is 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.

xinyao27 added 17 commits July 16, 2026 19:37
…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.
… 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.
Comment thread packages/session-kit/src/messages.ts Fixed
Comment thread packages/session-kit/src/messages.ts Fixed
Comment thread packages/session-kit/src/messages.ts Fixed
Comment thread packages/session-kit/src/messages.ts Fixed
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.
Comment thread apps/cli/src/hub/client.ts Fixed
Comment thread apps/cli/src/hub/credentials.ts Fixed
xinyao27 and others added 3 commits July 16, 2026 22:52
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.
Comment thread packages/core/src/parsers/pi.ts Fixed
xinyao27 added 6 commits July 17, 2026 08:10
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.
xinyao27 added 6 commits July 17, 2026 10:54
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).
Comment thread apps/cli/src/hub/client.ts Fixed
Comment thread apps/cli/src/hub/credentials.ts Fixed
xinyao27 added 13 commits July 17, 2026 15:32
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.
Comment thread packages/session-kit/src/messages.ts Fixed
xinyao27 added 3 commits July 17, 2026 21:59
- 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.
@xinyao27
xinyao27 added this pull request to the merge queue Jul 17, 2026
Merged via the queue into main with commit b9299d4 Jul 17, 2026
6 checks passed
@xinyao27
xinyao27 deleted the feat/v2-share-flow branch July 17, 2026 14:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants