# Introduction > URL: https://previously.ldwid.com/en/docs/introduction # Introduction Previously is a lightweight cloud agent whose defining feature is episodic memory: it reads, writes, reasons, and acts, and is designed to never forget across time. ## What Previously Is Previously is not another assistant you chat with in disposable threads. It is a persistent cloud agent organized around a single idea: **one continuous relationship on one timeline.** You are the commander; Previously is your staff. You show up, you talk, you leave. When you come back — hours, days, or weeks later — the first thing you see is a timeline of your past, not a list of chat threads. The agent figures out relevance by scanning *when* something happened, then retrieving *what* was said. There are no conversations. There is no conversation management. There is one timeline. ## The "Previously on..." Metaphor The name comes from television: before a new episode, a brief recap reminds you what happened last time — just enough context to resume. Previously does the same thing. Every time you return, it recaps your own timeline, pulling forward what matters so you never start cold. On screen, that recap is your **arrival briefing**: what happened recently, what is in focus now, and which threads are still open. **Key takeaway: Previously replaces the chat-thread model with time slices, so memory and interaction are unified. Fix the memory model and you remove the need to manage conversations at all.** ## How It Differs from Chat-Thread Agents Mainstream agents split your life into chat threads. Each thread starts cold. Memory across threads is siloed, fragile, and lossy — bolting on vector databases, RAG pipelines, and prompt hacking to approximate what should be a native capability. Previously replaces threads with **time slices**. Because interaction history and memory share the same structure — a chronological sequence of slices — cross-session recall is a property of the system, not a retrofit. The agent never abruptly forgets the start of a long exchange because context is assembled dynamically per request rather than accumulated in a growing prompt window. Per-request load stays bounded and relevant. ## Vocabulary Tour Three terms carry the architecture. **Slice** — One conversation burst. A directory on the timeline at `memory/episodic/slices/YYYY/MM/DD/HHMM/`. Opens when you start talking, stays active while you stay engaged, closes automatically after about 15 minutes of silence by default (or a configurable turn cap, 20 by default). Carries structured YAML frontmatter: focus, summary, decisions, open loops, tags, emotional tone. Slice = *what happened, and when.* **Strand** — A keyword (like `health`, `work`, `housing`) that recurs across slices. The index `memory/episodic/strands.json` maps each keyword to every slice path that carries it. Strands are built at slice-close from the slice's tags, and a worker-model pass then consolidates the index — merging near-duplicate keywords and pruning stale entries. A slice = what happened; a strand = what it was about. Strands form the thin, lossless semantic layer over the timeline. **Recall** — The two-tier retrieval engine. A **worker** model (a cheap tier derived from the main model's provider, thinking off) runs a small, bounded recall agent: it starts from the global timeline catalog, traces matching strands, and returns up to five pointers with relevance scores plus recommended reads. The **main model** (the one you pick in the toolbar, thinking enabled) receives those pointers and reads the actual slices through granular tools — `readSliceSummary` for a relevance check, `readTimelineWindow` to orient by date, `readSlice` with turn ranges or grep for specifics — with full-slice reads kept as the explicit last resort. The worker is expected to be imperfect; the main model does the deeper work when the scan returns nothing. | Term | Layer | Role | Speed | |------|-------|------|-------| | Slice | Episodic (when) | Records what happened | Storage, not retrieval | | Strand | Semantic (what) | Keywords across slices | Built at slice-close | | Worker recall | Retrieval tier 1 | Scans catalog + strands, returns pointers | One fast worker agent run | | Main recall | Retrieval tier 2 | Granular deep read + reasoning | Seconds (per tool call) | ## Research Grounding The episodic / semantic split is not arbitrary. It is grounded in Endel Tulving's episodic vs. semantic memory theory (1972) and context-dependent memory research (Godden & Baddeley 1975; Smith & Vela 2001). Slices encode the *when* (episodic). Strands and memory nodes encode the *what* (semantic). Recall follows the same order: scan when first, then retrieve what. ## Status Previously is **EXPERIMENTAL** — early development, not yet ready for personal or production use. It is a one-person research project, but intended to be maintained long-term. What works today: streaming chat with visible thinking, recall, and tool calls; time-slice storage with automatic time-silence slicing; a first-class timeline catalog (`memory/episodic/timeline/index.json` plus a rendered Markdown projection) rebuilt deterministically from slice frontmatter and injected into the agent's prompt as a brief; a two-tier recall pipeline (worker scan + main-model deep read) with turn-granular read tools; a compact user card — Identity, Past, Now, Horizon, Self-model — that evolves once per closed slice, plus explicit user-confirmed memory updates; server-side local-time annotation; a semantic gate that keeps trivial turns out of memory; GitHub file tools with a path whitelist; multi-provider model support (DeepSeek, Anthropic, OpenAI-compatible); durable background runs via Vercel Workflow; and English-plus-Chinese internationalization. What is still missing: per-strand rolling summaries with deeper recall integration, and the timeline visualization UI the catalog schema is designed for. Both are future milestones. ## Related - [The Timeline](/docs/timeline) - [Memory Model](/docs/memory-model) - [Architecture Overview](/docs/architecture) --- # Why Previously > URL: https://previously.ldwid.com/en/docs/why # Why Previously Previously rethinks AI interaction from the ground up: fix the memory model, and the interaction model fixes itself. ## Two Problems, One Root Current AI assistants share a pair of linked failures that most people experience as one frustration. **Problem 1: memory is siloed per conversation.** Every new chat starts from zero. The agent cannot see what you discussed yesterday, last week, or in a different thread about an adjacent topic. Cross-conversation recall requires stitching together vector databases, RAG pipelines, and prompt engineering — and even then the result feels like talking to someone with amnesia. **Problem 2: the conversation list is the wrong cognitive container.** Humans do not organize memories into "Chat #47 with Mom." You remember by *when* something happened and *what* it was about. The flat list of chat threads is a UI artifact inherited from messaging apps, not a model of how people actually think and recall. These are two faces of the same mistake: building the product around the conversation UI instead of around memory. ## Memory Model Fixes Interaction Model If an agent genuinely remembers you — across time, across topics, across gaps of days or weeks — then conversation management becomes unnecessary. You do not need to choose or create the right thread. You do not need to remind the agent who you are and what you were doing. You just show up and talk. Previously replaces the chat-thread list with a **timeline**: a vertical, top-to-bottom view of time slices. Each slice is one uninterrupted conversation burst — opened when you start talking, closed automatically after about 15 minutes of silence by default. The path tells you exactly when it happened: ``` memory/episodic/slices/YYYY/MM/DD/HHMM/ ├── previously.md ← the user card as of that moment └── timeline/ ├── core.md ← the conversation itself └── agent.md ← what the agent was thinking ``` A calendar day is a directory that can hold multiple slices. There are no conversations to name, organize, or search through — just one continuous relationship, scrolled up to revisit the past or down to continue. The timeline is also a first-class, derived index: `memory/episodic/timeline/` is rebuilt deterministically from the slice files, grouped by day and era, so the agent can orient by time before it reads anything. A line on the timeline is a pointer, never the answer itself — the slice files remain the single source of truth. Context stays bounded per request because the prompt is assembled from relevant slices on the timeline, not from an ever-growing conversation window. The agent does not forget the start of a long history because the start was never in the window — only the pieces that matter, assembled fresh each time. > The conversation is a UI artifact. The timeline is a cognitive model. *The name "Previously" comes from television: "Previously on..." — a brief recap of last time, just enough context to pick up where you left off.* ## Design Principles ### A full agent, not just a memory tool Previously reads, writes, reasons, and acts. Memory is what makes interaction feel continuous, but it is not the only capability. The agent runs tools against your GitHub repository, uses a two-tier model architecture (a cheap worker model for housekeeping analysis and the recall sub-agent, the main model you pick in the toolbar for deep reasoning and response generation), and operates within a whitelist security boundary that restricts agent writes to `memory/`, `tasks/`, and `sessions/` — the `src/` directory is read-only. ### Memory is the hard problem Storing conversations is trivial. Retrieving the *right* memory at the *right* moment with the *right* depth is the genuinely hard part — and that is where the effort goes. The architecture reflects this priority: a single worker-model call per turn handles tagging, the semantic gate, slice-close marking, and user-card evolution; recall itself runs as a dedicated sub-agent that explores the timeline and returns pointers, never content. The complexity budget goes to the core store-index-recall loop, not to configuration knobs or edge cases. ### Your memory belongs to you Memory is plain Markdown with YAML frontmatter, stored in your own GitHub repository. Every file is readable by any tool, portable to any system, and version-controlled through git. There is no cloud database, no vector store, no proprietary format. The storage backend switches between the local filesystem (development) and the GitHub API (production), but the format is identical either way. The whitelist security layer enforces this ownership. Agent tools can write only to `memory/`, `tasks/`, and `sessions/`. Code — the agent's runtime and capabilities — stays read-only in `src/`. ### Simplicity over sophistication One number decides when a conversation burst becomes a closed slice: about 15 minutes of silence by default. Context loss (a refresh, a device switch) closes a slice too, and a turn cap sits in the background as a safety net — but the richer continuity heuristics of earlier iterations were removed, not tuned. One worker-model call per turn handles the analysis — tagging, the semantic gate, close marking, card evolution — in a single round-trip, with a fallback to safe defaults. Simplicity is a deliberate choice: every configurable knob, every edge-case handler, every stored flag is a tax on future reasoning. Previously pays that tax only where it earns out — in the core loop. ### Human memory is the right metaphor The architecture maps directly to cognitive science. Endel Tulving's 1972 distinction between **episodic memory** (events tied to a time and place) and **semantic memory** (abstract knowledge, facts, concepts) is the blueprint: - **Slices** are episodic — "what happened" organized by when. - **Strands** are semantic — "what it was about." A strand is a keyword woven across every slice that carries it; `memory/episodic/strands.json` maps each strand to its slice paths, and the recall engine follows strands at query time to trace a topic across months. Recall follows this layered pattern. The **worker model** (the fast, lightweight, intentionally fallible tier) drives the recall sub-agent: it scans the timeline catalog and strands and returns pointers with relevance scores — a quick conditioned reflex. The **main model** receives those pointers and decides what to actually read, drilling from a slice summary to a date window to specific turns via `readSlice` — deliberate, deep, and resourceful. Context-dependent memory research (Godden & Baddeley, 1975; Smith & Vela, 2001) shows that recall improves when retrieval context matches encoding context. The timeline preserves temporal context, and recall uses it. *Two-tier recall is shipped for the live demo at `previously-demo.ldwid.com` (read-only, memory writes disabled, resets on refresh).* ## Status Previously is **experimental**. It is a one-person research project, not yet ready for personal or production use. Everything described here reflects design intent and working code as of v0.8 — features marked as roadmap are actively evolving, not finalized guarantees. ## Related - [The Timeline](/docs/timeline) — the first-class index of your past - [Memory Model](/docs/memory-model) — slices, strands, and the whitelist - [Recall](/docs/recall) — the two-tier recall engine --- # The Timeline > URL: https://previously.ldwid.com/en/docs/timeline # The Timeline The timeline is the center of Previously, and it exists twice: as the **interface** you scroll — a rolling wheel of dated slices beside the conversation — and as the **memory index** the agent navigates — a catalog rebuilt from the slice files that gives the agent a map of your past. Both are projections of the same source of truth: the slices themselves. ## Not a Chat List, Not a Search Bar The timeline is the explicit alternative to two dominant UI patterns that most AI chat products adopt. It is **not** a list of conversation threads that you must manually manage, rename, delete, and search through. It is **not** a search bar where you type keywords hoping the right memory surfaces. Both of these patterns impose overhead on the human — they assume you will curate your own memory. Previously rejects that assumption. Instead, it presents a single timeline — your story as an autobiography, oldest at the top, newest at the bottom, with the live conversation at the now end. **Key takeaway: you do not manage conversations. You scroll through your life.** ## The Interface: A Wheel Beside the Conversation The chat page is a split view. On the left, the **timeline wheel** (`TimelineWheel`); on the right, the conversation — live or historical. A fixed input bar sits at the bottom of the screen. The wheel is a vertical, virtualized list of **every slice you have**, loaded from the timeline catalog in one shot — oldest at the top, newest at the bottom, ending in a **now** row that returns you to the live chat. Key behaviors: - **Row = slice** — each row shows the slice's date, time, and focus line. The loaded slice's row is brand-colored; hovering lifts the others. - **Rolling digits** — timestamps count up or down odometer-style as you scroll, so moving through the past feels like winding a clock. - **Scroll or drag** — scroll down to reveal earlier slices, up to move toward now. The wheel expands in place (widening over the content with a blur mask) when you want the full catalog, and collapses when you pick a slice. - **Two gears** — on desktop the wheel shows full rows; on phones it narrows to a lock-screen-style clock spine (small date over large hour and minute). Selecting a past slice plays a **time-travel transition**: a rolling-digit clock readout spins from where you are to the target slice's moment, under a letter-spaced "PREVIOUSLY ON" eyebrow, before the historical view fades in. ## The Arrival Briefing When the live chat is empty — a fresh arrival, or a return after the last slice closed — the right panel shows the **arrival briefing** instead of a blank canvas: a film-title-card "PREVIOUSLY ON" over your name, then a hot-start summary drawn from real memory: - **Last topic** — the most recent slice's focus line. - **Open loops** — up to four unresolved threads carried forward. - **Suggestion chips** — one-tap prompts seeded from the last topic, the open loops, and a recent slice. Each chip sends a real message. - **View full previously** — opens the active slice's `previously.md` (the agent's compact user card snapshot) in a dialog. Every section renders only when it has real data — nothing says "last time we talked about" followed by nothing. In demo mode your name becomes the persona switcher. ## The Historical Slice View Selecting a past slice swaps the right panel into a historical view while the wheel stays mounted: - **Previously On bar** — a Brain icon + label; clicking opens a dialog with the slice's `previously.md` snapshot, with the slice's summary line beneath. - **History turns** — the slice's user/assistant exchanges with timestamps; each agent turn's thinking is available through a Thoughts popover (the cognition log). - **Open loops / decisions footer** — the slice's open loops (↗) and decisions (✓) as labeled pills. ## The Index: A Projection, Never a Second Truth Since v0.8 the timeline is also a first-class, maintained **index** over the slice archive. The design rule: **Slices are the single source of truth; the timeline is a derived projection.** Every catalog entry is rebuildable from the slice files, so a slice can never become unreachable because "the timeline lost it". The index lives in two renderings of one structure, both under `memory/episodic/`: - **`timeline/index.json`** — the canonical structured catalog, consumed by the UI (the wheel renders from it) and by tools. - **`timeline.md`** — the Markdown projection, era- and day-grouped, newest first. This is what the recall agent reads as its map. Each catalog entry is a compact **pointer with texture** — id, date, start/end, turn count, status, `focus`, `summary`, `tags`, emotional `tone`, `open_loops`, `decisions`, the `strands` it carries, and a `needs_marking` flag. One line is enough to judge relevance, never enough to answer from: ``` - **2026-08-11-1115** 回顾滴滴时期绩效背锅/晋升被吃,与当下对比 · 14轮 · mixed [状态回忆,创伤克服,人生思考] ``` ## `weaveTimeline`: Engineering Reconciliation The index is maintained by `weaveTimeline` (`src/lib/episodic/timeline/weave.ts`) — a deterministic pass that runs at the start of every turn's housekeeping, with **zero model calls**: 1. **Enumerate** the actual slice directories — the truth. On GitHub this is a single recursive Git Trees API call; locally, a filesystem walk. 2. **Load** the current projection (migrating legacy monthly `_index.json` files on first run). 3. **Diff** — a slice on disk but missing from the catalog is added from its frontmatter; a catalog entry with no slice on disk (a phantom) is dropped. **The slice always wins.** 4. **Rebuild** — resolve strand membership against `strands.json`, sort chronologically, write both views. Inside a turn, the writes join the turn's single git commit. A full reconcile is throttled (skipped while the catalog is less than 5 minutes old), so a per-turn weave is usually one cached read; the slice-close path forces a reconcile so a just-closed slice appears immediately. Because reconciliation is structural, orphaned slices — created but never indexed, closed across a day boundary — are picked up automatically. ## `needs_marking`: The Dry-Slice Queue A slice that closed without a `focus`/`summary` is a **dry** slice — anonymous to recall. The weave flags these as `needs_marking` instead of hiding them, and two mechanisms keep the count falling: - **Deterministic fallback at close** — when the analyzer's close-marking comes back incomplete, the slice is closed with a deterministic mark (first-turn topic + tags) rather than empty fields. - **Backfill pass** — at slice-close boundaries (never mid-turn), a worker-model batch (`backfill-marks.ts`) writes focus/summary for a few dry slices and clears their flags. Best-effort by contract: a backfill failure never takes a turn down. ## What the Agent Sees Every turn, housekeeping builds a **timeline brief** from the catalog and appends it to the system prompt's variable tail: ``` ## Timeline (recent) - **2026-08-11-1115** (08-11 Tue · 6 days ago) 回顾滴滴时期… · 14轮 · mixed [状态回忆,…] - **2026-08-10-1839** … - 往前共 152 片,可用 readTimelineWindow / readSliceSummary 回溯 ``` Roughly the ten newest pointer lines, each annotated on the user's local calendar (weekday + relative days, so the agent never does date math), plus catalog totals and the dry-slice count. The prompt is explicit: *timeline lines are pointers — if a line looks relevant, read the slice before answering from it.* Reads then follow a cost ladder — the smallest sufficient unit first: | Tool | Returns | Tier | |------|---------|------| | `recall(query)` | Worker-model scan: hits + recommended reads | 1 — primary past access | | `readTimelineWindow(from?, to?)` | Pointer lines over a date window (default 20, max 50) | 1 — orient by time | | `readSliceSummary(sliceId)` | Frontmatter only: focus, summary, tags, tone, loops, decisions | 1 — relevance check | | `readStrand(strand)` | A strand's slice paths | 1 — trace a topic | | `readSlice(sliceId, range)` | Specific turns / last N / after a date / keyword matches / line range | 2 — need actual text | | `readPreviously(sliceId)` | The slice's user-card snapshot | 2 | | `readSlice(sliceId)` full, `readAgentTimeline` | Whole slice / cognition log | 3 — explicit last resort | The recall agent works the same map: it reads the global timeline paginated newest-first, uses `readTimelineWindow` for older ranges, and stays on summaries — deep reads are the main model's job. See [Recall](/docs/recall). ## Three Timelines One naming collision worth knowing: each slice *directory* also contains a `timeline/` subfolder — that is the per-slice pair the landing page calls **three timelines**: - **`core.md`** — the shared record: what you and the agent actually said. - **`agent.md`** — its thinking: the cognition log behind each turn. - **Your life** — the third line only you know; the part that never gets written down. The global timeline described on this page is the catalog *over* all slices' shared records — the index that makes a hundred slices navigable as one line. ## Where Slices Live Each slice is a directory under `memory/episodic/slices/YYYY/MM/DD/HHMM/` (UTC), containing `previously.md` and `timeline/core.md` + `timeline/agent.md`. A slice opens when you start talking and closes after about 15 minutes of silence by default (configurable) — purely time-driven, no capacity limit, no topic-shift rule. Details: [Slices](/docs/slices). ## Data Flow (UI) The interface is populated through server actions: 1. **Initial load** — `getEpisodicState()` returns the most recent slice as `active`, an array of `recent` slices, and a `hasMore` flag; this seeds the briefing and the send window. 2. **The wheel** — `getTimelineCatalog()` returns the full catalog from `index.json` (oldest → newest, virtualized client-side); there is no page-by-page fetching. 3. **Slice content** — `getSliceContent(slice_id)` lazily loads a slice's turns, summary, previously snapshot, loops, and decisions on selection. ## Related - [Slices](/docs/slices) — the storage unit the timeline indexes: lifecycle, frontmatter, layout - [Strands](/docs/strands) — the topic axis across slices; the timeline is the time axis - [Recall](/docs/recall) — how worker and main models navigate the timeline - [Memory Model](/docs/memory-model) — how slices fit into the full episodic + semantic architecture --- # Slices > URL: https://previously.ldwid.com/en/docs/slices # Slices Every conversation is a timeline of slices — one burst of back-and-forth, one directory of plain Markdown. ## What a slice is A **slice** is the storage unit of episodic memory: a single conversation session captured as a small directory of Markdown files, the conversation itself held in a `core.md` with YAML frontmatter. It is the L2 memory layer — L0 and L1 are baked into the build, L2 (slices) is fetched on demand at runtime. No database, no binary format. Plain Markdown in your repo. Each slice holds a contiguous burst of turns — the messages you and Previously exchanged in one sitting — alongside metadata maintained by the **worker model** (the cheap housekeeping call that runs once per turn and marks the slice when it closes). ## Lifecycle A slice has two states: **active** and **closed**. | Phase | What happens | |-------|-------------| | **Open** | The chat route calls `createSlice()` when you send a message and no slice is active. It derives the `slice_id` from the current UTC time and seeds the first user turn. | | **Extend** | Each subsequent message appends a turn. The slice is recovered from storage at the start of every turn (serverless invocations share no memory), the new turn is appended, and the updated `core.md` is written back as a durable snapshot — all of a turn's writes land in a single batched commit. | | **Close** | When a slicing signal fires, the worker model first writes the close-marking (`focus`, `summary`, refined `tags`, `emotional_tone`), then `closeSlice()` sets `status: "closed"`, stamps `end` and `closed_by`, writes `core.md`, updates the monthly index (`_index.json`), weaves the slice's tags into the strand index (`strands.json`), and reconciles the timeline catalog. The next message opens a fresh slice. | Three signals can close a slice, and all of them are recorded in the `closed_by` frontmatter field: - **Time silence** — no activity for a configurable threshold (default 15 minutes, adjustable in Settings from 5 to 120). Closes are lazy: the silence is detected when your *next* message arrives, and `end` is stamped with the last turn's real timestamp, never the detection time. - **Turn cap** — a safety net that force-closes a marathon session (default 20 turns, adjustable from 5 to 100). - **Context loss** — the client history came back without any agent replies while the stored slice has them (a page refresh or device switch), so continuing it is impossible. > Both thresholds are read from your user config at request time, so changing them in Settings takes effect immediately — no redeploy. On page refresh, `tryLoadTodaySlice()` scans today's directory — and yesterday's, so a conversation crossing the UTC day boundary is not orphaned — finds the most recent slice still marked `active`, and re-hydrates it. You pick up where you left off. ## Directory layout A slice is no longer a single file: it is a **directory** holding the conversation, the agent's cognition log, and a user-card snapshot. Slices live under `memory/episodic/slices/` in a calendar hierarchy: ``` memory/episodic/ current-previously.md -- the live user card slices/ 2026/ 07/ 28/ 0658/ -- one slice directory timeline/ core.md -- the conversation: YAML frontmatter + turns agent.md -- agent cognition log (mechanical extraction) previously.md -- user-card snapshot carried by this slice 1024/ -- another slice, same day _index.json -- monthly index for July 2026 08/ ... strands.json -- global keyword-to-slice index timeline/ index.json -- canonical structured catalog of all slices timeline.md -- Markdown projection of the catalog ``` A calendar day is a directory; a slice is a directory inside it named `HHMM`. The three files have distinct roles: - **`timeline/core.md`** — the shared conversation record and the single source of truth: YAML frontmatter plus every turn. - **`timeline/agent.md`** — the agent's internal cognition for each turn (thinking traces, tool calls), extracted mechanically. Used for self-model evidence, never injected as conversation context. - **`previously.md`** — a snapshot of the user card as this slice knew it, seeded from the previous slice and overwritten by the card-evolution pass when the slice closes. Older flat `HHMM.md` files (pre-directory format) are still readable — the loader checks for slice directories first, then falls back to legacy files. > The `HHMM` in the path and in `slice_id` is **UTC** — derived from the first user message's UTC date+time. Your local wall-clock time is stored separately in the `timezone` frontmatter field. This keeps paths stable across time zones and DST shifts. ## YAML frontmatter Every `core.md` opens with `---` delimited YAML. Tags are refreshed every turn; `focus`, `summary`, and `emotional_tone` are written by the worker model at close (with a deterministic fallback so a slice never closes empty); the rest are mechanical: | Field | Type | Required | Description | |-------|------|----------|-------------| | `slice_id` | string | yes | `YYYY-MM-DD-HHMM`, UTC datetime of first message | | `focus` | string | yes | Core topic, one sentence (written at close; stripped while empty) | | `status` | `"active"` or `"closed"` | yes | Lifecycle phase | | `start` | string | yes | UTC ISO 8601 timestamp of first turn | | `end` | string | no | UTC ISO 8601 timestamp of last turn (absent while active) | | `timezone` | string | yes | User's IANA timezone at time of interaction, e.g. `"Asia/Shanghai"` | | `summary` | string | yes | Worker-generated recap, 100 characters at most (written at close) | | `open_loops` | string[] | yes | Unresolved questions carried forward — retained in the schema, currently serialized empty | | `decisions` | string[] | yes | Conclusions or action items — retained in the schema, currently serialized empty | | `tags` | string[] | yes | Semantic keywords (these weave into the strand index) | | `related_slices` | string[] | yes | Relative paths of related slices — may be empty | | `loops` | string[] | yes | IDs of durable background loops spawned from this slice — may be empty | | `emotional_tone` | string | no | `"positive"`, `"neutral"`, `"negative"`, or `"mixed"` — assessed by the worker model at close | | `closed_by` | string | no | The signal that closed the slice: `"time_silence"`, `"user_explicit"`, `"capacity"`, or `"context_lost"`. Legacy closed slices lack it and read back as `"user_explicit"` | Empty strings and `undefined` fields are stripped from the YAML (which is why an active slice has no `focus`/`summary`). Empty arrays (`open_loops: []`) are serialized as-is. ## Turn body After the frontmatter, each message becomes a level-2 heading: ``` ## Turn mVhV2g — 2026-07-28T06:58:22.811Z (user) Your message text here... ## Turn mVhV2g — 2026-07-28T06:58:43.641Z (agent) Previously's response... ## Turn J4oA9g — 2026-07-28T07:09:52.409Z (user) Your follow-up... ``` The format is always `## Turn ()` with an em dash separator. The ID is a 6-character base64url `turnId` shared by the user and agent turns of the same round, so every exchange is addressable (tools read individual turns by it). Legacy slices use simple numeric IDs (`1`, `2`, …); the parser accepts both. `parseTurns()` reconstructs the array from these headers. ## A real slice file Here is what a closed slice's `core.md` looks like on disk — at `memory/episodic/slices/2023/04/21/0610/timeline/core.md`: ```markdown --- slice_id: 2023-04-21-0610 status: closed start: "2023-04-21T06:10:00.000Z" end: "2023-04-21T07:18:00.000Z" timezone: America/Chicago open_loops: [] decisions: [] related_slices: [] loops: [] focus: Housing project delays and trust crisis management summary: Contractor qualification and property deed mismatches stalled multiple household files, triggering a community trust crisis... tags: - work-pressure - housing-project - trust-crisis - community-communication emotional_tone: mixed closed_by: time_silence --- ## Turn k3Fx92 — 2023-04-21T06:10:00.000Z (user) The housing rehab files that were supposed to close this month are stalled again. At the public meeting last night residents were angry... ## Turn k3Fx92 — 2023-04-21T06:13:00.000Z (agent) That sounds rough. What specifically broke down — contractor qualification, property deeds, or something else? ``` Every part of this file is human-readable, git-diffable, and parseable by any tool that handles Markdown and YAML. Gray-matter does the parsing on the TypeScript side. ## Strands: the semantic index over slices Each slice carries `tags`. When a slice closes, `updateStrands()` weaves every tag into `memory/episodic/strands.json` — a single file that maps keywords to slice paths: ```json { "work-pressure": [ "2023/04/21/0610", "2023/04/22/0915" ], "housing-project": [ "2023/04/21/0610" ], "trust-crisis": [ "2023/04/21/0610" ] } ``` A **strand** is one keyword's entry — "the whole history of that thing" across time. It is the thin, lossless semantic-memory layer over episodic slices. Entries are deduplicated, so the same slice never appears twice under the same tag. The index is also consolidated at close: single-use stale strands are pruned deterministically, and — once the index grows large enough — the worker model suggests merges for semantic duplicates (typos, the same concept under two names). Strands are traced automatically during recall; rich per-strand summaries are a future milestone. ## How slicing works with the worker and main models The **worker/main split** governs metadata and recall: - **Turn analyze** (the worker model, thinking disabled) runs once per turn in housekeeping. It extracts message tags (preferring existing ones, so the same concept merges across languages), a semantic hint toward existing strands, and the user's intent — and, when a slice is closing, its `focus` / `summary` / refined `tags` / `emotional_tone`. If the call fails, a deterministic fallback mark is built from the slice itself, so a closed slice always carries real descriptions. Older slices that predate this discipline sit in a `needs_marking` queue and are backfilled a few at a time on later close boundaries. - **Main deep read** (the main model) retrieves through granular tools: `readSliceSummary` for a relevance check, `readTimelineWindow` for time orientation, `readStrand` to trace a topic, and `readSlice` with turn/head/tail/range/grep modes for targeted reads — a full-slice read is the explicit last resort. When a slice closes, the worker freezes the metadata and the slice is sealed on disk. ## Storage backend All slice I/O goes through three wrappers (`fsReadFile`/`fsWriteFile`/`fsListFiles`), which delegate to the backend selected by the `STORAGE` environment variable: | Mode | Backend | Selection | |------|---------|-----------| | `local` | Local filesystem | Development, full read/write | | `github` | GitHub REST API (Octokit) | Production, full read/write | | `demo` | Remote benchmark data | Read-only persona data | When `STORAGE` is unset, the source auto-detects: `GITHUB_TOKEN` present → `github`, `NODE_ENV=development` → `local`, otherwise `demo`. The same code path works in every mode — no manual switching. Writes within one turn are queued into an explicit `WriteBatch` object and flushed as a **single git commit** (GitHub mode) or a single disk pass (local). Because the batch is a per-turn object rather than a module global, two concurrent turns can never flush each other's writes; a failed flush keeps the queue for retry. ## Indexes maintained at close When a slice closes, three auxiliary structures update: 1. **Monthly index** (`memory/episodic/slices/YYYY/MM/_index.json`) — a JSON array of slim entries (`SliceIndexEntry`: id, focus, summary, tags, status, start, open_loops, decisions). Enables the timeline UI to browse titles without reading every file. 2. **Strand index** (`strands.json`) — the keyword-to-slice mapping described above. 3. **Timeline catalog** (`memory/episodic/timeline/index.json` + the `timeline.md` projection) — a derived, rebuildable index over *all* slices, reconciled by `weaveTimeline`: slices on disk are the truth, the catalog is a projection. Missing slices are added from their frontmatter, phantom entries are dropped, and slices with empty `focus`/`summary` are flagged `needs_marking` for backfill. A slice created mid-turn is upserted into the catalog in the same commit. ## Design rationale - **Time-driven slicing with safety nets.** The primary trigger is silence; the turn cap exists so a marathon session cannot grow unbounded, and context-loss detection keeps refreshes and device switches from corrupting a slice. Thresholds live in user config, not code. - **Durable per-turn snapshots in one batched commit.** Serverless invocations share no memory, so the slice is recovered from storage every turn and written back the same turn — your message is safely on disk before anything streams back. Batching keeps GitHub API writes to one commit per turn. - **YAML frontmatter + Markdown body** is the raw material of every documentation ecosystem. It renders in any Markdown viewer, diffs cleanly in git, and parsers exist in every language. Gray-matter handles the TypeScript side. - **Episodic vs. semantic separation** mirrors Endel Tulving's 1972 theory. Slices are episodic (organized by *when*). Strands and the user card are semantic (organized by *what*). Recall scans *when* first, then retrieves *what*. ## Related - [Timeline](/docs/timeline) — the horizontal date-dot strip that browses slices by date - [Strands](/docs/strands) — semantic tag index over the slice archive - [Recall](/docs/recall) — how the worker scans and the main model retrieves - [Memory Model](/docs/memory-model) — the three-tier architecture --- # Strands > URL: https://previously.ldwid.com/en/docs/strands # Strands A strand is a keyword woven across every time slice that carries it — the semantic layer over the episodic timeline. Where a slice says *what happened* (by time), a strand says *what it was about* (by topic). ## The Problem: Episodic Memory Has No Topic Index Time slices organize your history chronologically. You can ask "what happened yesterday afternoon" and get the right slice file. But you cannot easily ask "everything I've ever said about Rust" — that question cuts *across* time, not along it. You need both axes: | Memory type | Organized by | Unit | Question it answers | |---|---|---|---| | **Episodic** | Time | Slice | "What happened when?" | | **Semantic** | Topic | Strand | "What was said about X?" | Strands fill the semantic axis. They are the by-topic counterpart to the by-time slice record. ## How It Works A slice carries `tags` in its YAML frontmatter — keywords like `"rust"`, `"async"`, `"deployment"`. Every time a slice closes, those tags are woven into a single global file: > `memory/episodic/strands.json` Each key in that file is a **strand** (one tag). Each value is the list of relative slice paths tagged with that keyword — "the whole history of that thing" across time. ```json { "rust": [ "2026/06/22/1400", "2026/07/01/0915", "2026/07/08/1630" ], "async": [ "2026/06/22/1400" ], "deployment": [ "2026/07/08/1630" ] } ``` The paths are relative: no `slices/` prefix, no `.md` extension. `sliceIdToRelPath` converts a slice id like `2026-06-30-1430` to `2026/06/30/1430` so you can walk directly to `memory/episodic/slices/2026/06/30/1430.md`. ## Derived, Not Authored Strands have no separate authoring step. They are **derived entirely** from the `tags` array in each slice's YAML frontmatter. Tags are maintained per-round by the worker model as part of its housekeeping update — it reads the conversation, classifies the topic, and writes the tags. Those tags become strands automatically. The cycle is: 1. **The worker runs metadata maintenance** → writes `tags` into the active slice's frontmatter 2. **The slice is persisted** (either closed after about 15 min of silence by default, or snapshotted mid-conversation) 3. **`updateStrands(slice)` runs** → reads `strands.json`, weaves each tag in as a key, appends the slice's relative path (deduplicated), writes back The weave is **merge-first**: before a tag creates a new key, it is normalized (trimmed, ASCII-lowercased, full-width forms folded to half-width, inner whitespace collapsed) and matched against existing keys — so `Apex` and `apex` land on the same strand instead of splitting one topic into two. Only a genuinely new tag creates a new key. Tags weave into strands. You never write a strand directly. ## When Strands Are Written `updateStrands` runs in two places: - **On slice close** (`closeSlice` in `manager.ts`): after the slice `.md` file is written and the monthly `_index.json` is updated, `updateStrands` publishes the slice's tags into the global index. - **On active-slice snapshots** (`ensureIndexEntries` in `manager.ts`): when a snapshot is saved mid-conversation, `updateStrands` also runs — but only if the slice has at least one tag. This means an in-progress slice appears in the strand index before it closes. The result: `strands.json` stays current even for active slices, so a recall scan never misses a tag just because the slice hasn't closed yet. ## Keeping the Index Clean A naive append-only index rots: typos split one concept across two keys, and a tag used once a year ago stays forever. At every slice close, a consolidation pass (`consolidateStrands`) runs in two stages: 1. **Deterministic pruning (always).** A strand that never became a thread is pruned — specifically, one with fewer than 2 slices whose slices are all older than 14 days. A strand survives if it has enough slices *or* at least one recent slice, so a genuinely new topic is never pruned the day it appears. 2. **LLM merge pass (only when the index has 25+ strands).** Normalization catches mechanical duplicates like `Apex`/`apex`, but the worker model itself mints semantic duplicates — typos in names, the same concept under two names. The worker model reviews the index and proposes a from→to merge map (capped at 30 merges per pass, precision over recall: when in doubt, do not merge). The engineering layer then unions the path lists and removes the redundant keys. The whole pass is defensive: on any failure it returns the index unchanged, so slice close never breaks because consolidation did. ## How Strands Are Read The index is not write-only — it feeds the agent on every turn, through three paths: - **The memory-topics menu.** During turn housekeeping, the system prompt's variable tail gets a `## Memory topics` block: up to 20 strands, sorted by most recently active slice, each annotated with when it was last seen. When the user mentions one of these topics, the main agent is instructed to run `recall`. - **The recall pipeline.** The `recall` tool hands the query to a Flash sub-agent that searches memory from pointers only. That sub-agent receives the strand list up front, is told to check which strands match the query, and has its own `readStrand` tool to trace a matching strand to every slice path that carries it. Strands are the topic axis of recall; `readTimelineWindow` is the time axis. - **The timeline projection.** Every `weaveTimeline` pass reads `strands.json` once and resolves each timeline entry's `strands` field — the subset of the slice's tags that are woven strands. The structured timeline index therefore records which strands pass through which slices, ready for consumers like a UI that renders a strand as a parallel timeline. The main agent itself does not walk the raw index — `readStrand` and `listStrands` stay with the recall engine. What the main agent sees is the menu plus whatever `recall` returns. ## The Type The `StrandIndex` type (defined in `src/lib/episodic/types.ts`) is a plain string-keyed map: ```typescript interface StrandIndex { [strand: string]: string[]; } ``` If `strands.json` does not exist yet — no slices have ever had tags — `readStrands()` returns an empty object `{}`. Writing always produces pretty-printed JSON via `JSON.stringify(index, null, 2)`. ## Dual Storage The read/write path goes through `fsReadFile`/`fsWriteFile` wrappers that transparently target the local filesystem in development or the GitHub API in production. The strand index itself (`strands.json`) is a runtime data artifact — it lives in the same memory repo as the slices, not in the application source tree. Nothing in the working tree of this code repository contains a committed `strands.json`. ## What a Strand Is Not (Yet) A strand is still the thin, lossless index: the map from keyword to slice paths, nothing more. A richer **first-class strand** — one with its own rolling summary, its own metadata, and a visual rendering as a parallel timeline — is an explicit future milestone. The v0.8 timeline schema already carries per-slice strand membership so that view can be built without reworking the data. ## Related - [Slices](/docs/slices) — the episodic counterpart: what happened, by time - [Timeline](/docs/timeline) — the vertical view of slices across days and months - [Recall](/docs/recall) — how the worker and main model navigate memory - [Memory Model](/docs/memory-model) — the three-tier architecture --- # Memory Model > URL: https://previously.ldwid.com/en/docs/memory-model # Memory Model Previously has no database and no vector store. Memory is plain Markdown files in a git repository, organized by time. Three timelines run through the product, and only one of them is shared: - **Your life** — everything you lived. Most of it was only ever experienced by you; Previously does not see it. - **The shared record** — every conversation, every decision, every unspoken moment that got remembered. This is what Previously stores, as a timeline of episodic slices. - **Its thinking** — the agent's own cognition while it worked: what it recalled, what it reasoned, what it concluded. This is recorded too, alongside each slice. The memory model is how these lines are kept: an immutable identity bundled at build time, a lossless episodic timeline at runtime, and a compact user card that distills the timeline into who you are. ## L0: The Identity Constitution The deepest layer is not data at all — it is who the agent is. `identity/agent/SOUL.md` and `identity/agent/DIRECTIVES.md` hold the agent's constitution: its character and its standing rules. At build time, `scripts/generate-identity.mjs` compiles both files into a generated TypeScript module (`src/lib/identity/agent-prompt.generated.ts`), so the constitution is frozen into the deployment. It is never read from disk at runtime, and its source sits outside the tool whitelist — the agent cannot rewrite its own soul, and neither can a bad edit to the repo change the live prompt. ## The Episodic Layer: Slices on a Timeline Every conversation burst becomes a **slice** — a directory on your personal timeline whose path encodes exactly when it happened: ``` memory/episodic/slices/2025/11/21/0825/ timeline/ core.md -- the shared record: YAML frontmatter + conversation turns agent.md -- its thinking: the agent's cognition log for this slice previously.md -- a snapshot of the user card as of this slice ``` Note the two files under `timeline/`: `core.md` is the shared record (what was said, with structured metadata — `focus`, `summary`, `open_loops`, `decisions`, `tags`, `emotional_tone`, `status`, `start`/`end`), while `agent.md` is the agent's own line — its thinking at the time, mechanically extracted. A slice opens when you start talking and closes after about 15 minutes of silence by default (or context loss, or a turn-count cap). There is no topic-shift rule: slicing is purely time-driven, so the timeline stays a lossless autobiography. See [Slices](/docs/slices) for the full lifecycle. **Strands** are the thin semantic index over that timeline: a keyword like `work` or `family` woven through every slice that carries it, stored in `memory/episodic/strands.json` as a strand-to-slice-paths mapping. A slice is what happened; a strand is what it was about. See [Strands](/docs/strands). **Key takeaway: slices are the single source of truth.** Everything else — the strand index, the timeline catalog, the user card — is a derived, rebuildable projection over the slice files. ## The Timeline Index Reading a hundred slices to answer one question wastes context, so Previously maintains a first-class, derived index over the whole history (`src/lib/episodic/timeline/`): - `memory/episodic/timeline/index.json` — the canonical structured catalog: one entry per slice with its `focus`, `summary`, `tags`, `strands`, `open_loops`, `decisions`, and a `needs_marking` flag for slices whose semantics haven't been filled yet. - `memory/episodic/timeline.md` — a Markdown projection of the same catalog, grouped by month and day, readable by the agent (and by you). Both are rebuilt deterministically by `weaveTimeline` — enumerate the slice files, diff, reconcile — with zero model calls. A fresh slice is upserted into the catalog the turn it is created; a worker model later backfills `focus`/`summary` for any entry flagged `needs_marking`, off the turn's critical path. The index is a map, not an answer source: each line carries enough texture to judge relevance, but correctness always comes from opening the raw slice with `readSlice`. The agent is explicitly instructed that a timeline line is a pointer, never a source to cite from. ## The User Card `memory/episodic/current-previously.md` is the living distillation of the timeline — a compact card that answers *who you are*, refined after slice boundaries. It is the difference between an agent that stores your history and one that arrives already knowing you: it is what makes an arrival briefing possible. The card has five sections: | Section | Contents | |---------|----------| | **Identity** | Structured head: name, how to address you, aliases | | **Past** | A rolling third-person profile paragraph, updated in place, plus anchor facts — durable dates, decisions, and red lines that will still be true in years | | **Now** | Current-state hooks, not narratives — mechanically expired after 7 days | | **Horizon** | Future-facing commitments and deadlines, each with a `by:` date; overdue items are flagged, never silently dropped | | **Self-model** | The agent's operating lessons about working with you — deltas from its standing directives only | Every entry carries `refs` pointers back to the slices that evidence it, so any claim can be drilled down to the raw record. The card is never rewritten wholesale and never touched by the generic write tool: a dedicated worker-model agent edits it through validated per-entry mutations (`addNow`, `updatePastProfile`, `resolveHorizon`, …), and over-limit writes are rejected with compression instructions. Hard caps keep the whole card under ~8 KB, so it stays cheap to inject into every prompt. See [The User Card](/docs/user-card) for the full format and evolution loop. ## The Whitelist Boundary Agent tools are confined to three writable directories (`src/lib/whitelist/index.ts`): ``` memory/ tasks/ sessions/ ``` The `src/` directory is agent-**read-only** — no tool may modify it. Path validation normalizes inputs first — decoding URI components, converting backslashes to forward slashes, resolving `./` and `../`, stripping leading slashes — then rejects empty paths, absolute paths (`/`), and drive-letter paths (`A:`) before matching against the allowed prefixes. Within the whitelist, system-managed paths are readable but write-blocked for the generic write tool: - `memory/episodic/` — slices, indexes, and the user card are system-owned - Any `_index.json` file - Any `strands.json` file This keeps the episodic timeline and its structural indexes safe from accidental or malicious corruption; the card evolves only through its dedicated mutation tools. ## Context Assembly: Built Fresh Every Turn There is no ever-growing prompt window. Each turn, the workflow assembles the system prompt dynamically from the timeline (`assembleSystemPrompt` in `src/app/api/chat/turn-workflow.ts`): 1. **Identity constitution** — SOUL + DIRECTIVES, stable across turns. 2. **The user card** — changes only when it evolves. 3. **The per-turn brief** — timestamp, intent, continuity, semantic links. 4. **Timeline and strands blocks** — a pointer view of recent slices and the strand menu. 5. **Notices** — evolution results, demo mode. The ordering is a cache decision, not just layout: the stable blocks form a long byte-identical prefix the provider's prompt cache reuses across turns, while the variable tail changes freely without invalidating it. Deep context is never stuffed into the prompt by default — recall works the timeline in two tiers: a worker model scans summaries and returns pointers, and the main model deep-reads only the slices that matter via `readSlice`. See [Recall](/docs/recall). ## Why Plain Markdown + YAML Every piece of memory is a Markdown file with YAML frontmatter. This choice is deliberate: - **Portable** — `git clone` anywhere, read on any device, no server needed. - **Git-diffable** — every change to a slice, the card, or the strand index is tracked in version history. You can see what Previously learned, and when. - **Human-editable** — open any file in your editor, fix a summary, add a tag. No admin UI, no SDK. - **Any tool reads it** — Previously writes, Claude Code reads, Codex extends. No lock-in, no integration tax, no schema migration. Files are the interface. ## Related - [The Timeline](/docs/timeline) — the interaction surface this memory model powers - [Slices](/docs/slices) — the episodic layer in detail - [Strands](/docs/strands) — the semantic keyword index - [The User Card](/docs/user-card) — the living distillation of you - [Recall](/docs/recall) — how the agent works the timeline at inference time --- # User Card > URL: https://previously.ldwid.com/en/docs/user-card # User Card Slices record what happened. The **user card** records who you are. It is a single Markdown file — `memory/episodic/current-previously.md` — that distills the entire timeline into one compact brief: your identity, a rolling profile, what is happening right now, what you have committed to, and what Previously has learned about working with you. This is the "A memory that learns." idea made concrete: the card evolves as you talk — corrected when wrong, refined when things change. Not a dossier. Just what matters. The card is injected into every turn's system prompt, right after the identity constitution. Because it changes only on evolution — never mid-slice — the prompt prefix stays byte-stable and the provider's prompt cache keeps hitting (`src/app/api/chat/turn-workflow.ts`). The stored card keeps raw ISO dates; the injected copy is annotated with relative times ("3 days ago") computed on your local clock, so the model never does date arithmetic itself. ## The v5 Format The card has five sections, making your time axis explicit — Past, Now, and Horizon: ```markdown # Previously On _Active slice: 2026-08-17-0515 | Format: user card v2 | Updated: 2026-08-17T05:27:48Z_ ## Identity - Name: Alex - Address them as: you - Alias: 小艾 ## Past Alex is an engineer who prefers answers grounded in real evidence over abstraction. They left a draining job in 2024 and now work somewhere they chose deliberately. What they want most is to spend their time on things they themselves chose — a north star they still hold today. - Graduated 2021; changed jobs in May 2024 — refs: [2026/07/26/1539] - Red line: no databases for single-user tools — refs: [2026/08/02/1105] ## Now - Training for a 5K — Wednesday morning runs are a two-person thing now — refs: [2026/08/11/1426] | since: 2026-08-11 ## Horizon - Draft the wedding speech for Lao Zhou — by: 2026-08-21 — refs: [2026/08/15/2030] ## Self-model - When Alex is venting, lead with empathy first, then weave their own narrative back as evidence — refs: [2026/08/11/1426] ``` **Identity** — a structured, machine-parsed head: Name, Address them as, Pronouns, Alias (up to 8 lines). This is what lets Previously greet you by name in the arrival briefing, and how it knows how to address you. **Past** — durable memory, in two parts: - A single **rolling profile paragraph** — third person, rewritten in place as the picture of you sharpens, hard-capped at 2,400 characters (~600 tokens). - **Anchor facts** — bullet-point dates, decisions, and red lines, each with evidence refs. The admission test: "almost certainly still true in 3 years?" Cap: 8. **Now** — a semantic compression pool of what is currently alive: hooks, not narratives. Each item carries `refs` to its evidence slice and a `since` date. Cap: 5. Items older than 7 days are flagged to the evolution agent, which decides per item: promote durable substance to Past, or drop the hook. Nothing is silently deleted by code. **Horizon** — future-facing open loops: commitments, deadlines, awaited replies. Every item carries an explicit `by:` date. Horizon items **never age out** — they leave the card only by being resolved. Overdue items are kept and flagged: the next turn's brief tells the agent to proactively ask you how they turned out. **Self-model** — compact operating lessons Previously has learned about working with you ("lead with empathy when they're venting"). These are a **delta from the standing directives only** — a lesson that restates or contradicts a core rule is rejected unless it cites an explicit user override. Cap: 10. **The card answers WHO you are, not what was said.** Every entry carries `refs` — pointers like `2026/08/11/1426` back into the slices, which remain the lossless source of truth. Before citing specifics from a past event, the agent drills down with `readSlice`. ## Mutation-Based Evolution The card is never rewritten wholesale. A dedicated worker-model agent — the **Previously Agent** (`src/lib/episodic/flash/previously-agent.ts`) — holds an in-memory copy of the card and edits it through fine-grained, validated mutation tools: | Tool | What it does | |------|--------------| | `setIdentity` | Set or update one Identity field in place | | `updatePastProfile` | Rewrite the Past profile paragraph | | `addPastAnchor` / `removePastAnchor` | Add or remove a durable anchor fact | | `addNow` / `removeNow` | Add or remove a Now hook | | `promoteNowToPast` | Graduate a Now hook to a Past anchor, keeping its refs | | `addHorizon` / `resolveHorizon` | Open or resolve a Horizon loop | | `addSelfModel` / `removeSelfModel` | Add or remove an operating lesson | Validation lives **inside the tools**, not in a post-hoc pass (`src/lib/episodic/card-session.ts`). An over-limit or malformed write is rejected with specific feedback — "2630 chars, limit 2400 — compress and retry" — and the agent itself decides what survives the cap. Refs are required: no evidence, no write. Untouched parts of the card are preserved by construction; a mutation session cannot silently drop a line it never looked at. The result is written back only when the card's substance actually changed (header stamps refresh on every pass and are ignored). Both the live card and a per-slice snapshot (`memory/episodic/slices/YYYY/MM/DD/HHMM/previously.md`) are written in the turn's single commit — so every slice on the timeline carries the card as it stood when that conversation ended, and the "Previously On" dialog in the UI can show it. ## When the Card Evolves Evolution runs inline in the turn's housekeeping step, not on every message. Engineering owns the trigger; the model owns the content. Two triggers: 1. **Slice boundary.** When a slice closes, the turn analyzer judges whether anything is worth sedimenting (`evolve_card.worth`). If yes, the Previously Agent runs a deep review of the whole closed slice — reading its turns, its cognition log, and past cards as needed. If the analyzer itself failed, the gate defaults to *running*: a wasted worker call is cheap, a missed evolution is permanent memory loss. A legacy (pre-v5) card forces a run so format migration never waits for a "worthy" boundary. 2. **Explicit request or correction.** When you say "记住…", "update your card", or state a behavioral correction — "stop doing X", "from now on always…" — the analyzer flags it (`memory_update`) and evolution runs immediately, mid-slice. Progress streams to the client as it happens, and when the card moved, an evolution indicator shows a one-sentence account of what changed plus the line-level diff. A skipped run is reported too, with the reason — a silent skip would read as "it never runs". **Experimental:** card evolution is model judgment all the way down — what to record, what to promote, what to drop. The validation rails (caps, refs, rejections) keep it well-formed, not correct. Because everything is plain Markdown in git, you can always read the diff, edit the card by hand, or roll it back. ## Related - [Memory Model](/docs/memory-model) — where the card sits in the layered memory stack - [The Timeline](/docs/timeline) — slices, the lossless source of truth the card points into - [Recall](/docs/recall) — how the agent drills down from card refs into full slices --- # Recall > URL: https://previously.ldwid.com/en/docs/recall # Recall Recall is how Previously retrieves relevant past conversations — a two-tier engine where a cheap worker model returns lightweight pointers, then the main model reads the slices it chooses, all in a single turn. Every step renders live in the chat: recall runs in the open, not behind a spinner. ## The Two-Tier Recall Engine Episodic memory in Previously is stored as **slices** — directories at paths like `memory/episodic/slices/2025/11/21/0825/`, one per conversation burst, opened when you talk and closed after about 15 minutes of silence by default. Over weeks and months these accumulate into a deep archive. The challenge: how does the agent find what matters without reading everything every time? Previously splits the problem into two reflexes: | Layer | Model | Cost | What it does | |-------|-------|------|-------------| | **Worker scan** (conditioned reflex) | The resolved worker model | Cheap, fast | A small multi-step agent: reads the global timeline's pointer lines, scopes date windows, traces matching strands — returns **pointers** (`{ slice_id, relevance, reason }`) plus recommended reads, never slice content | | **Main deep read** (deliberate reasoning) | The main model you pick in the toolbar | Slower, thinking enabled | Receives pointers, checks summaries via `readSliceSummary`, opens full slices via `readSlice` only when the content itself matters | The worker is fallible by design. It trades completeness for speed: it navigates pointer lines and the strand map, never full bodies. The main model is the safety net — it digs deeper wherever the pointers lead. **Key takeaway**: the worker answers "where to look." The main model answers "what does it mean." Neither replaces the other. ### Step 1: Worker Scan Recall is not automatic — the main model calls the `recall` tool (`recallExecute` in `src/app/api/agent/tool-executors.ts`) when a query references the past, primed by a menu of known strands in its system prompt. The tool hands the query to a recall mini-agent (`runRecallSearch` in `src/lib/episodic/flash/recall.ts`) running on the worker model. The worker is a small multi-step agent with three read-only tools of its own: - `readGlobalTimeline` — the entry point: the newest slice pointer lines (id · focus · tags · turns) from the global timeline - `readTimelineWindow` — the same catalog scoped to a date range, for "what happened around …" queries - `readStrand` — trace a keyword strand to every slice that carries it After a few exploration steps it files one structured report: a `hits` array of pointers — slice id, relevance score (0–1), one-line reason — plus a confidence score, a short reasoning line, and up to five **recommended reads** (slice id, priority, why, what to look for). The ongoing conversation's slice is excluded by construction, and any slice id not in the timeline catalog is dropped before the pointers reach the main model — a hallucinated pointer can't send the main model to a file that doesn't exist. **The worker never reads slice bodies.** It has no `readSlice` tool — the timeline and the strand map are its whole world, so its answers are navigation, not quotation. Tracing strands is a first-class part of the scan: the strand index (`strands.json`, a keyword-to-slice-paths map built at slice-close) is injected into its prompt, and matching strands give it a direct path to relevant slices. > **One recall, then stop.** If the scan finds nothing, that is a definitive answer — there is no past context for that query. The agent is instructed not to call `recall` again for the same topic, and to answer from the conversation and its own knowledge instead. The recall phase renders as a collapsible card with a History icon. While the worker explores, its steps stream live into the card — "Reading global timeline…", "Tracing strand: X…" — settling on the hit count when the report lands. Expanded: each hit as a `slice_id` + reason + relevance percentage, an italic reasoning line, and confidence. ### Step 2: Deep Read The worker's pointers and recommended reads land in the main model's context as the `recall` tool result. The main model decides how deep to dig with its own memory tools: | Tool | Purpose | |------|---------| | `readSliceSummary` | The cheapest check — a slice's frontmatter only (focus, summary, tags, open loops) | | `readTimelineWindow` | Orient by time — pointer lines over a date range | | `readPreviously` | Read the user-card belief snapshot | | `readSlice` | Read a slice's turns — the last resort; an optional `range` fetches only the turns it needs | The raw browsing tools (`readStrand`, `listSlices`, `listStrands`) belong to the recall worker — the main model never walks the directory tree itself. The rule it follows: timeline lines and summaries are enough to decide **whether** to dig — never enough to **quote**. Before citing specifics from a past conversation, it opens the slice with `readSlice`. And when the scan found nothing, that result is definitive — there is no past context to find, so it answers from the current conversation instead of searching again. ### Step 3: Metadata Maintenance Recall is separate from the per-turn housekeeping pass, which proposes metadata updates for the **active slice** — the one currently being written in this conversation. The turn analyzer (`src/lib/episodic/flash/turn-analyzer.ts`) is the single worker-model call inside housekeeping, producing in one cheap pass: - **message_tags** — merge-first tags for the current user message: reuse existing strand names verbatim, create a genuinely new durable topic only when nothing covers it - **semantic_hint** — which existing strands this message is about, plus why - **intent** — what the user is trying to do this turn (debug, write, explain, chat, review, clarify) - **emotional_signal** — how emotionally weighted the message is, so the agent can match the user's register - **memory_update** — the exact content to fold into the user card, when the user explicitly asks to record something or states a durable correction And only when a slice closes this turn: - **closed_marking** — focus, summary, refined tags, and tone for the closing slice - **evolve_card** — whether anything in it deserves sedimenting onto the user card (on analyzer failure the default is to run — a wasted review is cheap, a missed evolution is permanent memory loss) A **memory-worthiness gate** runs alongside: trivial turns ("thanks", "continue") produce no tags, no strands, and no evolution — so one-off noise doesn't pollute the timeline. ## Three-Phase Chat Rendering A single assistant message renders in three visually distinct phases, split by part type in `ChatMessage`: ``` Reasoning (Brain icon, expandable card, inside bubble) | v Recall / tools (History / tool icons, inline cards) | v Response (text + inline tool calls in a Bubble) ``` ### 1. Reasoning Phase When the main model has thinking enabled (default: on, reasoning effort per your toolbar selection), the reasoning streams as `reasoning` parts. The client renders them as `ThinkingSteps` — a `PhaseIndicator` expandable card with a Brain icon. - **Collapsed summary**: "Thought · Ns" timer - **Expanded content**: the reasoning markdown ### 2. Recall / Tool Phase Tool calls render inline as expandable cards via `ToolRenderer`. Each memory tool — `recall`, `readSliceSummary`, `readTimelineWindow`, `readSlice`, `readPreviously` — gets a content-aware label rather than a raw tool name: the recall card carries the query, a slice card names the date ("Viewing the July 25 conversation", not "readSlice"). While a tool runs, a live status line streams its progress. Each card has five visual states: **running** (spinner), **completed** (dot), **error** (red), **interrupted** (yellow), **denied** (red). Click to expand and see the full tool output — a CSS `grid-template-rows` transition, no JS height measurement. ### 3. Response Phase The response body renders inside a `Bubble`. Text flows through `MarkdownRenderer` (react-markdown + remark-gfm + rehype-highlight). Tool calls appear inline in the order the main model invoked them, each wrapped in a per-tool renderer. ## Related - [Slices](/docs/slices) — how slices are created, closed, and indexed - [Strands](/docs/strands) — the semantic keyword index across slices (experimental) - [Architecture](/docs/architecture) — the turn workflow and the full component tree for message rendering --- # Getting Started > URL: https://previously.ldwid.com/en/docs/getting-started # Getting Started Previously replaces chat threads with a single, continuous relationship organized on a timeline — you just show up and talk, and the agent decides what past context is relevant per message. ## The Interface A fixed **AppHeader** spans the top of every page: the "Previously" logo on the left, with GitHub, Docs, Settings, theme toggle, language toggle, and the version badge on the right. Always visible regardless of scroll position. Below it, the page is a split view: 1. **The timeline wheel (left)** — a vertical, scrollable wheel of your slices, one row per slice. On phones it collapses to a narrow spine; on desktop it can expand over the content. The bottom of the wheel is **NOW** — the live conversation. 2. **The content (right)** — the live conversation when NOW is selected, or a historical slice view when you pick a past row. Picking a past slice plays a time-travel clock that rolls from where you are to that moment, then lands on the slice as it was recorded. 3. **A fixed input bar** — pinned to the bottom of the screen, with the model picker, thinking toggle, and reasoning-effort control. Sending a message always returns you to NOW. **Key takeaway: there is no conversation list, no thread management, no "new chat" button. The timeline IS the conversation list.** ### The Arrival Briefing When the live conversation is empty — first visit, or coming back after a while — the content area shows an **arrival briefing** instead of a blank chat: a film-title-card "PREVIOUSLY ON {your name}", followed by where we left off (the last topic), your open threads, and suggestion chips seeded from real memory that send a real message when clicked. A "view full previously" link opens the agent's current user card — what it believes about you right now. ## How Memory Forms as You Talk Memory is not something you manage — it forms automatically while you talk. A **slice** is one conversation burst. It opens when you start typing, stays active while you're engaged, and closes after about 15 minutes of silence by default (configurable). That's the only rule. No capacity limits, no topic-shift detection, no ML-driven false splits. Pure time-driven slicing. Every closed slice is a directory at: ``` memory/episodic/slices/YYYY/MM/DD/HHMM/ ``` The path is the timestamp: year, month, day, and the hour-minute of the first message. Inside it, three timelines side by side: - `timeline/core.md` — **the shared record**: the conversation itself, with YAML frontmatter (focus, summary, decisions, open loops, emotional tone, tags) maintained by the agent per turn. You never annotate anything by hand. - `timeline/agent.md` — **its thinking**: the agent's cognitive record for that slice — what it recalled, which tools it called, and why. - `previously.md` — a snapshot of the agent's user card at that moment, so you can see how its understanding of you evolved. On top of the slices sits a derived timeline catalog (`memory/episodic/timeline/`) — one pointer line per slice, grouped by era and day — that the agent and the wheel both navigate by. It is rebuilt deterministically from the slice files, so a slice is reachable if and only if its files exist. Read top to bottom across months and years, the slice directory is your autobiography. Git-diffable, human-readable, portable. ## Asking About Your Past: Watching Recall Work When you ask something that touches the past — "What did I decide about the housing project?" or "Remind me when I last talked to Sarah" — Previously runs a visible two-tier recall that renders as expandable cards above the answer. 1. **Recall scan** (a cheap worker model) — a small sub-agent explores the timeline catalog and the strand index (`memory/episodic/strands.json`) for a few steps and returns **pointers**, never content: slice IDs, relevance scores, a reason for each hit, and recommended reads with priorities. A housekeeping pass on the same tier keeps the active slice's metadata (summary, tags, open loops) fresh as a side effect. 2. **Main-model deep read** — the main model (the one you picked in the input bar) receives the pointers and decides what to actually read, preferring the cheapest sufficient unit: `readSliceSummary` for a relevance check, `readTimelineWindow` to orient by date, and `readSlice` — optionally narrowed to specific turns or a keyword search — only when the exact words matter. If the scan found nothing, the model is told not to keep searching and answers from the conversation instead. The recall results render as a card with a History icon: - **Collapsed**: shows the recall query and hit count. - **Expanded**: shows the scan's reasoning text, a list of recall hits (slice ID, reason, relevance percentage), recommended reads, and confidence. Tool calls render as inline cards with human-friendly labels: | Tool | You See | |------|---------| | `recall` | Recalling "..." | | `readSlice` | Viewing {date} | | `readTimelineWindow` | Viewing {period} | | `readStrand` | Following "..." | | `readPreviously` | Reading 前情提要… | | `readAgentTimeline` | Viewing cognition… | | `thinkDeep` | Reasoning fragment | Each card has five visual states: **running** (spinner), **completed** (dot), **error** (red), **interrupted** (yellow), **denied** (red). Click or press Enter/Space to expand and see the full tool output. The message itself renders in phases, matching the agent's internal execution order: **Reasoning** → **Recall/tools** → **Response**. You see the agent think, scan the timeline, work, and then answer — all in one message. ## First-Run Experience: Demo Mode Previously does not have an in-app onboarding wizard. Instead, it ships a read-only **demo mode** that transforms the deployment into a tour. Run with `STORAGE=demo` (or leave `STORAGE` unset on a server without a `GITHUB_TOKEN`) and every `memory/` read is served from a seeded fictional persona dataset — by default **Caleb** (`personal_14`), 30+ slices spanning multiple years. In demo mode the name in the arrival briefing becomes a persona picker, so you can switch between the seeded personas. A fixed badge in the header tells you nothing written is saved. Writes appear to succeed but are silently discarded; refresh the page and the slate resets. The live demo is at [previously-demo.ldwid.com](https://previously-demo.ldwid.com). It runs the full two-tier recall system, so you can spin the timeline wheel, travel through past slices, chat freely, and watch recall work — all without setting up your own deployment. ### Starting Your Own Timeline To move from demo to your own timeline: 1. Deploy Previously with your own GitHub repository and a GitHub fine-grained personal access token (contents read/write scope, single repo). 2. Set `STORAGE=github` (or let auto-detection do it: unset `STORAGE`, set `GITHUB_TOKEN`). 3. That's it. The first message you type opens your first slice. There is no account creation, no database setup, and no seed data. The timeline starts empty. Memory accumulates from your first conversation forward. ## Strands: The Semantic Layer Over Time As slices accumulate, the agent builds a lightweight keyword index called **strands**. A strand is a topic keyword — like `work`, `family`, `housing` — that recurs across multiple slices. The file `memory/episodic/strands.json` maps each strand to every slice that carries it, and a consolidation pass keeps the index clean by merging duplicate keywords. Recall scans the strand index to find slices by topic, not just by recency. If you've talked about "housing" across 30 slices across three years, the scan finds them through the strand index, then the main model reads the actual content. **Roadmap:** Strands today are a thin keyword-to-slice index. A richer first-class strand with rolling summaries and deeper recall integration is a future milestone. For now, the index is lossless — it points to every relevant slice, and the main model reads what it needs. ## What This Means Day to Day There are no rituals. You don't open a "new conversation." You don't tag or classify anything. You don't manage memory. - **Show up** — open the page. The arrival briefing tells you where you left off: "PREVIOUSLY ON {your name}," the last topic, the open threads. - **Talk** — type and press Enter. If it's been more than about 15 minutes, a new slice opens. The agent reads the timeline, scans for relevant context, and responds. - **Ask about the past** — "What did I decide about X?" or "When did Y happen?" The worker scans the timeline catalog and the strand index, the main model reads what matters, and you see the tool cards appear and resolve. - **Travel** — the wheel on the left is your full history. Pick a row and the clock rolls you back to that slice. That's the loop. No setup, no cleanup, no thread management. Just one continuous conversation with memory that works. ## Project Status **Previously is in active early development and not yet ready for personal or production use** (status: experimental). The core architecture is functional, but many subsystems are still being designed and built. This guide documents the current codebase; it is framed as a personal self-hosted deployment, not a SaaS signup. ## Related - [The Timeline](/docs/timeline) — the main interaction surface in detail - [Memory Model](/docs/memory-model) — slices, strands, the whitelist, and context assembly - [Recall](/docs/recall) — the two-tier retrieval pipeline in detail - [Configuration](/docs/configuration) — `STORAGE`, demo mode, and the environment variables --- # Deployment > URL: https://previously.ldwid.com/en/docs/deployment # Deployment Get your own Previously running in about 10 minutes. You'll create a private copy from the template, create a GitHub token, and deploy to Vercel. No server management, no database setup — just a repo, a token, and a deploy. ⚠️ **Important: single-user, no login system.** Previously is in early development and has no user accounts or authentication — it is designed as a single-user, privately-deployed product. Anyone with your deployment URL can use your instance — and access or modify your memory data. Keep your deployment URL private. Do not share it publicly. Limit your Vercel deployment visibility if possible. The mutation endpoints do have a same-origin guard plus an optional access key for scripts (see [Access key for API callers](#access-key-for-api-callers) below), but that is not a substitute for real authentication. ## Create your private copy Previously is a [GitHub Template Repository](https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-repository-from-a-template). Click **Use this template** on the [Previously repo page](https://github.com/previously-lab/agent) and choose **Create a new repository**. **Make your new repository private** — this repository holds your agent's memory data (episodic slices, memory nodes, tasks, and session state). A private repo keeps everything accessible only to you and your agent. > Your repository contains the application code. Memory data your agent creates is stored in whichever GitHub repository the environment variables point to — by default your own repo, but you can point it at a different repo entirely. Code and memory are decoupled by design. ## Create a GitHub token Previously uses the GitHub API to read and write memory data. You need a fine-grained personal access token scoped to your repository. 1. Go to **GitHub Settings** (your profile picture → **Settings**). 2. In the left sidebar, click **Developer settings**. 3. Click **Personal access tokens** → **Fine-grained tokens**. 4. Click **Generate new token**. 5. Give it a name (like "Previously") and choose an expiration. 6. Under **Repository access**, select **Only select repositories** and pick your private repo. 7. Under **Permissions**, find **Contents** and set it to **Read and write**. 8. Click **Generate token** and **copy the token immediately** — you won't be able to see it again. The token needs Contents read/write because Previously reads memory files (episodic slices, memory nodes, task lists) and writes new ones as the agent works. Scoping it to a single repo limits the blast radius if the token is ever exposed. ## Deploy to Vercel Two options, from smoothest updates to quickest setup. ### Option A: Import to Vercel (recommended) 1. Create your private copy from the template (you already did this above). 2. Go to [vercel.com](https://vercel.com) and sign in with your GitHub account. 3. Click **Add New** → **Project**. 4. Import your private repo from the repository list. If it's not visible, click **Adjust GitHub App Permissions** and grant Vercel access to the repo. 5. In the **Environment Variables** section, add `DEEPSEEK_API_KEY`, `GITHUB_TOKEN`, `GITHUB_REPO_OWNER`, and `GITHUB_REPO_NAME` (see below). Optionally add `ACCESS_SECRET` if you plan to call the API from scripts. 6. Click **Deploy**. Vercel detects Next.js, builds, and gives you a URL. That's it. ### Option B: Local dev (for testing) To run Previously on your own machine: ```bash git clone https://github.com/YOUR_USERNAME/YOUR_REPO.git cd YOUR_REPO pnpm install pnpm dev ``` Create a `.env.local` file (see below) before starting. The app runs on `http://localhost:3000`. ## Environment variables For local dev, create `.env.local` in the project root. For Vercel deployment, add these in your project settings under **Settings → Environment Variables**. ```bash # Required — DeepSeek API key (powers the main model; the worker tier resolves from the same provider) DEEPSEEK_API_KEY=sk-... # Required — the GitHub token you created above GITHUB_TOKEN=github_pat_... # Required — your GitHub username and the repo name GITHUB_REPO_OWNER=your-username GITHUB_REPO_NAME=your-repo-name # Optional — read-only demo persona mode # STORAGE=demo # Optional — access key for non-browser API callers (see below) # ACCESS_SECRET= ``` | Variable | Required | What it is | Where to get it | |---|---|---|---| | `DEEPSEEK_API_KEY` | Yes | Authenticates requests to DeepSeek for the main model (and, by default, the worker tier derived from the same provider). | [platform.deepseek.com](https://platform.deepseek.com) — create an account and generate an API key. | | `GITHUB_TOKEN` | Yes | The fine-grained personal access token. Every GitHub read and write flows through it. | [Created above](#create-a-github-token) in the token setup steps. | | `GITHUB_REPO_OWNER` | Yes | Your GitHub username or organization that owns the repository holding memory data. | Your GitHub username. | | `GITHUB_REPO_NAME` | Yes | The repository name where memory data lives. By default this is your repo, but you can point it at any repo you own. | Your repo's name on GitHub. | | `STORAGE` | No | Selects the data source: `local` (filesystem), `github` (GitHub API), or `demo` (read-only pre-seeded persona dataset). Auto-detected when unset. | — | | `ACCESS_SECRET` | No | Access key that non-browser callers must send as an `x-access-key` header on mutation endpoints. Only enforced when set. | Any random string you choose. | See [Configuration](/docs/configuration) for the full variable reference, including the implicit storage backend switch. ## Access key for API callers Previously has no login system, but its mutation endpoints — `POST /api/chat`, `/api/loops`, and `/api/episodic/flush` — are protected by a lightweight origin guard. Requests from your own browser pass automatically (the `Origin` header matches the deployment's host). Read endpoints and CORS preflights always stay open. Calls that don't come from your browser — curl, scripts, cron jobs — look external to the guard. If you want to use them, set `ACCESS_SECRET` to any random string and send it as a header: ```bash curl -X POST https://your-deployment.vercel.app/api/chat \ -H "x-access-key: $ACCESS_SECRET" \ -H "Content-Type: application/json" \ -d '{"messages": [...]}' ``` When `ACCESS_SECRET` is not set, these callers are also allowed — the private-deployment default stays frictionless. Set the variable only if you expose the URL beyond your own browser and want the key check active. ## Syncing upstream updates Previously has a built-in sync mechanism. When a new version is released: 1. Open your Previously instance and go to **Settings**. 2. In the **Version & Updates** section, if you see "Update available," click **Sync from upstream**. 3. The latest code is merged into your repository automatically. The sync commit lands on your `main` branch, and Vercel redeploys from it automatically. Your personal data (`memory/`, `tasks/`, `sessions/`) is never overwritten — only code directories are synced. The sync creates a proper merge commit that preserves your git history. ### Manual sync (fallback) If the built-in sync button is unavailable, you can merge upstream changes manually from the command line. First, add the upstream remote (one-time setup): ```bash git remote add upstream https://github.com/previously-lab/agent.git git fetch upstream git merge upstream/main --allow-unrelated-histories git push origin main ``` Vercel redeploys automatically after the push. ### What sync touches Code directories (`src/`, `content/`, `public/`, `messages/`, `scripts/`, etc.) and root config files are synced from upstream. Episodic memory data (`memory/episodic/`) is gitignored, so pulling in new code never overwrites your accumulated memory. Your personal data in `memory/`, `tasks/`, and `sessions/` is always preserved. ## Related - [Architecture](/docs/architecture) — three-layer separation and data model - [Getting Started](/docs/getting-started) — tour the interface and send your first message --- # Configuration > URL: https://previously.ldwid.com/en/docs/configuration # Configuration Deployment configuration is entirely environment variables — no project config files, no settings YAML. Seven variables appear in the shipped `.env.example`, plus one optional API key per additional model provider. Day-to-day preferences (model choice, thinking, slicing) live in a per-user config file stored inside the memory data directory, not in the deployment. **Key takeaway:** `DEEPSEEK_API_KEY` is required for the default setup — it powers the default models and web search. `GITHUB_TOKEN` is the variable you think of as required — its presence gates the entire GitHub backend; without it the app reads and writes the local filesystem instead. `GITHUB_REPO_OWNER` and `GITHUB_REPO_NAME` are required only when the GitHub backend is active. ## Environment variable reference Every variable the runtime actually reads, in one table: | Variable | Required | In `.env.example` | Default | Runtime effect | |---|---|---|---|---| | `DEEPSEEK_API_KEY` | Yes (default setup) | Yes | — | API key for DeepSeek, the default provider. Powers the default main/worker models and — regardless of which provider you switch to — the web-search tool, which runs on DeepSeek V4 Flash through DeepSeek's Anthropic-compatible endpoint. | | `GITHUB_TOKEN` | See note | Yes | — | Presence of this variable **is the backend switch**. When set (and `STORAGE` unset), the app uses the Octokit/GitHub API backend; when absent, auto-detection falls through to local filesystem or demo. Leave unset or commented out for local development. An empty string `GITHUB_TOKEN=` correctly falls back. A fine-grained PAT with Contents read/write scoped to a single repository. | | `GITHUB_REPO_OWNER` | When using GitHub backend | Yes | `local` | GitHub username or organization that owns the memory repository. | | `GITHUB_REPO_NAME` | When using GitHub backend | Yes | `local` | The repository name for memory data. | | `STORAGE` | No | Yes | Auto-detected | Selects the data source: `local` (filesystem), `github` (GitHub API), or `demo` (read-only pre-seeded persona). When unset, auto-detects: `GITHUB_TOKEN` present → `github`; `NODE_ENV=development` → `local`; otherwise → `demo`. | | `BENCHMARK_BASE_URL` | No | Yes (commented) | — | Base URL for the remote benchmark-data repo used in `demo` mode (e.g. `https://raw.githubusercontent.com/previously-lab/benchmark-data/main`). When unset in `demo` mode, reads fall back to a local `../benchmark-data` sibling directory on disk. | | `ACCESS_SECRET` | No | Yes (commented) | — | Opt-in access key for non-browser callers. When set, POST requests to the mutation endpoints (`/api/chat`, `/api/loops`, `/api/episodic/flush`) that lack a same-origin signal must send an `x-access-key` header with this value, or they get a 403. Same-origin browser calls always pass. Leave unset to keep a private deployment frictionless. | ### Additional provider keys The model catalog (`src/lib/models/catalog.ts`) activates one provider per configured API key. Setting any of these makes that provider's models selectable in Settings — no other configuration is needed: | Variable | Provider | |---|---| | `ANTHROPIC_API_KEY` | Anthropic | | `OPENAI_API_KEY` | OpenAI | | `MOONSHOT_API_KEY` | Moonshot AI | | `DASHSCOPE_API_KEY` | Alibaba (DashScope) | | `GOOGLE_API_KEY`, `GOOGLE_GENERATIVE_AI_API_KEY`, or `GEMINI_API_KEY` | Google (first set wins) | | `MISTRAL_API_KEY` | Mistral | | `XAI_API_KEY` | xAI | | `GROQ_API_KEY` | Groq | > **Note on `GITHUB_TOKEN`:** When `STORAGE` is unset, the data source is auto-detected in one place — `src/lib/data-source/resolve.ts`: `GITHUB_TOKEN` present → GitHub API; `NODE_ENV=development` → local filesystem; otherwise → demo. There is no dedicated `USE_GITHUB` environment variable. This is intentional: the simplest possible toggle, no config file, no extra surface area. ## Backend switch: GitHub API vs local filesystem vs demo The storage backend is implicit by design. When `STORAGE` is unset it is auto-detected from `GITHUB_TOKEN` and `NODE_ENV`; you can also set it explicitly: ```typescript resolveDataSource(); // "local" | "github" | "demo" — STORAGE override, else auto-detect ``` | Backend | When selected | How reads work | How writes work | |---|---|---|---| | **GitHub API** | `STORAGE=github`, or auto-detected when `GITHUB_TOKEN` is set | `octokit.rest.repos.getContent`, base64-decoded. Requires `GITHUB_REPO_OWNER` and `GITHUB_REPO_NAME`. | `createOrUpdateFileContents` on the same repo. | | **Local filesystem** | `STORAGE=local`, or auto-detected in development | `fs.readFileSync` from the project root. Reads physical files from disk. | `fs.writeFileSync` to the same root. | | **Demo (read-only)** | `STORAGE=demo`, or auto-detected otherwise | Reads from a pre-seeded persona dataset — a local `../benchmark-data/{persona}/...` sibling or the `BENCHMARK_BASE_URL` remote. | No-op: returns success, discards data. | The storage backends enforce the same security boundary: - **Path whitelist**: only `memory/`, `tasks/`, and `sessions/` are read-write; `src/` is agent read-only - **Size cap**: `MAX_FILE_SIZE_BYTES = 1_000_000` (1 MB) on file reads and writes The local-filesystem backend is what you use during development (`pnpm dev`). It reads and writes real files on disk — no GitHub, no network, no rate limits. The GitHub backend is what you deploy to Vercel. Both implement the same interface, so the rest of the app never branches on which one is active. ## Demo mode behavior `STORAGE=demo` puts the entire memory layer into a read-only demonstration mode against a pre-seeded persona dataset. Here is exactly what changes: ### Where demo reads come from In demo mode, every `memory/` read is served from a benchmark-data persona dataset, not from the repo's own `memory/` directory: ``` memory/episodic/slices/... → {persona}/episodic/slices/... (from benchmark-data) memory/nodes/some-node.md → {persona}/nodes/some-node.md (from benchmark-data) ``` The default persona is `personal_14` (Caleb). Reads are served from a local sibling directory `../benchmark-data/{persona}/...` when `BENCHMARK_BASE_URL` is unset, or from the remote base URL (e.g. `https://raw.githubusercontent.com/previously-lab/benchmark-data/main/{persona}/...`) when set. Demo reads never touch the repo's real `memory/` directory. ### Writes: accepted, never persisted Demo mode makes writes a no-op: - `writeFileDemo` (`src/lib/demo/demo-fs.ts`) returns `{ path, created: false }` without writing anywhere — on both the local and remote demo backends. The agent sees a successful write. The data is silently discarded. ### Remote demo data When deploying a demo instance without a local benchmark-data checkout, set `BENCHMARK_BASE_URL` to the raw URL of the benchmark-data repo: ```bash STORAGE=demo BENCHMARK_BASE_URL=https://raw.githubusercontent.com/previously-lab/benchmark-data/main ``` With the variable set, demo reads fetch from that base URL; unset, they fall back to the local `../benchmark-data` sibling directory. ### Persona picker In demo mode the hero becomes a persona picker (`hero-section.tsx` loads the persona list via `listDemoPersonas`), so you can switch between pre-seeded personas. ## Model catalog and model routing Previously resolves its available models at runtime from each configured provider's live model-list API (`src/lib/models/catalog.ts`). Two tiers, one catalog: | Tier | Purpose | Model | Thinking | |---|---|---|---| | **Worker** | Cheap internal calls: turn analysis, recall search, tag extraction, belief evolution, background loops | Resolved worker model (a lightweight of the main provider) | Always off | | **Main** | Deep reasoning, full-slice reads, response generation | User-selected main model | User's choice | For every provider whose API key is set, the catalog calls the provider's list endpoint, normalizes legacy ids to current names, and enriches known ids with curated metadata. Unknown ids get sensible provider defaults; a provider whose list call fails falls back to its curated entries so it stays usable offline. The result is cached for 30 minutes — env changes take effect after the TTL. The curated fallback registry (`src/lib/models/registry.ts`) ships five entries: | Model ID | Provider | Thinking | Vision | Max tokens | |---|---|---|---|---| | `deepseek-v4-flash` | DeepSeek | Yes | No | 393216 | | `deepseek-v4-pro` | DeepSeek | Yes | No | 393216 | | `claude-haiku-4-5` | Anthropic | Yes | Yes | 200000 | | `claude-sonnet-5` | Anthropic | Yes | Yes | 200000 | | `claude-opus-4-8` | Anthropic | Yes | Yes | 200000 | Legacy ids are normalized forward: a stored `deepseek-chat` becomes `deepseek-v4-flash`, and `deepseek-reasoner` becomes `deepseek-v4-pro`. ### Main model selection The main model is resolved per request, in this order: 1. **Request override** — the client sends `body.model` with each chat request. 2. **User config default** — `model.provider` in `memory/user/config.json` (editable in Settings). Ships as `deepseek-v4-pro`. 3. **Deployment default** — the first curated model whose provider key is set. Unknown or unavailable ids fall back to the deployment default; the resolved model is logged per turn (`[Turn] model=…`) so a switch is verifiable in the server log. ### The worker is resolved, not hardcoded Internal calls never reuse the main model blindly — `resolveWorkerModel()` (`src/lib/models/worker.ts`) resolves a cheaper tier in this order: 1. **Manual pin** — an explicit `worker.provider` in the user config (mode `"manual"`). 2. **Same-provider lightweight** — curated for known providers (`deepseek-v4-flash` for DeepSeek, `claude-haiku-4-5` for Anthropic); otherwise the smallest non-thinking model of the same provider from the live catalog. 3. **The main model itself** — same provider, always correct. The worker's central call is the turn analyzer (`src/lib/episodic/flash/turn-analyzer.ts`): one cheap, structured, thinking-off pass that produces the message's keyword tags, a semantic hint about which existing Strands it belongs to, marking for any slice that closed this turn, and a decision on whether the closing slice should evolve the user card. ### Thinking toggle and reasoning effort The thinking toggle is a request-level boolean (`body.thinking`, falling back to `model.thinking` in the user config, default `true`) that is **not** a model switch. A separate effort control (`body.effort`, falling back to `model.reasoningEffort`, default `"medium"`) takes `low | medium | high`. Both are mapped onto each provider's own options shape by a single module (`src/lib/models/effort-injector.ts`): `reasoningEffort` for DeepSeek and OpenAI-compatible providers, `budgetTokens` for Anthropic. Thinking off always means an explicit thinking-disabled shape, on every provider. > **Nuance:** the thinking toggle and effort slider never swap models. `deepseek-v4-pro` and the effort axis are independent choices — the toggle is a provider-level option on whichever model is selected, not a model swap. ## The user config file Deployment is env-only, but per-user preferences live in `memory/user/config.json`, stored in the memory repo itself (so it syncs with the rest of your data) and editable via the Settings UI. Every field is optional; missing fields fall back to shipped defaults (`src/lib/config/defaults.ts`): | Field | Default | Purpose | |---|---|---| | `model.provider` | `deepseek-v4-pro` | Main model id | | `model.thinking` | `true` | Thinking on/off | | `model.reasoningEffort` | `medium` | Thinking depth: `low` / `medium` / `high` | | `worker.mode` / `worker.provider` | `auto` / — | Worker resolution: derive from main model, or pin a model id | | `slicing.maxTurnsPerSlice` | `20` | Force-close the active Slice after this many turns | | `slicing.timeSilenceMinutes` | `15` | Minutes of inactivity before a time-silence split | | `context.recentTurnsLimit` | `20` | Recent turns included in the assembled prompt | The file is read at request time through the same data-source channel as the rest of memory, and cached for 60 seconds. ## Internationalization i18n uses `next-intl` with exactly two locales: | Locale | Code | Default | |---|---|---| | English | `en` | Yes | | Chinese | `zh` | No | Configuration lives in `src/i18n/routing.ts`, translations in `messages/en.json` and `messages/zh.json`. Navigation must use utilities from `@/i18n/navigation` instead of `next/navigation`. ## The omitted vars A few environment variables you might expect, and why they do not exist: | You might expect | Reality | |---|---| | `USE_GITHUB` | Does not exist. The backend switch is the `STORAGE` env var with auto-detection in `src/lib/data-source/resolve.ts` — implicit, zero-config, intentional. | | `DEMO_MODE` | Does not exist. Demo is just another `STORAGE` value; no module reads a separate demo flag. | | `LOG_LEVEL` | Not implemented. Logging is thin. | | `DATABASE_URL` | There is no database. State lives in GitHub files. | | `PORT` | Not read by the app; Next.js handles it. | ## Related - [Deployment](/docs/deployment) — deployment walkthrough with the full `.env.local` template - [Memory Model](/docs/memory-model) — how slices and strands work; the data that configuration makes accessible - [Recall](/docs/recall) — how the worker and main models use the configured models --- # Architecture > URL: https://previously.ldwid.com/en/docs/architecture # Architecture Previously is a cloud agent on Vercel that reads GitHub files, calls LLMs, and streams responses back — no database, no cron, no persistent server. What makes it unusual is that **every chat turn runs inside a durable Vercel Workflow run**: each LLM call and each tool call is an individually durable, auto-retried step. GitHub remains the single source of truth for memory — Workflow is only the execution container. ## The Three Layers The system is split into three layers, each with a distinct responsibility and a hard boundary between them: | Layer | What it is | What it does | |-------|------------|--------------| | **Browser / Phone** | Next.js App Router UI | Renders the chat surface. Captures input. Streams the response. That is all — no business logic, no state machine, no local memory. | | **Vercel** | Orchestration | Receives a trigger, reads GitHub state, runs the agent workflow, writes back. Stateless, event-driven, durable. | | **GitHub Private Repo** | The single source of truth | Holds everything: `src/` (agent-read-only), `memory/`, `tasks/`, `sessions/` (agent-read-write). Code and data coexist in one repo. | **Key takeaway: no database, no always-on agent, no persistent connection.** Previously runs entirely inside Vercel's request lifecycle. You send a message, the turn workflow fires, and the response streams back. When the response ends, there is no background process. (Durable background loops are part of the design but temporarily disabled — and even they persist their state to the repo, not to a server.) ## The Durable Turn Workflow The whole agent is `src/app/api/chat/turn-workflow.ts` — a `"use workflow"` controller. Every user message triggers one of these runs: 1. **Housekeeping** — resolve or recover the active time slice, apply the about 15-minute silence rule (configurable), append the incoming turn. 2. **Worker pass** — a cheap worker model (derived from the main model's provider) scans recent slice summaries for recall pointers and maintains the slice's metadata, in one call. It also applies the **semantic gate**: trivial turns ("thanks", "continue") produce no tags and no strands. At slice close, a separate worker pass consolidates the strand index (merging semantic duplicates) and the timeline projection is reconciled. 3. **Prompt assembly** — the system prompt is assembled from identity, the user card, and the episodic timeline context. Order is a cache decision: stable blocks first (identity constitution, user card), variable blocks last (turn brief, timeline pointers, strands menu, evolution and demo notices), so the provider's prompt cache reuses the long prefix across turns. 4. **Pro agent loop** — the main model runs as a `WorkflowAgent` (`@ai-sdk/workflow`). Each LLM call and each tool call is its own durable step: tool executors are standalone `"use step"` functions in `src/app/api/agent/tool-executors.ts`, so the runtime retries them on transient failures. The agent reasons, recalls, reads, and answers. If a step is platform-killed or the model hits the token cap, the turn doesn't die — a bounded continuation rebuilds the messages from the completed steps and resumes where it left off. 5. **Finalize** — the turn is appended to the slice's `timeline/core.md` (the shared record), the agent's cognition — thinking, tool calls, sub-agent reports — is written to `timeline/agent.md` (its own timeline), and the timeline projection and strands are updated. Because every step is durable, the run survives dropped connections. `WorkflowChatTransport` on the client re-attaches to the same run when the tab returns to the foreground, and replays what was missed. Background loops use the same agent layer as separate workflow runs (currently disabled — see Tools below). Each slice therefore stores two of the product's three timelines: `core.md`, the record you share, and `agent.md`, what the agent was thinking at the time. The third timeline — your life outside the chat — is never stored; the agent only ever sees what passes through the conversation. ## Agent Layer The agent layer lives in `src/app/api/agent/` and is shared by chat turns and any background loops: | File | Purpose | |------|---------| | `agent.ts` | Creates the `WorkflowAgent` with the model, thinking, and tool set | | `tools.ts` | Tool definitions (schema + context schema) — the chat agent's tool set | | `tool-executors.ts` | Standalone `"use step"` executors for each tool | | `register-model-classes.ts` | Registers model hosts so they survive the workflow→step boundary | ### Tools The chat agent gets a small, deliberate tool set — **specialists, not brute force**. The main model orchestrates; focused work is dispatched to dedicated agents, and every dispatch is a visible tool call. Pointer-first memory reads, cheapest to most expensive: | Tool | Purpose | |------|---------| | `readSliceSummary` | A slice's frontmatter only — focus, summary, tags, open loops. The cheap relevance check | | `readTimelineWindow` | The timeline catalog over a date window — one pointer line per slice, for orienting by time | | `readSlice` | The actual conversation text — last resort. Optional `range` (turns / last N / date / search / lines) | | `readPreviously` | The user-card snapshot for a slice, current or historical | | `readAgentTimeline` | The agent's own past reasoning for a slice | Specialist dispatch: | Tool | Purpose | |------|---------| | `recall` | The recall agent — hands the actual search to the worker model's recall engine, returns pointers, never content | | `webSearch` / `webFetch` | The search agent — cited answers from the live web, then targeted page reads | | `thinkDeep` | The think agent — a think-only copy of the main model reasons through one self-contained fragment at low / medium / high effort; substantive turns fan out into fragments before answering | > **One recall, then stop.** The main agent does not browse memory itself — exploration is the recall engine's job. It asks `recall` once, reads what it needs with `readSlice`, and moves on. The raw browse tools (`listSlices`, `readTimeline`, `listStrands`, `readStrand`) stay with the recall engine and the loop tool set. `startLoop` is defined but currently commented out in `tools.ts` (background loops are temporarily disabled while being stabilized). Curation is a specialist job too: tags, strands, and the user card are maintained by the worker model during housekeeping — never by the main agent mid-answer. `thinkDeep` fragments reuse the turn's resolved main model and share the assembled system prompt as a prefix, so provider prompt-cache hits span main and sub-agent calls within one turn. ## Two-Tier Model Split The model layer (`src/lib/models/`) resolves two tiers: - **Main model** — user-selected in the chat toolbar, persisted to `memory/user/config.json`. Does the reasoning, tool calling, and response generation. - **Worker model** — a cheap tier derived from the main model's provider (or manually pinned). Runs the housekeeping calls: recall scanning, metadata maintenance, the semantic gate, strand consolidation. This keeps the per-turn cost low while the main model does the heavy lifting. `resolveWorkerModel()` (`src/lib/models/worker.ts`) picks it: manual pin → same-provider lightweight → the main model. The catalog is models.dev-driven (`src/lib/models/catalog.ts`), gated by configured API-key env vars and reverse-filtered against each provider's live `/models` endpoint. ## Frontend Architecture The Next.js App Router enforces a strict server-component/client-component boundary. `page.tsx` is a Server Component that preloads the user config and sets up the demo persona, then renders `ChatPage` — a thin client shell owning the AI SDK `useChat` hook with `WorkflowChatTransport`. The page is one vertical scroller: 1. **Hero** — server-rendered, `"Previously on {name}"`. 2. **Sticky horizontal timeline** — `HorizontalTimeline`, a date-dot strip that snaps below the AppHeader. 3. **Chat content** — live messages, or a historical slice view when a past dot is selected. 4. **Sticky input bar** — with model selector, thinking toggle, image attachments, and the demo trigger. Message rendering is a unified stream pipeline (`buildStream` in `src/lib/chat/build-stream.ts`): reasoning merges into `ThinkingSteps`, tool calls merge into `ToolRenderer` cards by `toolCallId`, `data-phase` parts become `PhaseIndicator`s, `data-evolution` parts drive the per-bubble `EvolutionIndicator`. Everything renders inline inside the assistant bubble via `AnimatePresence`. ## Core Modules | Module | Path | Purpose | |--------|------|---------| | Capabilities | `src/lib/capabilities.ts` | Global app-mode checks (isAIConfigured, isDemo, canWrite) | | Episodic Memory | `src/lib/episodic/` | Time-slice management, slicing rules, strands index | | Timeline Catalog | `src/lib/episodic/timeline/` | The timeline subsystem: `index.json` + `timeline.md` are pure projections of the slice files, reconciled by `weaveTimeline` (enumerate → diff → rebuild; throttled per turn, forced at slice close) | | Turn Analyzer | `src/lib/episodic/flash/turn-analyzer.ts` | The worker-model housekeeping call | | Strand Consolidator | `src/lib/episodic/flash/strand-consolidator.ts` | Worker-model semantic dedupe of the strand index at slice close | | Time Localization | `src/lib/episodic/time-localize.ts` | Server-side local-time annotation for read tools | | User Card | `src/lib/episodic/previously-*` | The compact user card + mutation tools in `card-session.ts` | | Model Registry | `src/lib/models/` | models.dev catalog, provider dispatch, worker resolution | | GitHub Tools | `src/lib/tools/` | readFile/writeFile/listFiles via Octokit | | Path Whitelist | `src/lib/whitelist/` | Security boundary: memory/tasks/sessions only | | Origin Guard | `src/lib/security/origin-guard.ts` | Same-origin guard on mutation endpoints + optional `ACCESS_SECRET` | ## Security Model Security is enforced entirely in TypeScript at the tool boundary. ### Path Whitelist `src/lib/whitelist/index.ts` defines the only three writable directories: ``` memory/ tasks/ sessions/ ``` `normalizePath()` decodes URI components, converts backslashes to forward slashes, resolves `./` and `../` segments, and strips leading slashes. `isPathAllowed()` rejects empty paths and absolute paths (Unix and Windows drive letters), then checks the path starts with one of the three allowed prefixes. ### `src/` Is Agent-Read-Only The `src/` directory simply does not appear in the whitelist. No agent tool can write there. The agent can read `src/` through git, but cannot modify it — the path whitelist rejects write attempts. This keeps the codebase integrity independent of the agent's execution. ### Same-Origin Guard This is a single-user, privately deployed product — there is deliberately no login system. Instead, `guardRequest()` (`src/lib/security/origin-guard.ts`) keeps drive-by traffic off the expensive POST mutation endpoints (`/api/chat`, `/api/loops`, `/api/episodic/flush`): 1. An `Origin` header whose host matches the request's own host is a same-origin browser fetch → allow. 2. No `Origin` but `Sec-Fetch-Site: same-origin` / `same-site` → allow. 3. Anything else (curl, scripts, cron, cross-site posts) must send an `x-access-key` header equal to `ACCESS_SECRET` — but only when that variable is set. Unset, the private-deployment default stays frictionless; operators opt into the key check by setting it. GET stream-replay endpoints and preflights stay open. ### Flush Endpoint Constraints `POST /api/episodic/flush` is the emergency save the browser fires on `beforeunload` via `sendBeacon`, appending in-flight turns to the active slice. Because the `sliceId` is interpolated into a file path, it is validated against the strict `YYYY-MM-DD-HHMM` format before anything is computed — a loose string would be a path-traversal hole. Defense in depth: the resolved write target must also land in an explicit allow-set of that slice's own `timeline/core.md` / `timeline/agent.md`, inside the system-managed episodic zone the generic write tools refuse to touch. ### GitHub Token Scope The `GITHUB_TOKEN` is scoped to a single repository with contents read/write. The agent operates on exactly one repo: the one defined by `GITHUB_REPO_OWNER` / `GITHUB_REPO_NAME`. There is no cross-repo access. All path validation is server-side — the client is untrusted. The browser never constructs file paths or makes storage decisions. ## Data Source Modes Storage has three modes, controlled by `STORAGE`: | Mode | When | Behavior | |------|------|----------| | `local` | local dev | Reads/writes the local filesystem | | `github` | production | Reads/writes your repo via the GitHub API (Octokit) | | `demo` | preview | Read-only, pre-seeded personas | Auto-detection: `GITHUB_TOKEN` present → `github`; `NODE_ENV=development` → `local`; otherwise → `demo`. ## What Comes Next (Roadmap) - First-class strands — a rolling summary + recall integration for each strand - Re-enable background loops (`startLoop` is defined but commented out) - Richer cross-slice navigation on the timeline - More demo personas The project status badge is **experimental**. ## Related - [Memory Model](/docs/memory-model) — how slices, strands, and the user card work - [Recall](/docs/recall) — the two-tier retrieval pipeline in depth - [Timeline](/docs/timeline) — the UI surface and how the agent sees your past --- # FAQ > URL: https://previously.ldwid.com/en/docs/faq # FAQ Common questions about how Previously works, what it costs, and where the boundaries are. ## Is there a database? No. All state is plain Markdown files with YAML frontmatter, stored in a GitHub repository. There is no database, no vector store, no ORM, and no proprietary format. Slices, strands, the user card — everything is a file. The indexes are a handful of JSON files. A global catalog at `memory/episodic/timeline/index.json` lists every slice (with a Markdown projection at `memory/episodic/timeline.md`); per-month slice indexes (`_index.json`) live in the month directory; and `memory/episodic/strands.json` maps each strand keyword to its slice paths. Each slice is a directory (`slices/YYYY/MM/DD/HHMM/`) whose `timeline/` holds the shared record (`core.md`) and the agent's own cognitive record (`agent.md`). That is the full extent of the "plumbing." No Postgres, no SQLite, no Pinecone. **Key takeaway: your memory is a directory tree of Markdown files in a git repo. That's it.** ## Can other tools read my memory? Yes. Memory is plain Markdown with YAML frontmatter — no SDK, no API gateway, no vendor lock-in. Previously writes memories. Claude Code reads them. Codex extends them. Any tool that can parse Markdown and read a GitHub repo can read your memory. This portability is a core design goal. You never need to export or migrate data; you just point a different tool at the same repo. ## Does it forget in a long conversation? No — not the way a growing prompt window does. Previously has no chat threads that accumulate indefinitely. Context is assembled from scratch on every turn and stays bounded. Each chat turn builds a fresh system prompt (`assembleSystemPrompt` in `src/app/api/chat/turn-workflow.ts`). The stable blocks come first — the identity constitution and the user card — so the provider's prompt cache can reuse that long prefix across turns. The variable tail follows: a per-turn brief (timestamp, intent, continuity), a recent-timeline block, and the strands menu. The visible chat history sent along is capped by a configurable recent-turns limit (20 by default). Crucially, timeline lines are pointers, not payloads. When a line looks relevant, the agent opens that slice on demand with the `readSliceSummary` / `readSlice` tools instead of carrying months of conversation in the prompt. The archive can grow across months, but what gets loaded into any single request stays window-sized. There is no point where the agent suddenly forgets the beginning of a long exchange, because there is no single long exchange — context is rebuilt every round. ## How is a slice closed? Three triggers, checked in the housekeeping step of every chat turn (`src/app/api/chat/steps.ts`): - **Time silence** — the slice closes after a period of inactivity (15 minutes by default). - **Turn cap** — a safety net that force-closes a marathon session (20 turns by default). - **Context lost** — if the client comes back with no assistant history while the recovered slice has agent turns (a page refresh, a device switch), the old slice is closed and a fresh one opens. Both thresholds are user-configurable in Settings — they live in `memory/user/config.json`: ```json { "slicing": { "maxTurnsPerSlice": 20, "timeSilenceMinutes": 15 } } ``` When a slice closes, its metadata is frozen, the timeline catalog and strand index are updated, and the next message opens a fresh slice. ## Is it production-ready? No. Previously is experimental (v0.8). The README carries an "experimental" status badge and explicitly states it is "not yet ready for personal or production use." It is a personal deployment, not a SaaS product. The author commits to long-term maintenance, but the project is in active early development. Expect rough edges, breaking changes, and features still on the roadmap. ## Do code and memory share a repo? They can. They can also be separate. The deployed app reads and writes the GitHub repo identified by the environment variables `GITHUB_REPO_OWNER` and `GITHUB_REPO_NAME`. Those same env vars can point at a dedicated private memory repo that is distinct from the code repo. The project's own convention (described in `CLAUDE.md`) co-locates code and data in one repo — with `src/` agent-read-only and data directories (`memory/`, `tasks/`, `sessions/`) agent-read-write. But the configuration gives you the choice. A separate memory repo is valid and documented in [Getting Started](/docs/getting-started). ## How much does it cost to run? Two cost centers: - **LLM API usage** — Previously runs on your own API keys. Nine providers are supported (DeepSeek, Anthropic, OpenAI, Moonshot AI, Alibaba, Google, Mistral, xAI, Groq); whichever keys you set determine the available models. The two-tier architecture is cost-motivated: a cheap **worker model** handles housekeeping, recall scans, and card evolution, while the **main model** (your choice) is reserved for the conversation itself. - **Hosting** — Vercel (the deploy button targets Vercel). Vercel's free tier may cover light usage, but sustained use will likely need a paid plan. GitHub repo storage is effectively free. Previously charges no subscription, no per-seat fee, and no usage markup. You supply the infrastructure and the keys; the project is the orchestration layer in between. ## Is my data private? Yes, by design. Memory lives in a GitHub repository you own (a private repo is recommended). Access is controlled by a GitHub fine-grained personal access token scoped to contents read/write on a single repository. There is no platform database that holds your conversations. No third-party server stores your slices or strands. On the HTTP side, mutation endpoints (`POST /api/chat`, `/api/loops`, `/api/episodic/flush`) are same-origin guarded, and non-browser callers need an access key when `ACCESS_SECRET` is set. LLM providers obviously see the prompts you send them — that is the one external data path, and it is inherent to using any hosted model. You own the data outright. Clone it, back it up, move it to another provider — it is just files in a git repo. ## Related - [Memory Model](/docs/memory-model) — how slices, strands, and the user card fit together - [Slices](/docs/slices) — the slice lifecycle and storage layout - [Architecture Overview](/docs/architecture) ---