Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

State, cache and limits

Where artifactize keeps its state, how to inspect and limit the reuse cache, how to cap Agent backends machine-wide, how saved Agent conversations are kept and bounded, and the local diagnostics and maintenance commands.

State

One state.sqlite holds Runs from every repository (bundled SQLite, WAL, schema 8), with the canonical repository path recorded on each Run. The state home is $ARTIFACTIZE_STATE_HOME, else $XDG_STATE_HOME/artifactize, else (on Windows) %LOCALAPPDATA%\artifactize, else $HOME/.local/state/artifactize.

The Microsoft Store package uses Windows’ default AppData virtualization, with no Flexible Virtualization exclusions. New state under AppData is package-private: the Store version keeps its own Runs, sign-in and cache instead of sharing them with winget, zip or Cargo installs. Windows removes that private state on uninstall. The path above is the app’s logical view, not necessarily its physical disk location. The Windows packaging smoke test measured both state.sqlite and review-store.sqlite under %LOCALAPPDATA%\Packages\lhj6102.Artifactize_ayfzfzbsv48sg\LocalCache\Local\artifactize, with no real %LOCALAPPDATA%\artifactize directory created on its fresh Windows Server runner (build 26100).

That same test launched a PowerShell child through a Runtime eval. The child’s new %LOCALAPPDATA% file was not redirected: the unpackaged shell found it in real AppData, not in the package-private location. This measures that runner and command, not every child program or Windows version. User-configured commands run with the user’s rights; do not assume their AppData writes are package-private or removed when the Store app is uninstalled.

This is not a sandbox or a guarantee that existing unpackaged state is inaccessible. Microsoft documents that on Windows 10 1903 and later, reads can fall back to existing unvirtualized AppData files, and modifications to those existing files can affect the real files. Do not rely on side-by-side Store and unpackaged installs to isolate existing data. State explicitly placed outside AppData is not covered by AppData virtualization or the Store’s private-state uninstall cleanup.

--state-dir PATH moves the whole state, including private output directories under PATH/runs. Saved Runs stay readable after the original repository is removed. State/output inside the reviewed repository or another artifactize workspace is rejected, including through symlink ancestors. For this safety check, any ancestor containing a .artf declaration, legacy artifactize.json or legacy .artifactizeignore marks a workspace, even though legacy declarations are not read. Sidecar-only workspaces are protected too. Database files and their WAL sidecars must be regular files.

Known limitation: a search-only ancestor can be traversed but not listed. When listing fails with permission denied, the guard falls back to fixed-marker probes for .git, index.artf, legacy artifactize.json and legacy .artifactizeignore. It cannot discover a sidecar-only workspace in that ancestor. Keep state and credentials outside such workspaces explicitly; do not rely on sidecar detection where directory listing is unavailable.

The state keeps what was reviewed, with which verdict, and how it was executed, in four tables:

TableOne row perColumns besides the saved JSON in data
runsRunid, repo, status, nullable request_key
requestseval of a Runid, run_id, eval_id, ordinal (its place in the Run’s selection order), execution_id, status, and claimed_by and claimed_at, the Human claim of a waiting request
executionsexecutionid, key, eval_def_hash, status, owner_pid and owner_start_time (the owning process), backend (an Agent review’s, see Backend capacity), profile_identity (the effective profile of a keyed claim), and the key history columns completed_at, bytes and last_used
state_metanamed value of the statename, value

A Run has one request per eval and per ordinal. One execution per (reuse key, effective evaluation profile) can be active (RUNNING or WAITING_HUMAN) at a time; a forced (--force) execution bypasses this claim and never holds its key’s active slot. state_meta holds the state’s stable id, a random UUID made when the database is created (row id), which Agent session references name. How a review went, its conversation and its tool calls, is not in the state: it is in the review’s saved session.

