---
title: "Memory schemas"
description: "The three JSONL logs under a project's logs directory — append-only fields, lifecycle, and read patterns."
---

Every project has an append-only memory at `.ralphy/workspaces/<ws>/projects/<id>/logs/`. Three JSONL files: every model call, every user prompt, every uploaded reference. The writers live in `cli/lib/gen-log.ts`. This page is the schema reference: field-by-field shape, nullability, examples, and the append-only contract.

## File layout

```
.ralphy/workspaces/<ws>/projects/<id>/logs/
  generations.jsonl   # every model call (one line per call)
  user-prompts.jsonl  # chronological user prompts and overrides
  user-assets.jsonl   # uploaded references (screenshots, photos, ref-urls)
```

Each file is line-delimited JSON. One JSON object per line. No header line, no trailer.

## The append-only contract

AGENTS invariant #13 makes this a hard rule: **the logs are append-only**. Never truncate, rewrite, or filter them in place. Read-and-rewrite to "tidy" is a defect.

Concretely:

- `logGeneration()`, `logUserPrompt()`, `logUserAsset()` open the file with `fs.appendFile()`. Never `fs.writeFile()`.
- Failed and rejected generations stay on disk. `status: "error"` rows are how cross-session reasoning works.
- If the user explicitly asks to clear a project's logs, use `ralphy project delete <id>` (registry-aware) — never patch the files.

The logs are the cost rollup, the reasoning trail, and the postmortem source. Truncating them destroys all three.

## generations.jsonl

One line per model call. Written by `cli/lib/gen-log.ts → logGeneration()`. Read by `ralphy project log <id>`, `ralphy project timeline <id>`, and the cost-rollup verbs.

### Example line

```json
{
  "timestamp": "2026-05-11T15:49:57.306Z",
  "provider": "openrouter",
  "endpoint": "google/gemini-3-pro-image-preview",
  "kind": "image",
  "slot": "scene-01-bg-image",
  "input": { "prompt": "...", "size": "1080x1920", "refs": [] },
  "output": { "url": "https://...", "local": "artifacts/images/scene-01-bg-image.png" },
  "status": "ok",
  "latency_ms": 8430,
  "cost_usd": 0.15,
  "request_id": "or-req-abc123",
  "note": "v2 — softened the cheek-kiss to a forehead-kiss per user feedback"
}
```

### Fields

| Field | Type | Nullability | Notes |
|---|---|---|---|
| `timestamp` | ISO 8601 string | required | Set at write via `new Date().toISOString()` unless a backfill timestamp is passed. |
| `provider` | enum | required | `fal`, `elevenlabs`, `openai`, `openrouter`, `vercel`, `ffmpeg`, `replicate`, `other`. |
| `endpoint` | string | required | The fully-qualified endpoint string (e.g. `kwaivgi/kling-v3.0-pro` for OR, `music_v1` for ElevenLabs). |
| `kind` | enum | required | `image`, `video`, `audio`, `music`, `voiceover`, `sfx`, `text`, `embed`, `other`. |
| `slot` | string | optional | The asset slot this call targets (e.g. `scene-01-bg-image`). Persisted so per-slot rollups don't need to infer from the note. |
| `input` | object | required | The request body passed to the provider. Keep small — prefer URLs over byte blobs. |
| `output` | object | optional | `{url?, local?, bytes?}`. Set on success. |
| `status` | `"ok"` or `"error"` | required | Drives cost rollups (`status: "ok"` only). |
| `error` | string | optional | Set only when `status: "error"`. Free-form message. |
| `latency_ms` | number | optional | Wall-clock duration of the call. Used by the daemon's throughput metrics. |
| `cost_usd` | number | optional | Best-effort estimate. May be empty for providers without a cost contract. |
| `request_id` | string | optional | Provider-side id (fal queue id, ElevenLabs generation id, OR request id). Used for support tickets. |
| `note` | string | optional | Free-form human tag. Conventions: `"clip-03 v2 hand crumples sample"`, `"--variants 4 A/B sweep"`. |

### Writer signature

```ts
// cli/lib/gen-log.ts
export async function logGeneration(
  projectId: string,
  entry: Omit<GenerationEntry, "timestamp"> & { timestamp?: string }
): Promise<void>;
```

The `timestamp` is auto-filled from `Date.now()` if you don't pass one. Pass an explicit timestamp only when backfilling.

### Lifecycle

- Written by `ralphy generate {image,video,voiceover,music,sfx,captions}` automatically. Skill code never writes directly.
- Written by `cli/lib/providers/media.ts → loggedFetch()` for any provider call routed through that helper.
- Read by `ralphy project log <id> --kind <generations|user-prompts|user-assets>` and surfaces under the `working/logs-and-costs` doc page.

## user-prompts.jsonl

Chronological user prompts, plus structured-override entries (e.g. `--no-ref-consent` reasons). Written by `logUserPrompt()`.

### Example lines

```json
{"timestamp":"2026-05-07T12:16:46.930Z","text":"Soft-soap fruit drama about a peach husband and a peach wife. Native EN VO via Veo 3.","stage":"brief"}
{"timestamp":"2026-05-07T14:33:12.001Z","text":"Make scene-03 longer, more emotional","stage":"scenario-feedback"}
{"timestamp":"2026-05-07T15:02:44.118Z","text":"explicitly accepting risk on Old Spice reference","stage":"no-ref-consent","note":"slot=scene-02-vid"}
```

### Fields

