# Design notes — dsh-autoload-history ## Goal In DSH Web, opening a session pulls only the latest history page (`session.history` with `maxMessages: 50`, hardcoded in the client runtime). Older messages require clicking the "Load earlier" button once per page. This plugin removes that manual paging: the moment a session opens, the full history loads itself. The host RPC (`session.history`) already accepts an arbitrary `maxMessages`, but the client runtime fixes the page at 50 and exposes no setting for it, and the official UI offers no configuration for this behavior. A plugin therefore cannot change the page size — but it *can* drive the client's own `Session.loadOlder()` verb repeatedly until the window is complete. ## Why `conversation.composer.dock` The plugin is pure client-side and needs three things: 1. the current session id, 2. the conversation snapshot (`openState`, `hasMore`, `loadingOlder`), 3. a lifecycle that mounts/unmounts with the session. The official session-scoped slot `conversation.composer.dock` (a list slot, `replaceRisk: none`) provides all three through its standard props: `sessionId` and `useSession` (a `SnapshotSelectorHook` — it takes a selector and returns the selected value; the whole snapshot is selected with `(s) => s`). Registering a component that returns `null` gives a hidden, session-tracked hook point with no UI footprint. ## The cascade The component's effect runs on `[sessionId, snapshot]`: - `openState !== 'open'` → wait (session still opening, cold, or error); - `hasMore === false` → done (the whole history is in the window); - `loadingOlder === true` → wait (a page is already in flight); - otherwise resolve the session binding (`sessions.binding(sessionId)`) and call `session.loadOlder()`. `loadOlder` flips `loadingOlder` and marks the snapshot dirty, so the snapshot changes immediately (guard: loading) and again when the page lands (guard: open, next page). The cascade is therefore **snapshot-driven, strictly serialized**, and self-terminating: `hasMore` becomes `false` on the final page (the runtime also forces it false on an empty page, so a gap cannot loop). ## No-progress guard `loadOlder` swallows transport errors internally (logs and resets `loadingOlder`), so a persistently failing host would otherwise re-trigger the effect forever. The plugin tracks per-session progress by the in-window node count (`snapshot.nodes.length`): three consecutive effect runs that fired a `loadOlder` without growing the window stop that session's auto-loading. The user can still click the shipped "Load earlier" button; the plugin just stops retrying. A thrown/rejected `loadOlder` is additionally contained at the call site so it can never propagate into the React render tree. ## Settings (opt-in, localStorage) Both settings are read through the pure core (`settingsFrom`) on every effect run; invalid or unreadable values always fall back to enabled + unlimited: - `dsh-autoload-history.disabled = '1'` — stop auto-loading entirely; - `dsh-autoload-history.maxPages = ''` — cap how many pages one session open may auto-load (default unlimited). The per-open counter resets when the session leaves the loadable state (closed / fully loaded). ## Lifecycle & safety - The slot injection and registration are owned by the plugin fiber — unload (stop/update/remove) disposes everything. - Switching sessions remounts the slot entry for the new session id; each session gets its own guard record. - No host half behavior, no network requests, no global DOM mutation; only opt-in reads from `localStorage`. - Renders nothing; the only observable effect is history being loaded. - The load decision lives in `src/core.mjs`, unit-tested via `npm test`.