Supported upgrades are ordered additive steps applied together with user_version in one opening write transaction; the version is read again under that lock. Schema 6 upgrades to 7 by adding nullable runs.request_key and a partial unique index on (repo, request_key) where the key is not NULL. Existing Runs keep NULL keys. Schema 7 upgrades to 8 by adding executions.profile_identity, filling it from saved profiles and replacing the active-key uniqueness index with (key, profile_identity). Run/request/execution IDs, cache, state identity and Human approvals survive; repeated opens do not remigrate. Existing-state readers and doctor use the same preflight without creating an absent database. Migration requires write access; a failed transaction rolls back rather than bypassing version checks.

Same repository/key returns the same accepted Run; NULL keys never coalesce submissions. Worker PID/start identity and admission, cancellation intent, activity and failure code live in optional/defaulted Run JSON control fields. Old Runs without them are readable and are not treated as dead workers. Saved Run-end validation remains historical; detailed results are derived from saved Runs, requests and executions, not stored as another result snapshot. Dead-worker recovery keeps committed evidence, cleans only identity-checked owned children and releases dead slots. It records OWNER_DIED instead of replaying work; unconfirmed cleanup retains its journal records.

Downgrade boundary: Binaries that only support schema 6 or 7 cannot open a schema-8 state after upgrade; their version guards refuse it. Unknown/newer versions and schemas 1–5 remain unsupported; commands exit 2 and doctor reports a hard error (exit 1). The older-schema message is:

This state was written by an earlier artifactize. Start a new state (set ARTIFACTIZE_STATE_HOME or move the old one away). Only schema 6 and later supported versions can be upgraded.

A new 0.9 state reviews every executable eval once, Human sign-offs included, and then reuses as before. Reuse-key v2 matches no earlier records, including script fingerprints and team-store records. See Upgrading to 0.9.

Tokens are never stored inside a repository. $STATE/auth, which holds the Codex sign-in and the review store token, is refused when it lies inside a git work tree or an artifactize workspace (an ancestor with any current or legacy marker above) or inside --repo, even in a gitignored folder. The .git check protects Git work trees separately from Artifact declaration markers. login codex and remote login then fail before signing in, with a message naming both folders:

Codex sign-in storage /work/project/.state/auth is inside the git work tree /work/project; artifactize keeps tokens outside repositories. Use a state directory outside it, or set ARTIFACTIZE_CODEX_AUTH_FILE.

Use a state directory outside the repository, or ARTIFACTIZE_CODEX_AUTH_FILE and ARTIFACTIZE_REMOTE_TOKEN, which need no storage. doctor reports such a state as a warning while nothing is stored there, and as a hard error once a sign-in is (for example, after git init above an existing state). No writer transaction spans a subprocess or async suspension. Completed results with a reuse key are shared within this database. Concurrent Runs, including other worktrees and processes, automatically join an in-flight execution only when both its reuse key and its complete effective evaluation profile match. The queued request names the execution it waits for; it takes no execution start or backend slot. Completion delivers that execution’s GREEN, RED or ERROR to its waiters as joined reuse. An ERROR is not a new cache entry for later Runs. For a Runtime or Agent execution, cancellation or a dead owner (checked by PID and process start time) releases the claim: one waiter starts a replacement, and the others join it. A Human sign-off is not replaced: when its owning Run is cancelled or its worker stops, the sign-off and every request waiting on it, in any Run, end as ERROR, and a new verify requests another one. Cancelling a waiter does not cancel the owner. --force always runs its own execution and never joins. There are no sharing settings, and in-flight sharing never crosses a remote store. Completed GREEN/RED reuse remains keyed by the reuse key alone, even if the producing profile differs.

Unreadable records

An individual saved record can be unreadable even when the database and its other records are valid. Artifactize skips that record as review evidence rather than inventing a verdict. Invalid requests are excluded from Run-list request counts; a Run with damaged requests can still open, while an unreadable Run record is marked unreadable rather than treated as a normal saved Run.

Status and Run listings expose unreadable diagnostics with the record’s kind, id and reason; evidence readers also print an Unreadable ... diagnostic on stderr. Valid records remain accessible. Preserve the state for diagnosis and start a new verification when fresh evidence is needed; missing counts do not mean the damaged reviews passed. This is different from a database-open or unsupported-schema failure, which can prevent the command from reading state at all. doctor checks that database/schema readiness; it is not a record-repair tool.

