WIP: Slice 1 — first-class session homes, client state contract, session-list drift gate - #296
Draft
aarontrowbridge wants to merge 10 commits into
Draft
WIP: Slice 1 — first-class session homes, client state contract, session-list drift gate#296aarontrowbridge wants to merge 10 commits into
aarontrowbridge wants to merge 10 commits into
Conversation
…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.
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueThanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…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).
This was referenced Sep 5, 2026
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. |
This was referenced Sep 5, 2026
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.
Implements #294 (spec
spec-20260905-045114-session-device-lifecycle, D1/D2/D6), TDD, one acceptance criterion per commit where possible.Acceptance criteria
Project.resolvekeys non-git directories on their resolved worktree path (dir:<path>hash) instead of collapsing them intoglobalwith/as worktree — coreproject.test.tsProjectBackfill.runresolves every distinct session directory and re-keys stale rows — coreproject-backfill.test.tsproject-backfill.test.ts(one transaction per worktree, #272 discipline)ProjectJunkLint.prune(zero sessions of ANY state + no workspaces + past 30d cutoff; global survives) —project-junk-lint.test.ts; restore re-resolves by worktree insetArchived—session-restore-resolve.test.tsh1-client-boot.ts): headless boot against a seeded hub, timeline + hub-request-log assertion that the fetch is initiated before the list first rendersSessionCurrencyproperty tests (H4) insession-currency.test.ts; token wired into the v2session.listresponse as an additive optional field (currency)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 onsessions_fetchedso a real fetch always outranks the snapshotsessionListStatemachine; wired in the sessions dropdown AND the home sessions view (home index query'sisSuccessas the fetched source)command.panel.reset→resetSessionCaches(session fields, loadSessions/activeSessions query keys, all persisted snapshots); touch-list pinned by test; i18n in all 18 localessession-list-semantics.ts+ gate fixture (H5); the server's default limit imports the manifest so declared defaults are load-bearingNotes
Event.Updatedpayload, sosetArchived's restore re-key must carry the newprojectIDinside the published event, not just the SQL update.packages/app/vendor/opencode-ai-client-1.17.13-v2.tgz) predates the additivecurrencyfield — the client reads it structurally until the vendored snapshot is refreshed (re-vendoring is its own chore).packages/opencodetool.write > file permissions;packages/appi18n parity (session.exportTracemissing from 17 locales);packages/appbootstrap.test.ts/comments.test.ts/terminal.test.ts/submit.test.tsfail identically on base (solid-js server-condition export error).Closes #294.