Skip to main content
Every project has an append-only memory at workspace/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

workspace/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

{
  "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": "assets/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

FieldTypeNullabilityNotes
timestampISO 8601 stringrequiredSet at write via new Date().toISOString() unless a backfill timestamp is passed.
providerenumrequiredfal, elevenlabs, openai, openrouter, vercel, ffmpeg, replicate, other.
endpointstringrequiredThe fully-qualified endpoint string (e.g. kwaivgi/kling-v3.0-pro for OR, music_v1 for ElevenLabs).
kindenumrequiredimage, video, audio, music, voiceover, sfx, text, embed, other.
slotstringoptionalThe asset slot this call targets (e.g. scene-01-bg-image). Persisted so per-slot rollups don’t need to infer from the note.
inputobjectrequiredThe request body passed to the provider. Keep small — prefer URLs over byte blobs.
outputobjectoptional{url?, local?, bytes?}. Set on success.
status"ok" or "error"requiredDrives cost rollups (status: "ok" only).
errorstringoptionalSet only when status: "error". Free-form message.
latency_msnumberoptionalWall-clock duration of the call. Used by the daemon’s throughput metrics.
cost_usdnumberoptionalBest-effort estimate. May be empty for providers without a cost contract.
request_idstringoptionalProvider-side id (fal queue id, ElevenLabs generation id, OR request id). Used for support tickets.
notestringoptionalFree-form human tag. Conventions: "clip-03 v2 hand crumples sample", "--variants 4 A/B sweep".

Writer signature

// 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

{"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

FieldTypeNullabilityNotes
timestampISO 8601 stringrequiredSet at write.
textstringrequiredThe user’s utterance verbatim (or the override reason for structured stages).
stagestringoptionalConventional values: brief, scenario-feedback, regeneration-request, no-ref-consent. Free-form.
notestringoptionalStructural context. For no-ref-consent rows, the conventional shape is slot=<slot-id>.
When the user overrides the reference-required gate (AGENTS invariant #3) via --no-ref-consent "<reason>", cli/commands/generate.ts → maybeLogNoRefConsent() writes:
{
  "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

{"timestamp":"2026-05-12T10:18:01.741Z","kind":"photo","source":"/Users/me/books/headphone-01.webp","dest":"assets/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

FieldTypeNullabilityNotes
timestampISO 8601 stringrequiredSet at write.
kindenumrequiredscreenshot, photo, video, audio, doc, ref-url, other.
sourcestringrequiredOriginal path or URL from the user. Absolute paths are common — ~/Downloads/....
deststringoptionalStored path inside the project (relative to workspace/projects/<id>/) when the file was copied in. Empty for refs the user kept in-place.
purposestringoptionalFree-form role label. Conventions: character-ref, product-ref, brand-screenshot, style-ref, voice-ref.
notestringoptionalAuthor’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:
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:
# total spend on a project
jq -s '[.[] | select(.status=="ok") | .cost_usd // 0] | add' \
  workspace/projects/fruit-drama-001/logs/generations.jsonl

# every model call that failed
jq -c 'select(.status=="error") | {endpoint, error, ts: .timestamp}' \
  workspace/projects/fruit-drama-001/logs/generations.jsonl
The CLI verbs are the preferred path:
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.