Cache inspection

artifactize cache list [--history] [--json]
artifactize cache show KEY [--history]
artifactize cache rm KEY

These commands use the shared state home or --state-dir PATH, without loading a repository. Each reuse key keeps every completed GREEN/RED record, and the latest by completion time is the one verify reuses. list shows the latest record of each key: key, eval, verdict, completion time, producer, repository (or the remote store it came from), how many records the key holds, retained JSON bytes and last use (a text table, or a JSON array that also carries the Eval definition hash, the target’s fingerprint and the profile variant). --history lists every record, latest first within each key. show prints the key’s latest saved execution as JSON, with result, actual profile, options (backend, model, reasoning, limits and variant), fingerprints (each Artifact the key covers), additive artifactKinds (the covered names mapped to folder/file/graph), provenance, usage and producer (user@host and artifactize version; submitted Human results also record their reviewer); a missing key prints null and exits 4. show --history prints all of the key’s records as a JSON array, latest first. Entries mirrored from a shared remote review store carry an origin (store, publisher, publication time), which list shows in place of the repository. Reads neither create missing state nor update access times. A key’s records are its completed GREEN/RED executions whose history columns (completed_at, bytes, last_used) are set; the latest is the newest completion, then the most recently stored. rm takes every record of the key out of its history and prints {"removed":true} (false if absent), preserving saved Runs and the executions themselves. rm refuses a key with an active execution or waiter. Keys come from cache list or from a request’s key in verify --wait --json and run show.

How reuse keys are built and what a hit returns is in Completed result reuse; entry and byte limits are in Cache limits.

Cache limits

Saving a new reusable result automatically removes the least recently reused eligible cache records to stay within 10,000 records and 1 GiB of saved result data. One record may occupy at most 16 MiB. A larger result still reaches its own Run and any current waiters, but it is not kept for later reuse.

Reusing a record updates when it was last used; inspecting it does not. Cleanup removes the least recently used records first, breaking ties by key and completion time. It keeps a key’s records while a review of that key is running, and any records another Run still needs, so these limits can temporarily be exceeded. A cleanup failure is printed on stderr without changing an already-completed verdict; the next saved result retries cleanup.

Cache cleanup and cache rm stop records from being reused. They do not remove the saved execution audit, Run receipts or Run output files. These limits therefore do not cap the whole database or temporary files of active Runs. Matching evidence has no age-based expiry, and cache cleanup does not automatically delete Run files. Use artifactize prune to remove eligible finished Run output.

Backend capacity

READY evals have no default Run-concurrency cap. An explicit --jobs N limits only that Run; add it when unrestricted concurrency causes trouble. Multiple Runs on one machine (worktrees, CI jobs, a team member’s integration run) can still send more Agent reviews to one backend at once than its rate limit or subscription allows. $STATE/limits.json caps the Agent reviews in flight per backend across every verify that uses that state directory:

{"backends": {"codex": 4, "openai": 16}}

Keys are backend names as profiles declare them (openai, anthropic, codex), and each limit is 1–100000. A backend without an entry is unlimited, and with no file nothing is limited. The file is read when a verify starts. A file that is not a regular file, is not valid JSON, has other fields, names an unknown or removed backend or a limit out of range fails verify before it creates a Run (exit 2); doctor reports the limits, or the problem as a hard error.

The bookkeeping is the executions table of state.sqlite: an Agent review’s execution names its backend, and it holds one of that backend’s slots while its status is RUNNING. A slot is taken just before a review would start: one write transaction (BEGIN IMMEDIATE) counts the RUNNING executions of the backend and, under the limit, inserts this one. A request that would reuse a result or join a live execution of its key takes none. The slot frees when the review’s execution completes. An execution whose owner process has exited (including an unreaped zombie, checked by process state, pid and start time) ends as ERROR (OWNER_DIED) the next time a process asks for a slot, which frees its slot with it; there is no separate cleanup. When a review’s failure stops its backend for the Run (BACKEND_STOPPED), the stop is recorded before its slot frees, so a request waiting for that slot is not started.

