workspace/projects/<id>/logs/ and one pointer table at workspace/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.
The three logs
generations.jsonl schema
One JSON object per line. Fields from cli/lib/gen-log.ts:
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
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. Every --no-ref-consent "<reason>" override appends one entry with the reason text.
user-assets.jsonl schema
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>.jsonlor pipe throughjq. Do not edit the source file. - Never reorder. Entries are in append order. Even if a
timestampis 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"anderror: "..."). The next agent looking at the log sees the failed attempts and knows what was tried.
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.
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
currentis archived to.v{N}.<ext>, manifest’sversionsarray grows,currentpoints at the new file. - Promote a version. Explicit user say-so —
ralphy project promote <slot> v1swapscurrentto 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:
- Slot has no file → write
<slot>.<ext>. - Slot has
<slot>.<ext>→ archive to<slot>.v1.<ext>, write the new file as<slot>.<ext>. - Slot has
<slot>.<ext>plus<slot>.v1.<ext>→ archive current to<slot>.v2.<ext>, write the new file as<slot>.<ext>. - And so on.
.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: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 wraps fetch() with auto-timing and auto-logging. The CLI uses it for every provider call:
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
Related
cli/lib/gen-log.ts— the writer source- Projects — the per-project layout
- References —
--no-ref-consentlands inuser-prompts.jsonl /advanced/memory-schemas— the full JSONL field reference- AGENTS.md hard invariant #13 — the append-only rule