ouroboros/docs/PERSISTENCE.md
Ouroboros 5507134f71 Merge remote-tracking branch 'origin/ouroboros' (55ad78213) into ouroboros-agent/presence-resilience
Semantic integration of #1299 (memory history visibility) and #1207 (reasoning-effort descriptor + Z.ai provider). One textual conflict: tests/test_persistence_inventory.py EXPECTED_SCAN_PATHS resolved 295 (+1 Presence quarantine members, -1 removed memory journal rewrite path); verified by test_persistence_inventory + reference-book/inventory/domain checks (48 passed).
2026-09-25 23:16:14 +03:00

48 KiB
Raw Blame History

PERSISTENCE.md — durable data-plane inventory

Every durable entity under the runtime data root (DATA_DIR, default ~/Ouroboros/data/), with its schema, migration path, retention and reset semantics (what deleting the entity while the server is stopped does). These contracts are local to each entity; there is no generic persistence framework.

tests/test_persistence_inventory.py scans data-path constructors in ouroboros/, supervisor/, server.py and launcher.py and requires every scanned data-relative path to be covered by a row here (count-anchored both ways).

Shared idioms (the vocabulary the rows use)

  • Stamp — the opt-in _schema_version key from ouroboros/contracts/schema_versions.py (ABI-2): missing/invalid reads as legacy 0; existing files are not retrofitted. Some pre-ABI-2 entities carry their own spelling (schema_version, schema, state_version) — noted per row; new stamps use the shared key.
  • GC retention — ouroboros/retention.py: one owner knob OUROBOROS_GC_RETENTION_DAYS (default 7, clamped 1–365; legacy per-subsystem keys migrate). Governs subagent worktrees, headless/task drives, task trees, service logs, consumed schedule receipts, confirmed capability probes, delegate recovery/supervision sweeps, code_intel reconcile-failed prunes, and agent media. Memory journals retain full new rows independently of this knob.
  • Rotation — supervisor/state.py::rotate_jsonl_log_if_needed: >800 KB → atomic rename to archive/<prefix>_<ts>.jsonl under the append lock. Applied on the supervisor tick to chat.jsonl, progress.jsonl, events.jsonl, tools.jsonl, supervisor.jsonl, task_reflections.jsonl. Chain readers enumerate archive/<stem>_*.jsonl name-sorted (chronological by construction); utils.jsonl_chain_handles is the rotation-race-safe traversal (open-live-first + inode dedup).
  • archive/ is durable history, never GC'd (BIBLE P1): readers backfill from rotated segments; no retention sweep touches archive/ and none may be added.
  • Atomic writes — atomic_write_json/atomic_write_text (tmp+rename) and update_json_locked (sidecar <file>.lock); JSONL appends go through append_jsonl (O_APPEND + sidecar lock) unless noted.

1. Root files