With every slot of its backend in use, a request waits, as QUEUED, with a reason such as Waiting for a free codex slot: all 4 are in use on this machine (limits.json).. As slots free up, retries follow the Run’s scheduling priorities: READY work first, then the longest remaining dependency path, with selection order only breaking ties. A later-selected request can therefore receive the next slot first. While waiting, a request does not count against an explicit --jobs cap and consumes no --max-executions start, so runtime evals, Human requests and other backends keep running. When admitted, it counts against --jobs if given; without that cap, backend capacity imposes no other concurrency limit. Explicitly declared eval mutex tags independently delay conflicting runtime and Agent evals across the same shared state store, without holding a backend slot while they wait. Explicit run cancel RUN_ID ends its waiting requests with status ERROR and errorCode = "CANCELLED", and its held slots are released. Interrupting verify --wait only stops observation; it does not cancel the Run.

Tool-session state lives under $STATE/tool-sessions/, separately from saved Agent conversations. Artifactize removes it after confirmed cleanup and retains unconfirmed state for recovery. See the tool-session lifecycle for the single detailed start, stop and crash-recovery contract.

Agent sessions

Each Agent review’s conversation is saved under the state, for session show and session send. limits.json sets whether conversations are saved and how large their store grows:

{"agentSessions": {"enabled": true, "maxBytes": 1073741824, "targetBytes": 805306368}}
FieldDefaultMeaning
enabledtruefalse saves no new conversation; saved ones stay readable
maxBytes1 GiBThe store’s size that starts a collection; at least 1
targetBytes768 MiB when maxBytes is greater than 768 MiB; otherwise (maxBytes / 4) * 3, dividing first with integer divisionThe size a collection brings the store down to; an explicit value overrides the default and must be lower than maxBytes

A collection runs at the end of every verify Run and in prune. When the conversation files take more than maxBytes, it deletes the sessions written longest ago, one at a time, until they take targetBytes or less, so each collection frees a fixed amount rather than one session at a time. It never deletes the session of a request that is still RUNNING, nor one that a session send holds; such sessions can keep the store above its target until a later collection. A collection that fails during verify is reported on stderr and leaves the Run as it is. The other limits.json rules apply: an unknown field, a value of the wrong type, maxBytes 0 or a targetBytes that is not lower than maxBytes fails verify and prune (exit 2), and doctor reports it as a hard error.

The store is $STATE/agent-sessions/, owner-only (0700; a protected owner-only DACL on Windows), and it is refused when it is not. Each session is one append-only JSON Lines file, owner-only (0600), named by its sessionId:

$STATE/agent-sessions/
  e83bf374-5136-484c-8d2f-2ce38ee4ca57.jsonl   the conversation, one event per line
  e83bf374-5136-484c-8d2f-2ce38ee4ca57.lock    held while a session send appends to it

Events identify their kind and normally record their time in at; older records can omit optional timestamps or header metadata. New records use:

kindHolds
reviewFirst line: version (1), sessionId, runId, requestId, evalId, target, producer, state, backend, model, reasoning, parameters (as sent, with prompt_cache_key), budgets (timeoutMs, maxToolCalls, maxTokens) and tools (the definitions offered)
messageOne message as the provider received or sent it (message, a rig message: role system, user or assistant, with text, tool calls, tool results and reasoning, encrypted content and signatures included), and the turn (provider request) that carried it; a message of tool results has isError, whether each result failed, in order; the repair prompt has "repair": true, and a follow-up’s framed question has the person’s own words in question
deliveryDisplay-only streaming data: turn, attempt, nonempty block identifier, contentKind (text or summary), text, and state (delta, complete or interrupted)
attemptOne provider attempt of a turn: its attempt number, the usage counters the provider reported (inputTokens, outputTokens, totalTokens, cacheReadTokens, cacheWriteTokens, reasoningTokens, where reported), and its error and errorCode when it failed
endThe review’s result, or its errorCode and error
sendA follow-up starts: its send number, the person’s text, the framing sent before it, filesChanged and the tools offered
answerThe follow-up’s answer text, or its errorCode and error

