Skip to content

WIP: Slice 1 — first-class session homes, client state contract, session-list drift gate - #296

Draft
aarontrowbridge wants to merge 10 commits into
local/amicodefrom
slice1-session-lifecycle
Draft

WIP: Slice 1 — first-class session homes, client state contract, session-list drift gate#296
aarontrowbridge wants to merge 10 commits into
local/amicodefrom
slice1-session-lifecycle

Conversation

@aarontrowbridge

@aarontrowbridge aarontrowbridge commented Sep 5, 2026

Copy link
Copy Markdown
Member

Implements #294 (spec spec-20260905-045114-session-device-lifecycle, D1/D2/D6), TDD, one acceptance criterion per commit where possible.

Acceptance criteria

Criterion State Evidence
Non-git homes render as first-class panel groups (new homes) green Project.resolve keys non-git directories on their resolved worktree path (dir:<path> hash) instead of collapsing them into global with / as worktree — core project.test.ts
… (homes created before this change — backfill) green ProjectBackfill.run resolves every distinct session directory and re-keys stale rows — core project-backfill.test.ts
Non-git home that later becomes a git repo re-keys cleanly; mid-flight merges converge green re-key + partial-merge convergence test in project-backfill.test.ts (one transaction per worktree, #272 discipline)
Junk-home lint never prunes a home with archived sessions; restore re-resolves green ProjectJunkLint.prune (zero sessions of ANY state + no workspaces + past 30d cutoff; global survives) — project-junk-lint.test.ts; restore re-resolves by worktree in setArchivedsession-restore-resolve.test.ts
Fresh client boots with a session-list fetch before first render (#288 harness) green H1 harness (h1-client-boot.ts): headless boot against a seeded hub, timeline + hub-request-log assertion that the fetch is initiated before the list first renders
Derived currency token advances on same-tick writes / archive churn / delete-then-touch / out-of-band writes green SessionCurrency property tests (H4) in session-currency.test.ts; token wired into the v2 session.list response as an additive optional field (currency)
#293 stale-storage client self-heals on boot green persisted per-workspace session snapshot (session:snapshot) + bootCurrencyDecision: a stale or tokenless snapshot is proven stale on every boot and invalidated by the overwrite — H1 harness tests cover the founding shape; hydration is gated on sessions_fetched so a real fetch always outranks the snapshot
"Not yet fetched" vs "genuinely empty" rendered differently green sessionListState machine; wired in the sessions dropdown AND the home sessions view (home index query's isSuccess as the fetched source)
In-product reset clears session caches only; preferences survive green command.panel.resetresetSessionCaches (session fields, loadSessions/activeSessions query keys, all persisted snapshots); touch-list pinned by test; i18n in all 18 locales
Boot fetch paginated (recent tail first) green bounded tests seed a 1250-session store: desc order, three continuation pages, one-page stores cost one request, abort honored. Found and fixed a real bug: the loader stopped on a short page instead of on cursor exhaustion, silently dropping sessions past a hub-capped page
Drift gate: semantic change fails, additive optional field passes green frozen semantics manifest session-list-semantics.ts + gate fixture (H5); the server's default limit imports the manifest so declared defaults are load-bearing

Notes

  • The D1 resolution change rewrites the old "returns global for non-git directory" tests (core + opencode packages) — that assertion encoded the incident-1 behavior, not a contract to preserve.
  • A finding worth carrying: the session projector rewrites the whole session row from every Event.Updated payload, so setArchived's restore re-key must carry the new projectID inside the published event, not just the SQL update.
  • The vendored client tarball (packages/app/vendor/opencode-ai-client-1.17.13-v2.tgz) predates the additive currency field — the client reads it structurally until the vendored snapshot is refreshed (re-vendoring is its own chore).
  • H1 is a headless harness for the client's session-list layer (real boot pieces, mock hub, request log) — it does not drive a DOM render; the honest-states UI wiring is covered by the components + the tested state machine.
  • Pre-existing failures on the clean base commit, not touched here: packages/opencode tool.write > file permissions; packages/app i18n parity (session.exportTrace missing from 17 locales); packages/app bootstrap.test.ts/comments.test.ts/terminal.test.ts/submit.test.ts fail identically on base (solid-js server-condition export error).
  • Out of scope per the issue: retention policy (slice 3), fleet overlay (slice 4).

Closes #294.

…jects

D1 (spec spec-20260905-045114-session-device-lifecycle): Project.resolve now
keys non-git homes on their resolved worktree path (dir:<path> hash) instead
of collapsing them into the global project with the filesystem root as
worktree — the founding shape of the invisible-group incident, where 687
sessions were API-visible but rendered nowhere. Git-derived identity is
untouched; global remains defined for legacy rows.
D1 (spec spec-20260905-045114-session-device-lifecycle): ProjectBackfill.run
resolves every distinct session directory, re-keys sessions whose project row
no longer matches that resolution, and retires the superseded auto-created
(directory-keyed) row — so a non-git home that later becomes a git repo merges
into the git-derived project. One transaction per worktree (#272 discipline):
a merge killed mid-flight converges on the next run, verified by the
partial-merge convergence test.

Wired into legacy Project.init (forked, failure-logged) so every instance
bootstrap reconciles all homes; the backfill itself is idempotent.
…by worktree

D1 (spec spec-20260905-045114-session-device-lifecycle): ProjectJunkLint.prune
removes project rows that have held no sessions of ANY state (active or
archived) and no workspaces past the 30-day cutoff — the global fallback and
archived-only homes survive by construction, verified per-row inside the
delete transaction. Restore (setArchived with time undefined) re-resolves the
session's home by worktree and carries the re-resolved project identity in the
published event too — the projector rewrites the whole session row from the
event, so a stale projectID there would revert the re-key.
…ponse

D2/H4 (spec spec-20260905-045114-session-device-lifecycle): session.list now
returns a currency token derived from the rendered projection — (count, max
time_updated, sum of time_updated) over the returned rows — plus the hub build
id, recomputed on every response. Property tests enforce 'derived, never
hand-bumped': same-tick writes, archive churn, delete-then-touch pairs, and
direct out-of-band SQL writes all advance it by construction, and it is a pure
function of the projection. Additive optional field with base default
(absent) — permitted by D6's change policy and registered in the semantics
manifest added next.
…gate

D6/H5 (spec spec-20260905-045114-session-device-lifecycle): the query
semantics of GET /api/session (scoping keys, filter fields, defaults) are
declared frozen surface in a checked-in manifest. The gate fixture fails any
semantic change — removals, requiredness flips, kind changes, default changes
— without a companion manifest update, while additive optional fields with
base defaults pass untouched. The server's default limit now imports the
manifest's constant, so declared defaults are load-bearing rather than
documentation. The founding incident: the v1 route's silent
directory-filtered → project-scoped change.
D2 (spec spec-20260905-045114-session-device-lifecycle): the UI now
distinguishes 'not yet fetched' from 'genuinely empty' — a session-list fetch
that has not resolved renders Loading, never the empty state, closing #293's
invisible failure shape. Child stores carry a sessions_fetched flag set when a
list fetch resolves (either path in loadSessions); the sessions dropdown
routes through sessionListState.

'Reset panel state' (command.panel.reset, all 18 locales) clears session
caches only — in-memory session fields, sessionTotal/status, and the
loadSessions/activeSessions query keys — and never touches persisted
workspace preferences or user drafts. The reset's touch-list is pinned by a
unit test so recovery can never silently grow into configuration.
@coderabbitai

coderabbitai Bot commented Sep 5, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…ion snapshot

D2 (spec spec-20260905-045114-session-device-lifecycle): the client now
persists a session-list snapshot per workspace (session:snapshot, render
accelerator — never authority) alongside the server's derived currency token.
Every list response verifies the snapshot via bootCurrencyDecision: a stale or
tokenless snapshot — the #293 stale-storage shape — is proven stale and
invalidated by the overwrite, so the client self-heals on boot with zero
manual action. Hydration is gated on sessions_fetched: a real fetch always
outranks the snapshot. The panel reset invalidates all snapshots; the reset
touch-list pins session:snapshot as a session cache. The vendored client's
generated types predate the additive currency field — read structurally until
the vendored snapshot is refreshed.
D2: the home sessions view routes through the same sessionListState machine
as the dropdown — while the home session-index query has never succeeded, the
view renders the loading skeleton instead of the empty state; only a resolved
fetch (even an empty one) may render 'genuinely empty'. The controller exposes
listState (isSuccess as the fetched source); the glue and view thread it
through. Home contract pinned in session-list-state.test.ts.
D2/D4 (spec spec-20260905-045114-session-device-lifecycle): the home
session-index boot fetch stopped paginating whenever a page came back shorter
than the requested limit — so a hub that caps page size below the request
silently dropped every session past its first short page. The cursor is the
only exhaustion signal. Bounded pagination tests seed a 1250-session store:
desc order (recent tail first), three continuation pages, single-page stores
cost one request, abort signal honored between pages.
…quest log)

D2 (spec spec-20260905-045114-session-device-lifecycle): the #288 boot
harness extracted as a named, reusable fixture. Headless by design — it
composes the real boot pieces (persisted-snapshot hydration, the
unconditional boot fetch, bootCurrencyDecision, the sessionListState machine)
in the boot order the contract requires, against a seeded hub whose request
log is the assertion surface (the same evidence that diagnosed #293).

The fetch-before-first-render and boot-self-heal criteria's tests wire
through it: a fresh boot's session-list fetch is initiated before the list
first renders (timeline assertion + hub log), and a client seeded with a
stale or tokenless snapshot — the #293 shape — self-heals with zero manual
action (rendered rows are the hub's; the snapshot is re-primed with the
server's token).
@aarontrowbridge

Copy link
Copy Markdown
Member Author

HELD — do not merge. The fork is retiring (harness-agnostic re-base landed: amicode main = canonical v1.18.29 + app overlay). This PR is the reference implementation of the session/device lifecycle mechanisms (spec-20260905-045114); re-homing: client pieces → amicode's app overlay, server semantics → Harness Contract vNext + upstream reference PRs. See the spec's placement amendment.

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.

Slice 1: first-class session homes, client state contract, session-list drift gate

1 participant