Path Writer Format schema_version Retention Reset
settings.json ouroboros/config.py save_settings (lock settings.json.lock, integrity guard) JSON env-key map none — not needed: migration is per-key inside load_settings (legacy keys migrate on read); external sha256 pin OUROBOROS_SETTINGS_SHA256 makes it immutable when set fixed-size overwrite recreated from defaults; ALL owner secrets/modes lost — never delete casually
.ouroboros_isolated_benchmark EXTERNAL writer — the benchmark launchers (devtools/benchmarks/evolve_smoke.py, devtools/benchmarks/editbench/run_editbench.py, devtools/benchmarks/cybergym/cybergym_server.py) stamp it into their throwaway data root; the runtime only READS it (supervisor/state.py rotate suppression, ouroboros/agent_startup_checks.py) one-line marker text none — not needed: presence IS the fact write-once per benchmark drive; never GC'd the drive stops declaring itself synthetic: JSONL rotation and the benchmark-only startup carve-outs switch back to live-root behaviour on a throwaway root
settings.json.lock, *.lock sidecars, locks/** ouroboros/platform_layer.py lock family (+ supervisor/state.py, supervisor/update_merge.py, ouroboros/skill_lifecycle_queue.py) O_EXCL lockfiles (unlinked on release) or flock files (persistent); plus one transient kernel-lock capability probe file per lock directory (.kernel-lock-probe.<pid>.<ns>, created, locked and unlinked once per process by kernel_file_locks_enforced) none — not needed self-healing (mtime/pid staleness) — except a lock whose owner died and whose pid was REUSED by a live process: it reads as alive whoever owns the pid (kill(0) succeeds for a same-uid impostor and answers EPERM for another user's — both mean alive), is never reclaimed by age while the impostor lives, and needs a hand repair meanwhile zero durable state; deleting while stopped is a no-op

2. state/ — singletons (fixed-size overwrite)

Path Writer schema_version Retention Reset
state/state.json, state/state.last_good.json supervisor/state.py (lock locks/state.lock) _schema_version: 1 (ABI-2, stamp-on-write) overwrite, totals-only cost projection; legacy keys popped on load falls back to last_good, then mints defaults; loses owner binding, spend counters, session id
state/queue_snapshot.json supervisor/queue_snapshot.py _schema_version: 1 (ABI-2) overwrite; self-expires (max_age 900 s) absent = 0 restored; queued-unstarted tasks silently dropped
state/direct_roots.json supervisor/direct_roots.py — the main loop's off-lock projection of live direct-chat turns, written beside the snapshot on every tick (publish_direct_roots, server.py); queue init hands the previous process's rows to snapshot restore, which fences them, and empties the file in the same step (take_direct_roots, supervisor/queue.py) so a dead process's turns never outlive it none — not needed: the whole file is re-derived every tick, and an unreadable one reads as no direct roots overwrite; the rows are whatever the actor registry holds this tick, and a turn mid-admission (its actor lock only ever TRIED) is skipped behind ONE aggregate incomplete fact rather than blocking the loop absent/empty = no live direct-chat roots are known: a task's peer roster (ouroboros/peer_roster.py) lists only the pooled roots from state/queue_snapshot.json until the next tick rewrites it
state/advisory_review.json ouroboros/review_state.py (lock locks/advisory_review.lock) own pre-ABI-2 spelling state_version: 3 (+duplicate schema_version) — kept; _schema_version deliberately avoids this key bounded on write: runs 10, attempts 50, debts 50; open_obligations coalesced recreated empty; recorded blocking obligations forgiven, commit gate demands fresh review (fail-closed)
state/scheduled_tasks.json supervisor/queue_schedules.py schema_version: 1 — authored at the write seam (_write_scheduled_tasks); legacy files gain it on their next write consumed once receipts age out past GC retention on the scheduler tick (prune_consumed_once_records); 2 MB WARN = prune broken or live set huge owner cron/once schedules lost; skill-manifest schedules resync automatically
state/terminal_deliveries.json supervisor/terminal_delivery.py own schema_version: 2 bounded: delivered 512, pending 64, replays 5 dedupe + owed-outbox lost: possible double- or never-delivery of one buffered terminal answer
state/cancel_intents.json ouroboros/cancel_intents.py own schema_version: 1 self-draining (settled rows leave) in-flight cancels lost: cancelled-unsettled task revives as pending; forensics survive in supervisor.jsonl
state/update_letter.json ouroboros/update_letter.py (refresh_after_check — the ONE writer, synchronous inside a FETCHING update check: boot and the Updates panel's check button) own record shape keyed by the update range (base_sha, target_sha, channel, target ref) — no _schema_version overwrite per check; the letter is never deleted after the update lands (it outlives its range; project_letter reads applied/other by SHA equality + the recorded target_in_head fact) absent = no letter: the Updates panel and the agent's Runtime context (official_update_projection) show the check's status alone; the next FETCHING check rewrites it
state/capability_evidence.json ouroboros/capability_evidence.py none — accepted (self-healing cache; TTLs on read) expired probe keys drop at the write seam: failed/unprobeable past their read TTL, confirmed past GC retention (blip-keep evidence survives inside retention); owner acks never expire recreated; every reviewer window resolves unknown (sends are sized by the full-window assumption and re-probed), owner acks must be re-given; no review authority depends on it
state/evolution_campaign.json supervisor/evolution_lifecycle.py (CAS under state.lock) own schema_version: 1 (campaign) / 2 (active_transaction); the active transaction carries the pre-commit commit_intent (reviewed tree + parents) written BEFORE git commit, which boot recovery turns into the commit_receipt a crash never wrote bounded histories (50) in-flight self-modification transaction unabsorbable; anti-repeat fingerprints lost
state/evolution_metrics_cache.json ouroboros/utils.py own schema: 1 (strictly validated) one point per git tag, no prune — accepted (derived cache) pure cache; recomputed from git
state/projects.json, state/project_task_bindings.json ouroboros/projects_registry.py (sidecar locks) _schema_version: 2 / 1 (ABI-2) never age-pruned (owner curates); deletes are durable tombstones tombstones live here: losing it can resurrect deleted project rooms (marker unlink mitigates)
state/ui_preferences.json ouroboros/gateway/ui_preferences.py none — not needed (defaults contract + strict unknown-key rejection; retired keys dropped on write) bounded (widget_order 200, cursors 1000) cosmetic; defaults restored
state/server_port ouroboros/server_entrypoint.py none — not needed (1-line transport fact) overwrite; pre-start unlink readers fall back to default port
state/server_port.bindings.json ouroboros/server_process.py (record_service_binding, clear_service_binding, shared locked update) no schema version; exact service keys and measured process identity validated on read at most main, host_service and local_model; overwrite per observed binding, exact-identity removal on close current endpoint facts are lost; browser falls back to existing port/custody facts and refuses known endpoints with unconfirmed identity
state/server_process.json launcher.py none — PID/path-bound process facts; optional server_host_source distinguishes parent env from saved Settings, absent on old launchers means unknown one-shot, self-deleting stray-server reap skipped once; bind-source disclosure becomes unknown, never inferred from value equality
state/auth_secret.key ouroboros/server_auth.py none — not needed write-once regenerated; one forced owner re-login (accepted)
state/worker_pids.json supervisor/worker_pool_lifecycle.py none — not needed (legacy reap path; SSOT is process_ledger) overwrite ≤ pool size orphan reap falls to the custody reaper
state/extension_companions.json ouroboros/extension_companion.py none — not needed (runtime snapshot) bounded by live companions launcher loses force-kill map; leaked child processes possible
state/pending_restart_verify.json supervisor/evolution_lifecycle.py (write_pending_restart_marker, the one writer — the supervisor's evolution restart and the agent's restart tool in ouroboros/tools/control_runtime.py both go through it) none — not needed (one-shot marker, consumed by rename to .claimed.<pid>.json) one-shot evolution restart refused fail-closed without its receipt
state/advisory_overrides.json ouroboros/tools/review.py, ouroboros/tools/claude_advisory_review.py none — not needed (visibility counter; each bypass also durably in events.jsonl) bounded (recent 10) bypass counter lost; no gate reads it
state/scope_delivery_migration.json ouroboros/tools/review_admission.py (disclose_scope_delivery_migration, atomic write) none — not needed: presence IS the fact (the notice was given) write-once per install the one-time review_scope_delivery_migrated notice — a stored bare api scope row now runs a retrieving inspection episode — is emitted again on the next review; no review behaviour depends on the file
state/reviewer_slot_last_execution.json ouroboros/reviewer_slot_config.py none — not needed (disclosure, never enforcement) capped 64, oldest evicted UI disclosure line lost until next review
state/usage_import_watermark.json ouroboros/usage_legacy_import.py none — completion boolean is the contract write-once legacy import re-runs, fingerprint-deduped against the ledger (safe)
state/presence_bindings.json ouroboros/presence_bindings.py (update_json_locked, strict existing dict) own schema_version: 1 — a mismatched version or a non-dict bindings map is a typed refusal, never a silent migration overwrite; bounded by the owner-created links every owner link from a transport room to a behavior skill is lost; presence stops routing those rooms until the links are re-created
state/presence_turn_gate/last-<sha256>.json ouroboros/presence_runner.py (atomic_write_json, one file per conversation key, written under the conversation lock at the end of an executed turn) flat JSON: conversation_key, task_id, outcome, message, transport_sends, work_ref, finished_at, delivery; a rebuildable projection, task results and delivery receipts stay canonical overwrite per executed turn; a cached replay writes only to rebuild a pointer that never landed or names an older turn; the write is best effort (a failure logs a warning, the next replay retries); no age GC (one small file per conversation) the next turn of that conversation loses the previous-turn fact until the following executed turn, or a replay of the last settled authored turn, rewrites it
state/request_wire_compatibility.json ouroboros/request_wire_contract.py (update_json_locked) own schema_version: 1; any other version reads as an empty store per-profile actions expire on read at REQUEST_WIRE_TTL_SEC (14 days) learned provider-wire adaptations are forgotten; the next send re-learns them from a fresh provider refusal (one extra failed attempt per route)
state/claudexor_rotation_provisioning.json ouroboros/claudexor_daemon.py _record_rotation_receipt (atomic text) none — not needed: a disclosure receipt, never read back for behaviour last reconcile that actually patched wins the receipt is lost; provisioning itself is idempotent — the next ensure re-reads the daemon settings and re-POSTs only when the values are absent
state/delegate_terminal_refresh_cursor.json ouroboros/delegate_terminal.py refresh_recently_settled_terminals (atomic_write_json, best-effort) none — accepted: a byte offset over the rotated custody chain plus a deferred map, self-grounding (an offset past the chain resets to 0) overwrite; deferred capped at 500 tasks the cursor re-grounds at 0 and the one-time historical pass repeats, paced by the 5 MB per-tick scan cap; no terminal evidence is lost
state/extension_generation.json ouroboros/extension_reconcile_queue.py publish_extension_generation (write-if-changed) own schema_version: 1 overwrite (one live-extension fingerprint) an absent marker is NO evidence of divergence (fail-closed): running workers keep their spawn-time extension set until the next publish
state/post_task_evolution_request.json, state/post_task_evolution_counter.json ouroboros/post_task_evolution.py (atomic publish; the supervisor polls and consumes) request: own schema_version: 1; counter: none — not needed (a single n) request is one-shot (consumed by apply_pending_request, deleted by the owner-stop sites); counter overwrites a pending promotion signal is dropped (no evolution task queued) and the every-N cadence restarts its count; the durable re-arm backstop stays the evolution_owner_stopped state flag, not this file
state/subagent_last_delegation.json ouroboros/subagent_history.py record_last_delegation (locked atomic, best-effort, canonical data plane; task finalization and common session custody producers) none — additive disclosure; old single-row receipts remain readable, missing/corrupt data is unknown latest dated observation per actor, bounded by MAX_CONFIGURED_SUBAGENTS, plus compatible latest receipt; undated occurrence stays unknown and replay does not refresh age helper history disappears until a new observation; admission is unchanged and full forensics remain in custody/attempt records
state/panic_stop.flag, state/owner_restart_no_resume.flag ouroboros/server_control.py, server.py (atomic pair) none — not needed (one-shot flags, consumed on boot) consumed absence is the default; next boot auto-resumes

3. state/ — append-only ledgers

Path Writer schema/record marker Retention Reset
state/usage_attempts.jsonl (+ state/usage_attempts.quarantine.jsonl, state/usage_attempts.lock) ouroboros/usage_ledger.py single chokepoint (own O_APPEND+fsync under named lock — NOT append_jsonl); ouroboros/usage_compaction.py rewrites it whole (verified candidate, atomic swap) under the same lock no _schema_version; its own validated contract: dense seq, kind discriminator, state machine, per-row candidate_measurement_kind, attribution physical_attempt_v1; compacted files lead with a stamped usage_baseline header + usage_baseline_group rows — accepted bounded by compaction (config USAGE_LEDGER_COMPACT_BYTES, 8 MB): terminal non-review attempt chains fold into the baseline block, raw segment archived first (fsync'd) — see docs/USAGE_COMPACTION.md; 20 MB WARN can reflect broken compaction, a large unfoldable residue, or a refused/skipped pass: the name-tier refusal emits usage_ledger_compaction_refused once per process per data root; a policy abort (_Abort) emits usage_ledger_compaction_skipped once per process per (data root, reason); the two snapshot-race exits before archive/swap only log warnings, without a typed event; torn tails — and a charge a compaction swap erased inside its last syscall, read back from the old inode (POSIX) — quarantined, never GC'd monetary history destroyed, seq restarts, budget fences read $0; watermark survives so legacy import will NOT re-run — deleting the ledger alone is unrecoverable; a ledger reset beside archive/usage_ledger/ makes history queries raise generation newer while an unreferenced segment represents a newer generation and is not a byte-prefix of the live file; after fresh compactions reach the surviving archive generations, those old unreferenced segments are skipped and their attempt IDs remain absent — see history readers; keep or reset the ledger and archive together
state/skill_review_root_tasks.jsonl (+ state/skill_review_root_tasks.gaps.jsonl) ouroboros/skill_review_history.py (_append_root_task_projection_once: one compact row per root task appended when its skill review lands; a row the projection could not attribute goes to the .gaps.jsonl ledger via _record_root_task_projection_gap) rows carry usage_attribution_schema: physical_attempt_v1; no version key — accepted (derived index over the per-skill review_history.jsonl, P7) unbounded append; reads are BOUNDED (skill_readiness.py: 1 MiB / 512 records tail, a truncated or gapped read is disclosed as projection_incomplete) and the startup hot-store check warns past SKILL_REVIEW_ROOT_TASKS_WARN_BYTES delete with state/; the index is not rebuilt — readiness and the acceptance packet read projection_incomplete until new rows accrue (the per-skill histories keep the truth)
state/process_ledger.jsonl ouroboros/process_custody.py (spawn chokepoint) none — downgrade-safe field split (start_time/start_time_boot) is the versioning device — accepted self-compacting: reapers rewrite survivors-only prior-generation supervised processes permanently orphaned (fingerprint index lost)
state/evolution_checkpoints.jsonl ouroboros/evolution_checkpoints.py schema_version: 1 on every row (+kind on outcome rows) unbounded append; read bounded (last 200) — accepted (structured solve-capability history is the product) absorbed/abandoned objectives can be re-proposed (BUG3 regression); the outcome row is DERIVABLE from the resolved campaign transaction, so a row lost to a crash between the two writes is replayed at boot (source: boot_backfill)
state/consciousness_observations.jsonl none — RETIRED with the observation inbox; nothing writes or reads it n/a the alarm clock MOVES a file left by an older version once, unread, to archive/consciousness_observations.jsonl (a name collision gets a _<boot ts> suffix) and never looks again nothing — the rows are already historical

4. state/ — directories

Path Writer schema_version Retention Reset
state/skills/<name>/ owner state (review.json, review_job.json, grants.json, enabled.json, deps.json, self_authored.json, owner_attestation.json, accepted_rebuttals.json, health.json, uninstalled.json, provenance sidecars, auto_repair.json, presence_profile_state.json) ouroboros/skill_loader.py, skill_review_runner.py, skill_owner_attestation.py, skill_review_cycles.py, skill_uninstall_state.py, extension_health.py, marketplace/*, ouroboros/gateway/marketplace.py; allowlist SSOT contracts/skill_payload_policy.py deps.json/self_authored.json/provenance: schema_version: 1; review.json/enabled.json/grants.json/review_job.json/owner_attestation.json/accepted_rebuttals.json: _schema_version: 1 (ABI-2, stamp-on-write — readers keep legacy-0 tolerance, unstamped files never retrofitted); verdict/grant staleness stays pinned by content_hash no age GC; hub uninstalls write an uninstalled.json tombstone and the startup sweep clears the dead state BY that mark — grants.json survives as owner authority, a reinstall self-heals the tombstone; the gateway's local delete removes the whole state dir absent state = disabled + pending review + grants revoked (fail-closed); owner_attestation absence invalidates its verdict
state/skills/<name>/review_history.jsonl + review_dispatch/ (legacy review_dispatch.json) ouroboros/skill_review_history.py rows carry usage_attribution_schema: physical_attempt_v1; no version key — accepted (derived-counter SSOT, P7) history unbounded per skill — accepted with BOUNDED reads: every reader windows the 4 MB tail (find_history_job_bounded idiom); lifecycle terminal rows persist their ordinals so counters stay exact inside the window (a group aged past it restarts low — under-counts, never over-blocks); per-skill archive rotation declined (no per-skill archive plane; disclosed) review-cycle ceiling resets to zero; paid dispatches become free again
state/delegate_project_retirements/<sha256[:24]>.lock ouroboros/delegate_custody_usage.py (project_retirement_lock: exclusive file lock around one project's settlement/retirement decision; stale after 120 s, owner-aware) none — not needed (lock file, no payload) one file per project ever settled; reclaimed as stale by the next holder delete freely; a live holder re-creates its lock
state/skills/<name>/ transport dirs: extension_calls/, __extension_imports/ ouroboros/extension_process_runner.py, extension_import_staging.py none — not needed (per-call transport files, staged import trees) per-call files consumed; import leaves reaped owner-dead+grace transient; recreated per call
state/delegate_recovery/, state/delegate_recovery_transactions/ (+active.json), state/delegate_supervision/ ouroboros/delegate_recovery.py, delegate_supervision.py (+ startup sweep delegate_state_sweep.py) own schema: 1 on supervision/transactions; recovery rows fingerprinted, unversioned terminal+age startup sweep: terminal-status recovery rows (vetoed/adopted), unreferenced transactions and settled-task supervision files past GC retention; live/resumable rows, active.json, no-result tasks and unreadables kept fail-closed; unreadable custody log skips the sweep interrupted delegated runs cancelled instead of adopted; duplicate wake replay; planned handoffs vetoed
state/delegate_actor_claims/*.lock, state/.payload_delegation_claim.lock ouroboros/delegate_custody.py, delegate_start_claims.py none — locks unlinked on release no durable state
state/code_intel/<root-sha>/inventory.json ouroboros/code_intelligence.py own schema_version: 2 (older/malformed rebuilt silently) per-repo rewrite in place; stale roots age-pruned at startup by inventory.json mtime past GC retention (pure cache) pure derived cache; one full re-index
state/extension_reconcile/ (+failed/) ouroboros/extension_reconcile_queue.py none — not needed (one-shot markers) consumed by server loop; after 5 attempts moved to failed/, where markers age-prune past GC retention (the failure fact stays durable in events.jsonl) pending worker→server reconciles lost; re-toggle heals
state/workspace_executor_processes/ ouroboros/workspace_executor.py own schema_version: 1 + owner tag unlink on stop; stale rows filtered at read (pid/cmd-sha) service processes survive unreaped
state/acceptance_fence_acks/<token>.<req>.json supervisor/events_worker_reports.py (writer, one file per pooled-worker request), ouroboros/agent.py (the requester reads and unlinks only its own req; direct turns fence in-process and write none) none — transport ack inline GC on write (255 newest / 3600 s); a late ack nobody waits for ages out here the waiting worker re-sends a transition once (a read never), then records a typed supervisor_ack_unavailable unknown — never read as a refusal
state/headless_tasks/<id>/ (child data drives) ouroboros/headless.py child state/state.json: schema_version: 1 GC-retention prune at startup (terminal + age; skips artifacts-not-terminal / refs-unpromoted) in-flight child drives and unpromoted child refs lost
state/cx/ (managed Claudexor runtime) ouroboros/claudexor_runtime.py meta files versioned (_NODE_META_SCHEMA_VERSION: 2, pin 1) deliberate keep-all (rollback selects older pins); staging/displaced temporaries reaped re-downloaded on demand; nothing durable lost
state/betterleaks/ (managed runtime + cache) ouroboros/betterleaks_runtime.py own manifests (schema_version: 1) keep-all archive cache — accepted (managed runtime) re-downloaded on demand
state/pycache/, state/python-userbase/ launcher.py, ouroboros/packaged_cli.py, launcher_bootstrap.py (PYTHONPYCACHEPREFIX / PYTHONUSERBASE) none — caches no GC; python-userbase is a documented manual-recovery hazard (outranks bundle site-packages) pycache: always safe (recompile); python-userbase: deletes REAL user-installed deps
state/review_continuations/<task>.json (+ state/review_continuations/archived/**, state/review_continuations/corrupt/** with its .txt reason notes) ouroboros/task_continuation.py (atomic write; rename to archived/ on retire, replace_atomic to corrupt/ on quarantine) none — accepted: a typed dataclass with an ownership check (a task_id mismatch raises) and malformed JSON quarantined, never migrated live payload per blocked task, cleared on resume; a settled, un-resumed continuation is retired to archived/ past RETIRE_SETTLED_CONTINUATION_AFTER_DAYS (7) so history stops riding into every new prompt; archived/ and corrupt/ unbounded — accepted (small typed payloads; the quarantine IS the corruption evidence) blocked tasks lose their resume pointer: findings, obligations and the commit intent must be re-derived by re-running review
state/skills/<name>/auth_token.json ouroboros/extension_plugin_api.py mint_skill_token (atomic write, chmod 0600) none — not needed: the token is bound to content_hash, which IS its staleness contract one token per skill, rotated on a content-hash change; a transient hash failure reuses or fails closed, never rotates the next mint issues a new token, so a companion still holding the old one is de-authorized until respawn — do not delete while companions run
state/skills/<name>/repair_admission.json ouroboros/skill_repair_admission.py record_repair_admission (atomic; the newest admission owns the record) own schema_version: 1 one record per skill, superseded by the next admitted repair the repair CAS has nothing to check against and its writes are refused — fail-closed by design, since this record exists to remove exactly that fail-open
state/skills/<name>/jobs/<job>/ (assets/, output/, tmp/) ouroboros/extension_plugin_api.py skill_job_dir (creates the three children on request; the extension owns the contents) none — not needed: a workspace, not a record unbounded: nothing sweeps it, and the gateway's local skill delete is the only path that removes it with the state dir in-flight extension jobs lose their assets and output; the next call recreates the tree
state/skills/<name>/chat_id_counter.json ouroboros/gateway/host_service.py allocate_internal_chat_id (atomic, under the in-process counter lock) none — not needed: range_name plus last/next id one file per skill; ids descend from A2A_CHAT_ID_MAX allocation restarts at the top of the range, so a fresh A2A room can reuse a chat id already present in history
state/project_source_locks/ none in this tree — orphan plane seen in live layouts (removed feature leftover) none n/a harmless; nothing reads or recreates it

5. logs/

Path Writer Record marker Retention Reset
logs/chat.jsonl supervisor/message_bus.py (+presence, project summaries) direction + optional type; no version — accepted (projection replayed by chain-aware readers) rotated 800 KB → archive/chat_*.jsonl; archive chain WARN at 100 MB newest generation lost; consolidation cursor reports gap (recoverable)
logs/progress.jsonl supervisor/message_bus.py (+plan review) type: send_message, is_progress rotated 800 KB → archive/progress_*.jsonl; 8 MB WARN = rotation broken current segment lost; readers archive-chain-aware
logs/events.jsonl ~60 modules via append_jsonl (+delegate_custody.emit) universal type discriminator — accepted (per-type payloads owned by emitters) rotated 800 KB → archive/events_*.jsonl; custody readers (replay, fault tail-scan, complete_custody_rows, settled-terminal chain cursor, legacy-usage import, swarm rollup, worker-boot verify) are chain-aware; delegated start rows reference their complete raw replay envelope in observability/blobs/** (blob before event), with inline legacy rows still readable; task_received omits only an equal metadata mirror of its top-level task contract; 8 MB live WARN = rotation broken; 100 MB chain WARN = replay degradation delegated-run custody destroyed (chain incl. archive segments): open runs invisible/unreapable; lineage, citations, legacy-usage source lost
logs/tools.jsonl ouroboros/loop_tool_execution.py (+budget-drive mirror) type: tool_call + task lineage (root_task_id/delegation_role from resolve_task_lineage, a direct root is its own root), untruncated args rotated 800 KB → archive/tools_*.jsonl; tail readers (api_logs_tail, task_events) archive-backfill; 8 MB WARN = rotation broken untruncated tool record + result_ref pointers lost
logs/supervisor.jsonl supervisor family, process_custody, gateway control, server shutdown type (+secondary event_type) rotated 800 KB → archive/supervisor_*.jsonl + 8 MB tripwire; tail readers (memory.read_jsonl_tail, api_logs_tail) archive-backfill reap receipts, rescue disclosures, shutdown causes lost
logs/task_reflections.jsonl ouroboros/reflection.py (+ project-scoped copy under projects/<id>/logs/) full rows unversioned; pointer rows type: project_reflection_pointer rotated 800 KB → archive/task_reflections_*.jsonl + 8 MB tripwire; tail-20 read archive-backfills; project-scoped copies follow project retention (never age-pruned) inter-task memory-carry signal lost
logs/containment_faults.jsonl ouroboros/delegate_custody.py (mirrored to events.jsonl) type ∈ CONTAINMENT_FAULT/RESOLVED joined on run_id unbounded BY DESIGN — read whole so an open fault never ages out — accepted health invariant degrades to the 4 MB events tail scan (the regression this file fixed)
logs/chat_annotations.jsonl ouroboros/project_dialogue.py (append_jsonl under the shared sidecar append lock) type: chat_annotation keyed by (client_message_id, routing_token); latest row per key wins (an owner message keeps one row per routing act; task-authored acts ride synthetic agent-steer:<token> ids) self-compacting at 800 KB under the append lock: the latest row of every message still present in the chat chain (live chat.jsonl + the 3 newest archive/chat_*.jsonl) is rewritten, the rest is DROPPED — presentation state, so it is not rotated into archive/ annotation cards fall back to their plain chat rows; the one named exception (#198) also loses the durable picker decision-card token, so a pending manual routing choice must be made again
logs/tasks/task_<id>.txt ouroboros/utils.py log sanitization (write_text, best-effort) raw oversized task text, no envelope; the log row keeps text_full_path unbounded: no retention sweep names logs/tasks/ the truncated text in the log row stays; only the spilled full text of oversized task prompts is lost
logs/agent_stdout.log launcher.py pipe-copy thread unstructured text bounded ~8 MB (2 MB × .1..3 backups, rotated by the copy thread) pre-logging crash output lost; nothing parses it
logs/server.log (+.1..3), logs/launcher.log stdlib RotatingFileHandler (server.py, launcher.py) with secret-redacting filter text bounded ~8 MB (2 MB × 4) — the model citizen stdlib log history lost; nothing parses it

6. memory/ (Ouroboros cognition — operator read-only)

Path Writer Marker Retention Reset
memory/identity.md ouroboros/tools/control_runtime.py (full overwrite, ≥50 chars, shrink notice) none unbounded document — accepted (identity is the product) reseeded default; identity lost (journal keeps history)
memory/scratchpad.md + scratchpad_blocks.json ouroboros/memory.py (derived, regenerated from blocks under lock) none bounded: 10 blocks, eviction journaled first (fail-closed) regenerated; evicted history in journal
memory/WORLD.md ouroboros/world_profiler.py (write-once) none fixed regenerates on restart — deletion IS the refresh mechanism
memory/registry.md, memory/deep_review.md ouroboros/tools/memory_tools.py (section RMW), ouroboros/agent.py (overwrite) none unbounded / last-wins — accepted recreated lazily
memory/dialogue_blocks.json + dialogue_meta.json ouroboros/consolidator.py, memory_nomination_receipts.py (locked atomic) pending_knowledge_nominations source-entry IDs; legacy last_unpublished_nominations preserved blocks bounded by era compression (10 blocks, oldest 4); unresolved nomination index unbounded; no tool-level resolver yet, later success never retires old debt blocks: compressed biography irreproducible; meta: cursor and unpublished-obligation evidence lost
memory/dialogue_summary.md none — legacy read-only (reader in context.py) none frozen legacy artifact; nothing writes it
memory/knowledge/** (topic .md + index-full.md + patterns.md) ouroboros/tools/knowledge.py, consolidator.py (index rebuild), reflection.py (patterns CAS rewrite) none topic files unbounded — accepted (curated by consolidation); backlog topic merge-only fail-closed recreated lazily; knowledge lost
memory/*_journal.jsonl, memory/knowledge_history.jsonl, memory/knowledge/patterns_history.jsonl ouroboros/memory.py, tools/control_runtime.py, tools/knowledge.py, reflection.py — every append through the append_jsonl sidecar-lock seam scratchpad journal: type rows; others unversioned full-text snapshots; historical digested rows retain content_digested: true complete new old+new snapshots are retained indefinitely; memory_journal_compaction.py is a read-only compatibility entry point, not a source rewriter; existing digest-only rows cannot be restored; the memory_journal_observation startup event gives byte sizes (or missing/unreadable) for the three named journals; scratchpad keeps its eviction journal deleting the journals loses undo/provenance; eviction/rewrite paths fail closed when journal append fails; historically digested content remains irrecoverable
memory/owner_mailbox/<task>.jsonl + .acks.jsonl ouroboros/owner_mailbox.py (append-only; revocation appends, reader resolves) kind discriminator lifecycle-bounded: unlinked at task terminal; a startup sweep unlinks mailboxes whose task has a SETTLED durable result (no result / non-terminal keeps the mailbox fail-closed) undelivered owner directives + restart-surviving hurry latch lost; acks lost ⇒ re-delivery

7. Skills payloads, tasks, uploads, projects, services

Path Writer Marker Retention Reset
skills/{native,clawhub,ouroboroshub,external}/<name>/** (+.staging/, .ouroboros_env/ with cache/ tmp/ home/) ouroboros/marketplace/*, launcher_bootstrap.py seed, agent self-authoring provenance sidecars schema_version: 1; env fingerprint.json: 1 no age GC (payloads are installed software); staging rmtree'd per install, crash orphans recognized by name fragments; package caches live with the skill bucket recreated empty; native seeds NOT re-seeded (deletion intent preserved) except post-bootstrap set; orphaned state/skills/ rows keep grants + tombstone after the startup sweep
state/skills/<name>/dependency_cache/ marketplace/isolated_deps.py via existing installer subprocesses and verified resource downloads downloaded resources keyed by sha256; package-manager cache formats reused across payload/environment replacement; follows existing skill-state cleanup resources are verified/downloaded again and package caches rebuilt
state/skills/<name>/go/, state/skills/<name>/go-cache/ Go compiler launched by tools/skill_exec.py:_run_go_skill, with GOPATH/GOCACHE bound to skill_state_dir Go-owned cache formats reused across script runs; follows existing skill-state cleanup compiler recreates caches; skill payload and review remain unchanged
task_results/<id>.json ouroboros/task_results.py (locked merge) _schema_version: 1 (ABI-2); unstamped/future/malformed → quarantine, no conversion — with one carve-out: the boot latch migration re-stamps unstamped rows still at cancel_requested in place (same status, one typed task_result_cancel_latch_admitted event) so a wedged task still reaches its cancelled terminal UNBOUNDED — one file per task forever, no GC; lifecycle authority is retained deliberately lifecycle authority lost; drive prunes degrade to age-only; strict authority reads break
task_results/quarantine/ ouroboros/task_result_schema.py (same-dir rename); presence_runner.py reads the retained id before a transport retry quarantined bytes unchanged; a Presence retry with quarantined authority refuses regeneration NEVER GC'd (pinned); recovery is manual owner re-stamp quarantined evidence lost; a previous Presence attempt could be mistaken for a new event
task_results/artifacts/<id>/** (+verification_receipts.jsonl, .directory.*.tmp, .directory.*.json.tmp), task_results/artifact_versions/ ouroboros/artifacts.py, headless.py, outcome_receipt_store.py artifact and complete directory manifests schema_version: 1; scratch manifest 2 artifact versions bounded (5 per name); artifacts live with their result; directory capture stages ZIP and manifest beside the result, removing owned temporaries on caught failures deliverable bytes lost; results keep dangling manifests
task_drives/<id>/** (+tmp_scripts/) ouroboros/headless.py, tools/tool_context.py, tools/shell.py child stamps as above GC-retention prune at startup (terminal + age, default 7 d); the data/tmp_scripts fallback's hard-kill orphans are in sweep_stale_temp_files scope (startup-only when no script can be live) scratch lost; canonical artifacts survive
task_trees/<root>/blackboard.jsonl ouroboros/task_tree_ledger.py rows unversioned; snapshot digest schema_version: 1 GC-retention prune at startup (root terminal + age) swarm coordination facts lost for live trees
state/subagent_worktrees.json (registry; checkouts live OUTSIDE data root) ouroboros/subagent_worktrees.py none — malformed → typed refusal (absent = empty is the designed asymmetry) — accepted; file_baseline retains copied binary input identities prune_orphans (age + missing checkout; skips delegated_exec) + custody-cross-checked snapshot prune (fail-closed on unreadable custody) permanent leak of checkouts + pinned refs (nothing else names them)
task_results/artifacts/<task>/source_handles/context_checkpoints (focus_source_<reader>-<sha256>.md) ouroboros/tools/project_journal.py (_retain_focus_source, through artifacts.store_actor_source_bytes) on the canonical root; read by task_finalization.focus_source_projection (digest glob for a historical selector) native task_source handle on focus.source_handle (size + sha256, verified on read) write-once, digest-named; lives with the task's artifacts (no separate prune) a roster's retained_source reads source_unavailable; the focus text itself survives on the task result
task_results/artifacts/<task>/delegated_runs/<run>/engine-files-manifest.json, task_results/artifacts/<task>/delegated_runs/<run>/*-* ouroboros/delegate_directory.py, existing streamed task-artifact owner exact engine manifest SHA and per-file before/after digests canonical result artifacts retained; engine keeps unapplied copy results under its existing disposition/retention owner loss of complete binary results, baseline references and evidence after execution copy cleanup
uploads/** (+screenshots/, views/, atomic-copy .tmp files) ouroboros/gateway/files.py through artifacts.copy_artifact_file for chat uploads; tools/browser.py, tools/vision.py, server_owner_routing.py raw owner bytes; chat ingestion measures size and SHA256 while copying owner attachments in the uploads/ root: NO retention, owner-explicit delete only; chat upload no longer applies the former 50 MiB cap, while Files-browser and downstream transport limits retain their own contracts; agent-generated screenshots//views/ age out past GC retention at startup chat attachments dangle (readers skip missing); staged task copies survive
services/<task>/*.log ouroboros/workspace_executor.py, tools/services.py none — raw text; archived content becomes observability blob with events.jsonl receipt GC-retention prune at startup (archive-then-unlink); per-task terminal archive; oversize logs retained live live tails lost; archived blobs survive
projects/<id>/** (knowledge, journal, workpad, reflections, .project.json marker) ouroboros/project_facts.py, projects_registry.py marker unversioned; registry stamped (§2) NEVER age-pruned (owner curates; delete = durable tombstone) per-project memory lost; registry row survives, room reappears empty
projects/<id>/knowledge_history.jsonl, projects/<id>/knowledge_journal.jsonl ouroboros/knowledge.py through the shared knowledge write lock append rows retain source topic, revision and operation facts follows the owning project shelf; retained with the project until explicit deletion history/provenance lost while authored project notes remain
archive/** (rotated segments, rescue/, usage_import/, managed_repo/) rotation + supervisor/git_ops_rescue.py, usage_legacy_import.py, launcher_bootstrap.py segments inherit source shape; usage_import carries sha256 sidecar UNBOUNDED BY DESIGN — durable history, never GC'd (P1) — accepted memory horizon truncated; rescue copies of uncommitted work destroyed
archive/usage_ledger/segment_*.jsonl ouroboros/usage_compaction.py (exact pre-compaction ledger bytes, written + fsync'd BEFORE the live swap) each segment is a whole valid ledger generation; hash-pinned by the live usage_baseline header (source_sha256), chained recursively through each segment's own leading header UNBOUNDED BY DESIGN — the folded monetary history, never GC'd (P1); read via archived_attempt_ids (tamper-evident, per-attempt joins for the model-send reverse sweep) folded per-attempt monetary history unrecoverable; live aggregates (baseline block) survive, but seal/attempt joins for folded ids break — the mirror of the ledger row: deleting the ARCHIVE alone under a stamped ledger is a typed chain break on every history question, deleting the LEDGER alone raises generation newer for surviving newer, non-prefix segments; once fresh compactions reach those generations, old unreferenced segments are skipped and their attempt IDs are absent (history readers); reset both together
observability/{calls,blobs,salvaged}/** ouroboros/observability.py (private 0700/0600, CAS gzip); model-send records beside the call manifests: model_send_seal block + write-once <attempt>.model_send_violation.json typed facts (ouroboros/model_send_seal.py) call manifests schema_version: 1 + custody/redaction honesty markers; blob refs sha-verified on read; model_send_seal.seal_version: 1 with the canonical_json_v1 basis string preserved indefinitely BY CONTRACT (the startup census counts, never deletes); the inert OUROBOROS_OBSERVABILITY_RETENTION_DAYS knob is RETIRED (key in RETIRED_SETTING_KEYS) every recorded result_ref/manifest_ref dangles (strict readers raise); pending delegated request bodies become unknown, while custody identity still blocks replacement and protects snapshots; replay refuses without the recorded body; salvaged outputs unrecoverable; a lost seal on a seam-dispatched attempt surfaces as a typed unlogged_attempt fact at the next startup sweep
claudexor/** EXTERNAL writer — the claudexord daemon (Ouroboros only mkdirs, appends daemon.log, writes ouroboros-owned.json marker) marker unversioned daemon-owned; grows unbounded under our root — disclosed external plane owner harness logins/profiles lost (fresh device-auth required)
playwright-browsers/ ouroboros/tools/browser.py (vendor install) none — vendor tree no GC — accepted (vendor cache) re-downloaded on next browser use
cache/pip, cache/uv ouroboros/test_environment.py package-manager formats; TEST ROOT ONLY, not live data disposable verification-root lifetime recreated by explicit dependency setup

Reset ladder (summary)

Always safe (pure caches, recreated): state/pycache, state/code_intel, state/evolution_metrics_cache.json, playwright-browsers/, state/cx, state/betterleaks, lock files, state/server_port. Safe with bounded cost: WORLD.md (regenerates), state/usage_import_watermark.json (safe re-import), ui_preferences.json, auth_secret.key (one re-login). Fail-closed losses (system stays correct, work/authority is forgone): skill state dirs, advisory_review.json, capability_evidence.json, pending_restart_verify.json. Dangerous (authority/history destruction): settings.json, state/usage_attempts.jsonl, task_results/**, logs/events.jsonl, memory/** (including dialogue_meta.json: deletion erases cursor and pending nomination obligations; re-consolidation cannot reconstruct the old IDs), archive/**, observability/**, state/subagent_worktrees.json (leak), claudexor/**, state/python-userbase (real deps).