11 KiB
11 KiB
Task: Implement PI-110 Renderer Snapshot Patch Store
Identity
- Task ID: 20260823-pi-renderer-store-b7e2c4a1
- Mode: Feature
- Branch: codex/20260823-pi-renderer-store-b7e2c4a1-pi-renderer-store
- Worktree: D:\Datas\OthersProjects\makelore-pi-renderer-store-b7e2c4a1
- Base commit:
a0722574f3 - Owner: codex-root
- Status: Done
Scope
- Implement PI-110's vendor-neutral Renderer facade and one Zustand store keyed
by stable Makelore
conversationId. - Consume the shared PI-010 Snapshot/Patch reducer for snapshot-first SSE, stale-generation discard, target-only gap recovery, and reconnect.
- Own per-Conversation snapshot/run/error/queue/model/draft/unread/cursor plus optimistic prompt request state and fine-grained selectors.
- Add focused facade/store/SSE/render-count tests. Do not migrate Chat UI, feature-complete controls, or remove the old OpenCode store in this ticket.
Intent And Constraints
- Base is the planner-approved PI-100 cumulative HEAD
a072257; the exact and sole Ready Frontier is{PI-110}. - Renderer access goes only through
src/lib/host-api.ts; the SSE facade must initialize the cached Host token/base before constructing EventSource. - Reuse the PI-010 contract/reducer as the single product normalization seam.
The first real Renderer consumer exposed that its Electron-only source
location requires unavailable composite-project declarations in a clean
worktree, so the canonical modules now live in
shared/and Electron keeps thin re-exports. Do not duplicate Pi/OpenCode event mapping or import vendor types, paths, wire enums, credentials, or raw diagnostics. - A generation/sequence gap invalidates and reloads only its target Conversation. Old generations are ignored and accepted prompts are never replayed because an event stream reconnects.
- Optimistic user identity is stable across durable reconciliation by
clientRequestId. Definite rejection and uncertain acceptance retain enough request/draft/attachment state for explicit UI recovery; neither path silently resends. - Store updates replace only the target Conversation entry. Timeline selectors for the selected Conversation must not rerender when a hidden Conversation streams; hidden/sidebar consumers use lightweight summaries.
- PI-120 owns Composer and timeline UI; PI-130 owns model/thinking/queue/ compaction/interaction/subagent/changes UI; PI-140 owns old OpenCode removal.
- Real Provider remains Explicitly Waived / Accepted Risk with
realTurnVerified=false; macOS remains deferred to mandatory PI-150. Neither is Pass evidence. No subagents are authorized.
Project Context Loaded
- Task identity:
20260823-pi-renderer-store-b7e2c4a1, feature mode, isolated branch/worktree above, basea0722574f3b36cb73ec8833787711e6bef1c5c54. - Concurrent Task Gate passed. The only semantically adjacent planning task,
20260820-partner-create-model-parity-c83d, still has no defined scope or owned files. The canonical main integration task is Blocked and is not a write target. Other active tasks concern unrelated Canvas/Robot/Learning or review scopes. - Planning Gate inputs read: memory index, project positioning/success fields, current state, decision/system/architecture/domain documents, evidence/ reflection/commitment/stale indexes, PI-010 and PI-100 records, and the exact PI-110 ticket plus relevant Spec sections.
- The canonical project-positioning fields remain placeholders; repository guidance and the planner-owned Pi Spec/ticket are authoritative here.
- Confirmed seams: vendor-neutral contracts and strict reducer in
electron/coding-runtime, snapshot-first/api/coding/events, target GET snapshot,hostApiFetch,ensureHostApiToken, and Zustand selector patterns. - No accepted ADR changes product positioning or conflicts with this task. Existing real OpenCode/provider commitments remain historical and do not authorize a compatibility store or runtime fallback.
- Gate result: Passed.
Plan
- Add a Renderer-owned type surface and minimal typed
/api/codingfacade for target snapshot, prompt acceptance, and initialized global/target SSE. - Implement a dependency-injectable Zustand store that keeps reducer state, summary, draft, unread, request lifecycle, loading/recovery and errors per Conversation while owning selection and one global event stream.
- Route snapshot and ordered patch events through the shared reducer; ignore stale generation, reload only a gapped target, and never replay mutations on reconnect.
- Add optimistic user-node/draft/attachment reconciliation and granular selectors that preserve unrelated Conversation object identity.
- Add facade, two-Conversation SSE, gap/reconnect, optimistic failure, vendor boundary and render-count regressions; then run focused/full verification, build, documentation gates, and planner review.
Outcome
- Added a Renderer-owned product type surface and minimal typed Host facade for target snapshots, prompt acceptance, and EventSource creation. EventSource construction first hydrates the cached Host token/base, including a cold Renderer process.
- Added one dependency-injectable vanilla Zustand store with React selectors. Each Conversation owns its reducer snapshot/cursor, load and target error, unread state, lightweight summary, draft revision/attachments, and prompt request lifecycle. Store updates preserve unrelated Conversation identity.
- Added snapshot-first SSE ingestion, native reconnect state, stale-generation discard, per-target sequence/generation-gap recovery, load single-flight, and connection-generation protection. Reconnect never replays a prompt and an obsolete connection flight cannot clear a newer one.
- Closed the planner review's two P1 recovery windows: patches arriving while
a target GET is in flight are buffered only for that invalidated Conversation
and replayed in generation/sequence order after a valid Snapshot; unresolved
pending/accepted/uncertain prompt requests re-overlay their local optimistic
nodes after snapshot-first reconnect until a durable
clientRequestIdreconciliation arrives. If the reconnect Snapshot already contains that durable message, its message id is first reconciled to the local request's stable UI node id before the request is removed. Neither mechanism replays a mutation. - Added optimistic user nodes keyed by
clientRequestId. Durable upsert keeps the optimistic UI node id; definite rejection restores an untouched draft and marks the node failed; uncertain delivery restores the draft while retaining the request/node for explicit reconciliation without resend. - Promoted the existing product-neutral PI-010 contracts, reducer, product
detail validator, and subagent detail validator to
shared/. Existing Electron paths are compatibility-free source re-exports of the same functions, so Main, Renderer, fixtures, and tests cannot drift into separate reducer implementations. - Added facade/store tests for two interleaved Conversations, target-only gap recovery, stale generations, native reconnect, connection-flight ABA, optimistic accepted/rejected/uncertain outcomes, attachment restoration, vendor-neutral source boundaries, shared reducer identity, and render-count/ summary identity under hidden streaming.
- PI-120/130/140 boundaries remain unchanged. No Chat page or legacy OpenCode store consumer was changed by PI-110.
- Implementation commits:
36626c8(Renderer store/shared seam),1abb2ff(gap recovery + snapshot optimistic overlay), and934ba02(durable Snapshot UI identity reconciliation).
Verification
pnpm exec vitest run tests/unit/coding-conversations-facade.test.ts tests/unit/coding-conversations-store.test.tsx: initial implementation 2 files / 12 tests passed; after review fixes, 2 files / 14 tests passed, including delayed gap GET + concurrent patch replay, reconnect Snapshot optimistic-node continuity, and a reconnect Snapshot that already contains the durable user message while preservingnode-1.pnpm exec vitest runfor the two PI-110 tests plus PI-010 contracts, coding core routes, event projector, Conversation runtime, product tools, and subagent suites: initial 8 files / 75 tests; final review-fix range 8 files / 77 tests passed. One intervening parallel run hit a non-reproducibleCODING_STORAGE_WRITE_FAILEDin the unrelated Conversation runtime temp persistence test; that file passed alone immediately afterward and the same 8-file command then passed in full.pnpm run typecheck: passed after the shared product seam removed the clean Renderer -> composite Electron declaration dependency.pnpm run lint:check: passed with zero errors and the same six unrelated Renderer warnings inExecutionGraphCard.tsx, Home, and Makelore.- Final
pnpm testat934ba02: 211 files / 2277 passed / 2 skipped (2279 total). pnpm run build:vite: Renderer, Electron Main, Preload, and release utility production builds passed; existing dynamic-import and large-chunk warnings remain.- Electron E2E is not applicable yet: PI-110 intentionally exposes no page or user-visible interaction. The real Chat/Composer binding and its E2E belong to PI-120; the render-count store acceptance is covered directly here.
- Real Provider remains Explicitly Waived / Accepted Risk with
realTurnVerified=false. macOS x64/arm64 remains deferred to mandatory PI-150. Neither is recorded as Pass. - Planner review of
a072257...36626c8: Standards PASS / 0 findings; Spec Needs Fix with two P1 findings for recovery-window patch loss and reconnect Snapshot optimistic-node loss. The first fixed-range re-review accepted gap replay and missing-durable-node overlay, then found one remaining P1 where a Snapshot already containing the durable node adopted the server id. That reproduction is now fixed byclientRequestIdid reconciliation before request cleanup, with the focused regression above. - Planner final review of
a072257...934ba02: Standards PASS / 0 findings and Spec PASS / 0 findings. Independent reproduction ended withnode-1, the durablesourceEntryId, zero pending requests, and exactly one prompt call; no Conversation leakage, retry loop, or mutation replay was found.
Follow-ups
- PI-120 should bind the Composer/timeline to this store without restoring any
read from
src/stores/opencode.ts. - Planner dependency computation identifies
{PI-120}as the next exact Ready Frontier after this task reachesready_for_integration; PI-130/140 remain locked by their declared dependencies.
Promotion Candidates
- Type: Architecture. Proposal: keep Makelore-owned Conversation contracts and
the strict Snapshot/Patch reducer in
shared/, with Main and Renderer importing/re-exporting that single implementation rather than depending on Electron composite build artifacts or copying normalization logic. Evidence: cleanpnpm run typecheck, shared/Main function identity regression, final 8-file / 77-test impacted suite, and 211-file / 2277-passed full unit/build verification. Future impact: PI-120/130 can consume product DTOs without vendor/Main imports; any contract amendment must retain Main/Renderer reducer equivalence.