> For the complete documentation index, see [llms.txt](https://faction-os.gitbook.io/faction-os-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://faction-os.gitbook.io/faction-os-docs/.spec_system/archive/sessions/phase22-session06-persistence-lifecycle/spec.md).

# Session Specification

**Session ID**: `phase22-session06-persistence-lifecycle` **Phase**: 22 - Projection Foundation **Status**: Not Started **Created**: 2026-07-05 **Base Commit**: 2ad15c9ddaa9c9b9fcb478aad5b3d276931ba095 **Package**: apps/web **Package Stack**: TypeScript

***

## 1. Session Overview

This session adds the browser-local persistence lifecycle for the Phase 22 `GameProjection` aggregate. Session 05 already hydrates the projection and folds each server event once through `useGameStore`; this session adds the guarded write path, debounce cadence, max-latency flush, page lifecycle flush, test flush helper, and reset storage removal that make that state durable.

It is next because sessions 01-05 established the projection contract, reducer coverage, camp-link boundaries, store field, selectors, and one-event folding. Persistence must now be guarded so replay and mock traffic can still fold in memory without writing false progress into `localStorage["factionos-game-v1"]`.

The work stays non-visual and browser-local. It adds no battlefield rendering, audio, toasts, hosted persistence, cross-device sync, protocol events, server routes, or second projection state authority.

***

## 2. Objectives

1. Add replay-buffer-style projection persistence scheduling with a 2 second idle debounce, 30 second max-latency flush, and synchronous test flush.
2. Schedule projection writes only when `applyEvent(e)` changes projection state and `shouldPersistGameProjection(ctx)` allows persistence.
3. Flush pending projection writes on hidden `visibilitychange`, `pagehide`, and `beforeunload` without throwing when browser APIs are unavailable.
4. Ensure `resetToSeed()` creates a fresh projection and removes the persisted projection storage key.

***

## 3. Prerequisites

### Required Sessions

* [x] `phase22-session01-projection-contract` - Provides projection storage constants, parse/load/persist helpers, and `shouldPersistGameProjection`.
* [x] `phase22-session02-hero-attention-reducer` - Provides replay-safe attention reducer behavior.
* [x] `phase22-session03-legion-enemy-reducer` - Provides replay-safe enemy reducer behavior.
* [x] `phase22-session04-camp-link-boundaries` - Provides camp-link reducer behavior without fabricated progress.
* [x] `phase22-session05-store-folding-and-selectors` - Provides store hydration, one-event folding, selectors, and in-memory reset behavior.

### Required Tools Or Knowledge

* npm workspace commands from `.spec_system/CONVENTIONS.md`.
* Existing replay persistence helpers in `apps/web/src/store/useGameStore.ts`.
* Existing projection persistence helpers in `apps/web/src/lib/gameProjection.ts`.
* Existing focused store and replay persistence tests in `apps/web/tests/gameProjectionStore.test.ts` and `apps/web/tests/replayPersist.test.ts`.

### Environment Requirements

* Node 26.2.0+ and npm 11.16.0+ per project conventions.
* Vitest with happy-dom `localStorage`, `window`, and `document` support.
* No database, migration, server, hosted service, Worker, or credential dependency applies.

***

## 4. Scope

### In Scope (MVP)

* The web store schedules `persistGameProjection()` only when projection folding changes state and `shouldPersistGameProjection({ isReplaying, mockEnabled })` returns true.
* Projection persistence mirrors the replay-buffer posture while using Phase 22 constants: `GAME_PROJECTION_PERSIST_DEBOUNCE_MS` for idle debounce and `GAME_PROJECTION_PERSIST_MAX_LATENCY_MS` for forced max-latency flush.
* A synchronous `__flushGameProjectionPersist()` helper drains pending projection writes for tests without wall-clock delays.
* Page lifecycle handlers flush pending projection writes on hidden `visibilitychange`, `pagehide`, and `beforeunload`, with idempotent listener installation and safe behavior when browser globals are missing.
* `resetToSeed()` resets the in-memory projection and removes `localStorage["factionos-game-v1"]`.
* Focused tests cover debounce/flush behavior, max-latency behavior, replay/mock no-write behavior, page lifecycle flushing, unavailable storage, and reset storage removal.

### Out Of Scope (Deferred)

* Browser-local erasure inventory updates and `localErasure` tests - Reason: Session 08 owns browser erasure coverage and documentation closeout.
* Broad replay/reconnect anti-farming regression matrix - Reason: Session 07 owns the public replay and reconnect honesty regression set after persistence exists.
* New rendering, battlefield game presentation, audio/SFX, toasts, hosted or cross-device persistence, protocol events, server routes, and new state authorities - Reason: Phase 22 only establishes the projection authority and local persistence lifecycle.

***

## 5. Technical Approach

### Architecture

Extend the existing projection imports in `apps/web/src/store/useGameStore.ts` to include the storage key, cadence constants, `persistGameProjection`, and `shouldPersistGameProjection`. Replace or wrap the current store-local `foldGameProjection` helper so it returns both the next projection and the persistence context used for the fold. That keeps one source of truth for `isReplaying` and `mockEnabled`.

Add projection-specific scheduling helpers near the replay persistence helpers. The scheduler stores the latest changed projection, arms a 2 second idle timer, and also arms a 30 second max-latency timer for the first pending write in a burst. Flushing clears both timers, calls `persistGameProjection` with the pending projection, and then clears pending state. Storage and serialization failures stay swallowed by `persistGameProjection`.

Install page lifecycle listeners from the store module through an idempotent helper that is safe when `window` or `document` is unavailable. The `visibilitychange` handler flushes only when `document.hidden` is true; `pagehide` and `beforeunload` always flush pending projection writes. `resetToSeed()` clears pending projection writes, removes the projection storage key, and commits a fresh `createInitialGameProjection()`.

### Design Patterns

* Replay persistence parity: Keep the projection scheduler small, synchronous to flush, and local to `useGameStore.ts`, matching the existing replay helper test posture.
* Guarded write boundary: Use `shouldPersistGameProjection` instead of reimplementing replay/mock conditions in the store.
* Idempotent lifecycle setup: Avoid duplicate global listeners across module reloads in tests and normal app startup.
* Focused happy-dom tests: Prove browser lifecycle and storage behavior at the store seam without rendering components.

***

## 6. Deliverables

### Files To Create

| File | Purpose                                                | Est. Lines |
| ---- | ------------------------------------------------------ | ---------- |
| None | This session extends existing store and test coverage. | 0          |

### Files To Modify

| File                                         | Changes                                                                                                                                           | Est. Lines |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `apps/web/src/store/useGameStore.ts`         | Add projection persistence scheduler, flush helper, guarded scheduling from `applyEvent`, lifecycle listener setup, and reset storage removal.    | \~150      |
| `apps/web/tests/gameProjectionStore.test.ts` | Extend focused store tests for projection persistence scheduling, replay/mock guards, lifecycle flushing, unavailable storage, and reset removal. | \~180      |
| `apps/web/tests/replayPersist.test.ts`       | Add focused parity coverage only if shared replay/projection scheduler behavior requires it; otherwise leave unchanged.                           | \~40       |
| `apps/web/src/lib/gameProjection.ts`         | Add only narrowly needed type/test seam adjustments if store integration exposes a missing production helper.                                     | \~20       |

***

## 7. Success Criteria

### Functional Requirements

* [ ] Projection writes are scheduled only after `applyEvent(e)` changes `gameProjection` by reference.
* [ ] Projection writes are skipped while replaying.
* [ ] Projection writes are skipped when `useSettingsStore.getState().mockEnabled` is true.
* [ ] Pending projection writes flush after the 2 second idle debounce or the 30 second max-latency window.
* [ ] `__flushGameProjectionPersist()` drains pending projection writes synchronously for tests.
* [ ] Hidden `visibilitychange`, `pagehide`, and `beforeunload` flush pending writes without throwing when storage or browser lifecycle APIs are unavailable.
* [ ] `resetToSeed()` leaves a fresh in-memory projection and removes `localStorage["factionos-game-v1"]`.

### Testing Requirements

* [ ] `apps/web/tests/gameProjectionStore.test.ts` covers debounce/flush, max-latency flush, replay no-write, mock no-write, lifecycle flush, unavailable-storage behavior, and reset storage removal.
* [ ] Existing reducer coverage in `apps/web/tests/gameProjection.test.ts` still passes.
* [ ] Existing replay persistence coverage in `apps/web/tests/replayPersist.test.ts` still passes.
* [ ] `npm test -- apps/web/tests/gameProjectionStore.test.ts apps/web/tests/gameProjection.test.ts apps/web/tests/replayPersist.test.ts` passes.
* [ ] `npm --workspace @factionos/web run typecheck` passes.

### Non-Functional Requirements

* [ ] Persisted projection state remains bounded aggregate JSON, not an event log or presentation queue.
* [ ] Store code does not duplicate projection reducer logic or persistence guard rules owned by `gameProjection.ts`.
* [ ] No new UI, protocol, server, hosted persistence, audio, or second projection authority is introduced.

### Quality Gates

* [ ] All files ASCII-encoded.
* [ ] Unix LF line endings.
* [ ] Code follows project conventions.
* [ ] `npm exec -- biome check apps/web/src/store/useGameStore.ts apps/web/src/lib/gameProjection.ts apps/web/tests/gameProjectionStore.test.ts apps/web/tests/replayPersist.test.ts` passes for changed files.
* [ ] Primary user-facing surfaces are N/A because this session changes store/test code only.

***

## 8. Implementation Notes

### Working Assumptions

* Module-local lifecycle helper: `useGameStore.ts` is already the singleton store module and owns replay persistence helpers, so projection persistence lifecycle hooks can live in the same file with an idempotent installer. The repo evidence is the existing replay scheduler and `useGameStore` import surface; planning can proceed because no React component lifecycle is needed.
* Persistence write tests must disable mock mode explicitly: `DEFAULT_SETTINGS` has `mockEnabled: true`, and Phase 22 names `mockEnabled` as the drill write guard. Tests that expect writes should set `mockEnabled: false`; tests that expect no writes can set or leave it true.
* Page lifecycle tests can use happy-dom events: existing Vitest tests already run under happy-dom with `localStorage`, `window`, and `document`; lifecycle flush assertions can dispatch `visibilitychange`, `pagehide`, and `beforeunload` directly.

### Conflict Resolutions

* Replay debounce vs projection cadence: Existing replay persistence uses a 250 ms local debounce, while Phase 22 explicitly requires 2 second idle debounce and 30 second max-latency projection flush. Projection persistence should use `GAME_PROJECTION_PERSIST_DEBOUNCE_MS` and `GAME_PROJECTION_PERSIST_MAX_LATENCY_MS`; replay persistence remains unchanged.
* Reset memory vs storage removal: Session 05 intentionally left projection storage untouched in `resetToSeed()`, while Session 06 requires storage removal. The chosen interpretation is that Session 06 replaces the Session 05 deferred behavior and updates the affected store test expectations.

### Key Considerations

* `persistGameProjection` already swallows unavailable storage, quota, and serialization failures; store tests should prove no throw at the public seam.
* `shouldPersistGameProjection` is the only guard for replay/mock writes, so store code should not add a second divergent condition.
* The scheduler should not write when unknown events return the exact same projection reference.
* Lifecycle flush should drain only pending projection writes, not mutate the replay buffer or unrelated store branches.

### Potential Challenges

* Module-level timers can leak across tests: call `__flushGameProjectionPersist` and `__flushReplayPersist` in teardown where relevant.
* Fake timers and module resets can hide max-latency behavior: keep tests deterministic by importing fresh store modules, setting fixed time, and advancing timers explicitly.
* Browser global listener installation can duplicate across fresh imports: use an idempotent module-local flag and test behavior rather than listener counts where possible.

### Relevant Considerations

* \[P18-apps/web] **Pure normalization before store mutation**: Keep projection folding at the existing pre-switch store seam and only add persistence scheduling after that fold changes state.
* \[P20] **Broad privacy gates are release-critical**: Persist only the projection aggregate; do not write raw prompts, terminal output, file contents, provider payloads, tokens, secrets, diagnostics, or broad paths.
* \[P08] **Full trusted erasure remains no-claim**: Reset storage removal is browser-local cleanup only and must not imply trusted unified erasure.
* \[P03] **Stable docs are the current contract**: Follow live Phase 22 PRD, package README, and current store/test patterns instead of archived evidence.

### Behavioral Quality Focus

Checklist active: Yes Top behavioral risks for this session:

* State-mutating persistence writes must have duplicate-trigger prevention while a debounce/max-latency write is in flight.
* Browser lifecycle handlers must tolerate unavailable `window`, `document`, and storage APIs without throwing.
* Replay and mock traffic must be unable to persist drill or replay-generated progress even when in-memory folding changes projection state.

***

## 9. Testing Strategy

### Unit Tests

* Extend `apps/web/tests/gameProjectionStore.test.ts` with focused tests for debounce flush, max-latency flush, lifecycle flush, reset removal, and reference-unchanged no scheduling.
* Use fixed server events that visibly change projection state, such as `awaiting_input`, `input_received`, `ts_errors`, or failed `tool_result`.

### Integration Tests

* Exercise `useGameStore.getState().applyEvent()` as the seam between event capture, projection folding, settings-derived persistence context, and scheduling.
* Exercise `resetToSeed()` through the public store API to prove both memory reset and storage removal.

### Runtime Verification

* Run `npm test -- apps/web/tests/gameProjectionStore.test.ts apps/web/tests/gameProjection.test.ts apps/web/tests/replayPersist.test.ts`.
* Run `npm --workspace @factionos/web run typecheck`.
* Run `npm exec -- biome check apps/web/src/store/useGameStore.ts apps/web/src/lib/gameProjection.ts apps/web/tests/gameProjectionStore.test.ts apps/web/tests/replayPersist.test.ts`.

### Edge Cases

* Unknown or no-op events do not schedule a projection write.
* Pending writes survive multiple changed events and persist the latest projection only.
* Replay-mode changed projection can fold in memory but leaves the storage key absent.
* Mock-enabled changed projection can fold in memory but leaves the storage key absent.
* Lifecycle flush does nothing when there is no pending projection write.
* Storage write failures do not throw from `applyEvent`, lifecycle flush, or explicit test flush.

***

## 10. Dependencies

### Other Sessions

* Depends on: `phase22-session05-store-folding-and-selectors`
* Depended by: `phase22-session07-honesty-regression-tests`, `phase22-session08-erasure-docs-and-validation`

***

## Next Steps

Run the `implement` workflow step to begin implementation.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://faction-os.gitbook.io/faction-os-docs/.spec_system/archive/sessions/phase22-session06-persistence-lifecycle/spec.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