| Field | Type | Nullability | Notes |
|---|---|---|---|
| `timestamp` | ISO 8601 string | required | Set at write. |
| `text` | string | required | The user's utterance verbatim (or the override reason for structured stages). |
| `stage` | string | optional | Conventional values: `brief`, `scenario-feedback`, `regeneration-request`, `no-ref-consent`. Free-form. |
| `note` | string | optional | Structural context. For `no-ref-consent` rows, the conventional shape is `slot=<slot-id>`. |

### The `no-ref-consent` row

When the user overrides the reference-required gate (AGENTS invariant #3) via `--no-ref-consent "<reason>"`, `cli/commands/generate.ts → maybeLogNoRefConsent()` writes:

```json
{
  "timestamp": "...",
  "text": "<reason from the flag>",
  "stage": "no-ref-consent",
  "note": "slot=<slot-id>"
}
```

This is how future sessions see that the user deliberately accepted the quality hit on a named-entity generation. The agent reads `stage: "no-ref-consent"` rows before refusing a similar request again.

### Lifecycle

- Written by the agent / playbook on every user-driven beat (intake answer, scenario feedback, regen ask).
- Written automatically by `--no-ref-consent` and similar override flags.
- Read by `ralphy project timeline <id>` to reconstruct the user-flow.

## user-assets.jsonl

References the user supplied — local files, screenshots, ref URLs. Written by `logUserAsset()`.

### Example lines

```json
{"timestamp":"2026-05-12T10:18:01.741Z","kind":"photo","source":"/Users/me/books/headphone-01.webp","dest":"artifacts/refs/headphones/headphone-01.webp","purpose":"product-ref","note":"Nothing Headphone (1) reference pack — angle 01"}
{"timestamp":"2026-05-18T12:18:10.041Z","kind":"photo","source":"/Users/me/Downloads/photo.jpeg","purpose":"character-ref","note":"User selfie — DO NOT view content; pass-through only."}
{"timestamp":"2026-04-22T08:09:02.846Z","kind":"screenshot","source":"screenshot-cap-front.png","purpose":"product-ref","note":"nobody.solutions METAL bucket hat"}
```

### Fields

| Field | Type | Nullability | Notes |
|---|---|---|---|
| `timestamp` | ISO 8601 string | required | Set at write. |
| `kind` | enum | required | `screenshot`, `photo`, `video`, `audio`, `doc`, `ref-url`, `other`. |
| `source` | string | required | Original path or URL from the user. Absolute paths are common — `~/Downloads/...`. |
| `dest` | string | optional | Stored path inside the project (relative to `.ralphy/workspaces/<ws>/projects/<id>/`) when the file was copied in. Empty for refs the user kept in-place. |
| `purpose` | string | optional | Free-form role label. Conventions: `character-ref`, `product-ref`, `brand-screenshot`, `style-ref`, `voice-ref`. |
| `note` | string | optional | Author's note. Privacy reminders, version tags, source attribution. |

### Lifecycle

- Written when the user drops a file into the chat, runs `ralphy ref add <url-or-path>`, or attaches a reference via the intake protocol.
- Read by the art-director playbook to assemble the `--ref` list for `ralphy generate image / video`.
- Read by `ralphy project show <id>` for the references panel.

## Read patterns

`cli/lib/gen-log.ts → readLog<T>(projectId, name)` returns the parsed array:

```ts
const gens = await readLog<GenerationEntry>("fruit-drama-001", "generations");
const prompts = await readLog<UserPromptEntry>("fruit-drama-001", "user-prompts");
const assets = await readLog<UserAssetEntry>("fruit-drama-001", "user-assets");
```

For ad-hoc work, raw `jq` is fine:

```bash
# total spend on a project
jq -s '[.[] | select(.status=="ok") | .cost_usd // 0] | add' \
  .ralphy/workspaces/<ws>/projects/fruit-drama-001/logs/generations.jsonl

# every model call that failed
jq -c 'select(.status=="error") | {endpoint, error, ts: .timestamp}' \
  .ralphy/workspaces/<ws>/projects/fruit-drama-001/logs/generations.jsonl
```

The CLI verbs are the preferred path:

```bash
ralphy project log fruit-drama-001 --kind generations
ralphy project timeline fruit-drama-001
ralphy project log-prompt fruit-drama-001 --text "make scene-03 shorter"
ralphy project log-asset fruit-drama-001 --source ./ref.png --purpose product-ref
```

## Stability commitment

The fields above do not change without a migration. Specifically:

- **Renaming a field is a breaking change.** Add a new field, deprecate the old in a separate PR after one major release.
- **Tightening a field's type is a breaking change.** A field that was `string?` cannot become required without a migration script that backfills.
- **Adding a new optional field is safe.** Readers must tolerate unknown fields.
- **Adding a new value to an enum** (`provider`, `kind`, `stage`, `purpose`) is safe — readers must tolerate unknown values.

The writer types live in `cli/lib/gen-log.ts`. Open a PR that touches the type to discuss any breaking change.

## Why JSONL, not a single JSON document

Append-only is the constraint. A single JSON document would force a read-and-rewrite on every event; the JSONL shape allows naive append without parsing. The trade-off is that consumers must handle line-by-line parsing, which is fine — every read patten above does exactly that.

## Related

- [Concepts: generation log](/concepts/generation-log) — what the log is *for*, narrated.
- [Working: logs and costs](/working/logs-and-costs) — how to use the data day-to-day.
- [Asset manifest](/advanced/asset-manifest) — the per-slot pointer table that sits next to the logs.
- [`cli/lib/gen-log.ts`](https://github.com/alecs5am/ralphy/blob/main/cli/lib/gen-log.ts) — writer source.
- [`AGENTS.md`](https://github.com/alecs5am/ralphy/blob/main/AGENTS.md) — invariant #13 (append-only).
