---
title: "Generations log and manifest"
description: "Every model call is appended to generations.jsonl. Every produced file is a versioned slot in asset-manifest.json."
---

Every project keeps three append-only JSONL logs under `.ralphy/workspaces/<ws>/projects/<id>/logs/` and one pointer table at `.ralphy/workspaces/<ws>/projects/<id>/asset-manifest.json`. The logs are the audit trail — every model call, every user prompt, every uploaded ref. The manifest is the slot pointer table — which file is the current version of `scene-01-bg`, which is the previous. Together they enforce AGENTS.md invariant #13: append-only on generations, never delete or overwrite user / agent-produced artifacts without explicit user request.

The schemas and the writer live in [`cli/lib/gen-log.ts`](https://github.com/alecs5am/ralphy/blob/main/cli/lib/gen-log.ts).

## The three logs

```
.ralphy/workspaces/<ws>/projects/<id>/logs/
├── generations.jsonl     # every model call (fal / ElevenLabs / OpenRouter / ffmpeg)
├── user-prompts.jsonl    # every user-facing prompt or request
└── user-assets.jsonl     # every user-uploaded reference (screenshot, photo, doc, URL)
```

All three are append-only by definition. Never truncate, rewrite, or filter in place — read-and-rewrite-to-tidy is a defect. Failed and rejected generations stay on disk until the user explicitly purges them.

## `generations.jsonl` schema

One JSON object per line. Fields from [`cli/lib/gen-log.ts`](https://github.com/alecs5am/ralphy/blob/main/cli/lib/gen-log.ts):

```ts
type GenerationEntry = {
  timestamp: string;            // ISO 8601, set at append time
  provider: Provider;           // "fal" | "elevenlabs" | "openai" | "openrouter"
                                // | "vercel" | "ffmpeg" | "replicate" | "other"
  endpoint: string;             // e.g. "fal-ai/kling-video/v3/pro/image-to-video"
  kind: "image" | "video" | "audio" | "music" | "voiceover"
      | "sfx" | "text" | "embed" | "other";
  slot?: string;                // e.g. "scene-01-bg-image"
  input: Record<string, unknown>;
  output?: {
    url?: string;               // hosted URL (preferred over byte blobs)
    local?: string;             // local path inside the project
    bytes?: number;
  };
  status: "ok" | "error";
  error?: string;
  latency_ms?: number;
  cost_usd?: number;            // best-effort estimate
  request_id?: string;          // fal queue id, elevenlabs id, etc.
  note?: string;                // free-form, e.g. "clip-03 v2 hand crumples sample"
};
```

A typical entry:

```json
{
  "timestamp": "2026-05-20T10:42:18.901Z",
  "provider": "fal",
  "endpoint": "fal-ai/kling-video/v3/pro/image-to-video",
  "kind": "video",
  "slot": "scene-03-clip",
  "input": {
    "prompt": "Megan turns to camera, slight smile, raises mug.",
    "image_url": ".ralphy/workspaces/<ws>/projects/coffee-shop-001/assets/scene-03-bg.png",
    "duration": 5,
    "audio": true
  },
  "output": {
    "url": "https://v3.fal.media/files/.../output.mp4",
    "local": ".ralphy/workspaces/<ws>/projects/coffee-shop-001/assets/scene-03-clip.mp4",
    "bytes": 4218492
  },
  "status": "ok",
  "latency_ms": 78421,
  "cost_usd": 0.96,
  "request_id": "fal-queue-abc123",
  "note": "v2 after v1 had identity drift on Megan"
}
```

The `slot` field exists because per-slot cost rollups need it. Two postmortems (noski, venom) had to grep `.locked-vN` filenames to reconstruct slot-level spend because `slot` wasn't there yet; it is now part of the schema.

The `note` field is for the agent's human-readable tag — what version this is, what changed since the previous version, why the user asked for the regen. Always populate it.

## `user-prompts.jsonl` schema

```ts
type UserPromptEntry = {
  timestamp: string;
  text: string;
  stage?: string;     // "brief" | "scenario-feedback"
                      // | "regeneration-request" | "no-ref-consent" | ...
  note?: string;
};
```

A typical entry:

```json
{
  "timestamp": "2026-05-20T10:30:01.123Z",
  "text": "make the second scene more cinematic, less product-shoot",
  "stage": "scenario-feedback",
  "note": "user response to v1 scenario draft"
}
```

The `stage` field is how the postmortem reconstructs the chronology — intake → scenario draft → feedback → regen → final sign-off. The agent writes one entry per substantive user prompt; trivial back-channel ("yes go ahead") does not need to land here unless it changes direction.

The `no-ref-consent` stage is reserved — see [`/concepts/references`](/concepts/references). Every `--no-ref-consent "<reason>"` override appends one entry with the reason text.

## `user-assets.jsonl` schema

```ts
type UserAssetEntry = {
  timestamp: string;
  kind: "screenshot" | "photo" | "video" | "audio" | "doc" | "ref-url" | "other";
  source: string;     // original path or URL from user
  dest?: string;      // stored path inside the project, if copied in
  purpose?: string;   // "character-ref" | "product-ref"
                      // | "brand-screenshot" | ...
  note?: string;
};
```

A typical entry:

```json
{
  "timestamp": "2026-05-20T10:15:42.500Z",
  "kind": "ref-url",
  "source": "https://www.tiktok.com/@user/video/7321...",
  "dest": ".ralphy/references/tiktok-7321/source.mp4",
  "purpose": "style-ref",
  "note": "the user said 'I want it like this' and dropped this URL"
}
```

Asset entries log *what the user gave Ralphy* — not what Ralphy produced. They are how the agent answers "what references did the user attach to this project."

## The append-only contract

The rules from AGENTS.md invariant #13, applied to the three logs:

- **Never truncate.** No "drop entries older than X" rewrites. The file grows monotonically.
- **Never filter in place.** If you want a filtered view, write a new file under `logs/views/<name>.jsonl` or pipe through `jq`. Do not edit the source file.
- **Never reorder.** Entries are in append order. Even if a `timestamp` is backfilled for an old call, the file order reflects when the entry was written.
- **Append-only is bidirectional.** A failed call also lands an entry (with `status: "error"` and `error: "..."`). The next agent looking at the log sees the failed attempts and knows what was tried.

The writer `appendJsonl(file, entry)` does exactly this — `fs.mkdir -p` then `fs.appendFile`. No rewrite path exists in the codebase.

## `asset-manifest.json` schema

The manifest is *not* a log. It is a pointer table: which file on disk is the current version of which slot, plus per-slot metadata.

```json
{
  "version": 1,
  "project_id": "coffee-shop-001",
  "slots": {
    "scene-01-bg": {
      "kind": "image",
      "current": "artifacts/images/scene-01-bg.png",
      "versions": [
        { "version": 1, "file": "artifacts/images/scene-01-bg.v1.png", "created": "2026-05-20T10:32:00Z" },
        { "version": 2, "file": "artifacts/images/scene-01-bg.png",    "created": "2026-05-20T10:45:30Z" }
      ],
      "promoted": null
    },
    "scene-01-clip": {
      "kind": "video",
      "current": "artifacts/videos/scene-01-clip.mp4",
      "versions": [
        { "version": 1, "file": "artifacts/videos/scene-01-clip.mp4", "created": "2026-05-20T10:48:12Z" }
      ],
      "promoted": null
    },
    "voiceover-en": {
      "kind": "voiceover",
      "current": "artifacts/voiceover/voiceover-en.mp3",
      "versions": [
        { "version": 1, "file": "artifacts/voiceover/voiceover-en.mp3", "created": "2026-05-20T10:52:00Z" }
      ],
      "promoted": null
    }
  }
}
```

The manifest is updated by `ralphy generate` after every produced file. Three operations:

- **Add a new slot.** First generation for a slot — manifest grows by one entry.
- **Add a new version to a slot.** Regen — existing file at `current` is archived to `.v{N}.<ext>`, manifest's `versions` array grows, `current` points at the new file.
- **Promote a version.** Explicit user say-so — `ralphy project promote <slot> v1` swaps `current` to point at `.v1.<ext>` and archives the previous-current.

## Versioning: `.v2`, `.v3`, ...

Since commit `753d2f7` (2026-05-19), `ralphy generate {image,video,music,voiceover,sfx}` auto-archives the existing slot file before writing the new one. The rule:

1. Slot has no file → write `<slot>.<ext>`.
2. Slot has `<slot>.<ext>` → archive to `<slot>.v1.<ext>`, write the new file as `<slot>.<ext>`.
3. Slot has `<slot>.<ext>` plus `<slot>.v1.<ext>` → archive current to `<slot>.v2.<ext>`, write the new file as `<slot>.<ext>`.
4. And so on.

The *unversioned* filename is always the *current* version. The `.v1`, `.v2`, ... are the previous versions. The manifest's `current` field also points at the unversioned filename.

`--force-overwrite` opts into legacy destructive behavior — write directly, skip the archive. Pass it only when the user explicitly asks ("just overwrite, I do not need the old one"). The default is safe.

## The promote workflow

The user previewed v2, did not like it, wants v1 back as the current version:

```bash
ralphy project promote coffee-shop-001 scene-01-bg v1
# Archives current (v2's file) to scene-01-bg.v3.png, restores v1 to scene-01-bg.png.
# Manifest's current now points at the restored file.
# Manifest's versions array gains a new {version: 3, ...} entry for the demoted v2.
```

Promotion is *additive* — it does not delete the demoted version. The demoted file is archived as the next version number. The append-only contract holds: every file that ever existed is still on disk.

## Why this is structured this way

Four downstream uses rely on the log + manifest contract.

**Postmortem mining.** The `/postmortem` skill reads `generations.jsonl`, `user-prompts.jsonl`, and the manifest to write `02-lessons.md`, `03-bugs.md`, `04-models-cost.md`. The lessons file is what the *next* project's agent reads at session start. The mining is only possible because nothing was truncated.

**Cross-session memory.** When you reopen a project after a week, the agent reads the logs to reconstruct what happened. A `note` field on every generation that says "v2 after v1 had identity drift on Megan" tells the next agent the lineage immediately.

**Cost rollup.** `ralphy project cost <id>` sums `cost_usd` across `generations.jsonl`. The per-slot breakdown depends on the `slot` field being present. The per-stage breakdown (image vs. video vs. music) depends on `kind`.

**Audit and debug.** When the user asks "why did this scene look weird," the agent reads the generation entries for that slot, sees the prompts and the model picks, and can diff v1 against v2.

## The convenience writer

`loggedFetch()` in [`cli/lib/gen-log.ts`](https://github.com/alecs5am/ralphy/blob/main/cli/lib/gen-log.ts) wraps `fetch()` with auto-timing and auto-logging. The CLI uses it for every provider call:

```ts
const resp = await loggedFetch({
  projectId: "coffee-shop-001",
  provider: "fal",
  endpoint: "fal-ai/kling-video/v3/pro/image-to-video",
  kind: "video",
  input: body,
  note: "scene-03 v2",
}, url, { method, headers, body: JSON.stringify(body) });
```

The wrapper appends a generation entry with `status`, `latency_ms`, `request_id`, and (when the response is JSON) the output URL — automatically, even on error. The verb-level code does not need to write logs by hand for the common case.

## Useful reading verbs

```bash
ralphy project log <id>                       # raw JSONL, pretty-printed
ralphy project log <id> --kind video          # filter by kind
ralphy project log <id> --status error        # only failures
ralphy project timeline <id>                  # chronological view across all 3 logs
ralphy project cost <id>                      # cost rollup
ralphy project cost <id> --by-slot            # per-slot breakdown
```

## Related

- [`cli/lib/gen-log.ts`](https://github.com/alecs5am/ralphy/blob/main/cli/lib/gen-log.ts) — the writer source
- [Projects](/concepts/projects) — the per-project layout
- [References](/concepts/references) — `--no-ref-consent` lands in `user-prompts.jsonl`
- [`/advanced/memory-schemas`](/advanced/memory-schemas) — the full JSONL field reference
- [AGENTS.md hard invariant #13](https://github.com/alecs5am/ralphy/blob/main/AGENTS.md) — the append-only rule
