Skip to main content
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

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

Fields

Writer signature

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

Fields

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

Fields

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:
For ad-hoc work, raw jq is fine:
The CLI verbs are the preferred path:

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.