Delivery delta records carry incremental text, complete marks a completed block, and interrupted marks delivery that ended before completion (including an interrupted attempt). They drive the live display only: they never add to conversation replay history or usage totals, which come from semantic messages and attempt records.

The events of a follow-up also carry its send number. When a budget, the deadline or a repeated call ID stops a turn’s tool calls, the results of the calls that ran are saved and the others have none. The request’s verdict, result and usage stay in state.sqlite; the session is the only record of the conversation and its tool calls, and session show --summary counts them. A last line cut short by a crash is ignored when the file is read.

Follow-up replay

session send uses the saved review’s backend, model, reasoning and session id. OpenAI and Codex retain the same prompt_cache_key; Codex also retains the session-id header. Saved history and the system prompt are replayed unchanged so the cached prefix matches. Current project tools are offered again; changed covered fingerprints add a warning to the header and the model’s context.

A question is a new user turn framed as a follow-up, not a new review. The frame asks for plain text, permits the tools and states that the recorded verdict will not change. Modern review system prompts state that same distinction; older saved conversations rely on the new turn’s framing. session show prints the person’s original question, while its JSON export retains the framing too.

Questions, answers and tool calls append to the same saved thread. Sends to one session serialize: each continues after the preceding answer. Each question has fresh profile budgets. A RUNNING request cannot be followed up; recorded verdict, result, usage and reuse evidence remain immutable.

Provider transport

The rig-core 0.43.0 provider client uses streaming OpenAI Responses with store:false, encrypted reasoning replay and parallel tool calls disabled, or Anthropic Messages. Anthropic requires a per-turn output cap of 16384 tokens, separate from the review budget. One review deadline covers all turns, retries and tools. Incomplete responses are errors even if a partial answer contains a verdict. Nested HTTP retries and redirects are disabled; artifactize handles turn retries as described in retries and error codes.

Transcript reader

Monitor Detail displays readable message text, public reasoning summaries and collapsed tool activity, not raw message envelopes or private reasoning payloads. Streaming updates replace their provisional text with final content; interrupted partial output is labeled. Public OpenAI/Codex summaries are read from streaming frames without changing the bytes passed to rig, the provider client library. Display-only events never enter replayed model history.

The reader follows new messages until you scroll away. Resize preserves the reading position while paused and stays at the bottom while following. Tool activity expands to names, targets and recorded states, not full tool arguments or successful output. session show --json exports the complete saved events. Missing local sessions, never-saved conversations and remote references are reported separately; a remote reference cannot open arbitrary local files.

History is indexed incrementally into private temporary text and row files, with only the visible pages loaded. There is no fixed first-2-MiB history cutoff. One complete event needs memory proportional to its size while being decoded; other history and layout work yields between bounded chunks. Complete malformed events report errors, while partial lines and incomplete UTF-8 wait for the next append. Replacement, truncation or deletion resets the reader; closing or resetting it removes its temporary files. Later session send events remain readable after the original review ends.

Local readers

Monitor and Review use read-only state queries and cached Git discovery, rather than rediscovering declarations or fingerprints on every refresh. Local readers share a temporary hub inside the first subscriber process: an owner-only Unix socket or Windows named pipe, not a permanent daemon. Registration is acknowledged before the initial snapshot. Readers reconnect and resync after hub exit or a bounded hint-queue overflow; writers never elect a hub.

If IPC is unsafe or unavailable, five-second authoritative reconciliation replaces it without an insecure transport fallback. A persistent read-only SQLite data_version connection and file-identity probe detect missed commits and database replacement. Clocks and spinners redraw cached data. Session hints wake only the selected transcript reader; a five-second file probe also catches missed appends or removal. Rendering does no file I/O, and input takes priority over history work. This is local coordination, not remote team-store push delivery.

The Run worker separately reconciles dead execution owners and backend slots and optional remote results, without a Human wait deadline. Explicit Human actions use the shared atomic claim, fingerprint recheck, schema validation and submit_and_publish APIs. Followers resolve to their original request. Publication failure after local settlement preserves and shows that settlement.

Local diagnostics and maintenance

artifactize doctor [--repo PATH] [--state-dir PATH] [--json]
artifactize models openai|anthropic|codex [--json]
artifactize prune [--older-than 7d] [--dry-run] [--state-dir PATH] [--json]

prune operates on the selected state’s Runs across repositories, not the current repository. Eligible Run statuses are exactly GREEN, RED, ERROR and INCOMPLETE, with a completion timestamp, only terminal requests, and no live execution owner (PID plus process start time). SIGNOFFS_PENDING Runs are always skipped, even after their Human requests settle: later submissions do not rewrite that historical Run status. Other unfinished Runs are also skipped. --older-than compares Run completion time; use a whole-number s, m, h, d or w duration. With no age filter, every eligible Run is considered. --dry-run returns wouldRemove without deleting; normal JSON returns removed, and both include skippedRuns. prune also collects the Agent session store under its size bounds, whatever --older-than says: removedSessions lists the deleted sessions, and with --dry-run wouldRemoveSessions those over the size bounds. A dry run does not check for a running session send, which an actual prune leaves alone, so the two can differ.

What doctor checks, the models output and exactly which directories prune removes are in the reference.

Doctor, models and prune

doctor makes no provider calls and creates no Run, verdict, cache entry or auth file. It reports the resolved state directory and tests writability with a temporary directory, removed immediately (in the nearest existing ancestor when state does not yet exist). Before the writable probe it enforces the same current/legacy workspace-marker safety boundary, even without --repo; unsafe state fails without writing a probe inside that workspace. It reads the state database’s schema, applying a supported additive upgrade if needed: a database written by an earlier artifactize (schemas 1–5) or by a newer one is a hard error (exit 1), with the message other commands refuse it with and its schema and the supported one in the details. --repo additionally runs the same static validation as config check. The limits check reads limits.json and reports the backend capacity (an invalid file is a hard error). The sessions check reports the Agent session store: how many sessions it holds and their bytes, maxBytes, targetBytes, whether saving is enabled, the store’s directory and the state’s stateId. API keys are reported only as present/absent, never validated or printed. Missing keys are warnings: optional backends need not all be configured. A backend with a test endpoint is a warning that reports it as testEndpoint; an invalid test endpoint is a hard error. The codex check reads the sign-in without a lock, refresh or network call: no sign-in is a warning, a stored sign-in passes even when its access token has expired (the next use refreshes it), and an expired ARTIFACTIZE_CODEX_AUTH_FILE token is a warning. A $STATE/auth inside a repository is a warning while no sign-in is stored there, with refused in the details holding the message, and a hard error once one is. Its details name the source (stored, file or none), expiresAt and expired, and testEndpoint/testAuthEndpoint when they are set. The remote review store check is also offline: it reports the resolved URL, share level and token source; a missing token is a warning, and an invalid configuration (including plain HTTP to a non-loopback host) or an unsafe token file is a hard error. Invalid config, unsafe/unwritable state or invalid auth storage are hard errors. Exit is 0 without hard errors, 1 with hard errors, and 2 for invocation errors.

models openai and models anthropic call the provider’s models endpoint with the corresponding API key through rig; Anthropic pagination is followed. models codex lists the Codex models of the signed-in account. A test endpoint replaces the provider’s API root here too. There is no account or backend fallback. Text lists tab-separated slug/name pairs; JSON is consistently {"backend":"openai","models":[{"slug":"model-id","display_name":"model-id"}]}. Listings preserve provider order.

Only known scratch directories below state/runs/<run-id> are removed: runtime output (and tmp, home and cache left by older versions) and leftover tool output. Run roots, unknown files/directories, database rows, results and reuse records remain. Symlinks (including nested links), non-directory targets and repository content are refused before deletion. .git and the current/legacy Artifact markers above protect workspace copies, including Git-less projects copied inside Run output. Both --dry-run and real prune enforce this boundary. Database reads have a five-second busy timeout and finish before deletion; prune holds no writer lock. This is plain prune, without quarantine, crash-recovery machinery or hostile filesystem-race protection. Saved run show and request show remain readable after pruning.