Capture all successful state reads with hashes and session-local IDs. Record action intent before POST, retain accepted/rejected/unknown results, and link subsequent observations. Preserve legacy feeds and finalize each invocation synchronously.
8 KiB
Linked session recordings
Files and ownership
recording.SessionRecorder owns one recording per run.main() invocation.
It has no game or model client. It records and invokes an action callback supplied
by the runner. Policy execution remains in the runner.
capture/
decisions.jsonl compatibility feed
sessions.jsonl compatibility session summaries
sessions/<session-id>/
events.jsonl authoritative, ordered journal
session.json finalized session summary
observations/000001.json full parsed observation
observations/000002.json
...
The session ID combines a timestamp with a random UUID. It is generated inside each invocation, not once per Python process. Session directories and observation files are created exclusively. Later sessions cannot overwrite earlier captures. The default session artifacts are ignored by version control.
All successful state reads are captured, including preflight, menus, overlays,
selection screens, rewards, and terminal screens. Identical observations remain
separate reads. The old live_<step>_combat.json files are no longer written.
Observation files contain the unmodified parsed state dictionary. They are not copies of HTTP headers or the original wire encoding. Each observation event contains the relative path and SHA-256 hash of the stored bytes. Paths are relative to the configured capture root.
Journal schema, version 1
Each journal row includes schema_version, session, sequence, recorded_at
(UTC), and the legacy local-time ts display field. Sequence orders events
within one session. It is not an ordering across processes.
| Event | Purpose and links |
|---|---|
session_start |
Establish the session before preflight or model initialization. |
observation |
Full state reference: observation_id, path, sha256, step, phase, and state type. |
proposal |
Policy/system decision, linked by proposal_id and observation_id. Includes action, parameters, reason, source, and available model evidence. |
proposal_status |
Disposition of a proposal: wait, suppressed, dry_run, execute, or session_stopped. |
action_attempt |
Intent to submit one request, linked to its proposal and input observation. Has a distinct attempt_id. |
action_result |
Response to an attempt: accepted, rejected, or unknown. Response/message or transport error is retained. |
session_end |
Final exit code, reason, outcome-file candidates, and any attempt still lacking a subsequent observation. |
Existing diagnostic events, such as model_error, action_error, and identity
changes, also appear in the journal. observation_error records a failed state
read without inventing an observation. Its optional observation reference is the
last successful read, not a payload for the failed request.
Join direction
observation.observation_id
<- proposal.observation_id
<- action_attempt.proposal_id
<- action_result.attempt_id
<- subsequent observation.after_attempt_id
The first successful read after an attempt carries after_attempt_id. Later
reads do not reuse that field until another attempt occurs. A terminal dismissal
may have no subsequent read. The session summary explicitly preserves that
unresolved link in awaiting_observation_after_attempt.
The subsequent observation is temporal evidence, not proof of causation or completion. A transition overlay can be that first read while policy memory continues waiting. The journal does not invent a semantic success label.
Rejected requests are attempts too. Waits, suppressed duplicates, and dry-run previews have proposals and dispositions, but no attempt or action result. Dry-run proposals do not update execution-based duplicate suppression.
Preflight dismissal and stop-on-run-end dismissal use the same recording path as policy actions. Their observations, proposals, attempts, and results are linked.
Model evidence
Successful model calls retain the request state, questions, and parsed answers
with the proposal. Failed model calls retain the request with model_error.
This is not a raw HTTP transcript or a ledger of the model client's internal retries.
answer_gate_scope: default_helper_not_policy_gate labels the existing
per-answer gated field. That value does not establish the actual gate used by
a particular policy handler. Actual policy-gate metadata remains separate work.
No correctness label is inferred from confidence, action acceptance, or a run outcome.
Reported run IDs and card provenance remain subject to the limits in run-state documentation. The recorder does not strengthen save-derived identity into an atomic observation identity.
Durability and failure behavior
Observation files are flushed before their references are used. The journal's action intent is flushed before the runner calls the game transport. A recording failure before that point prevents the POST.
A transport failure has an unknown result and is not retried automatically.
If recording fails after a POST, its effect can remain unknown. A missing result
must never be interpreted as rejection or permission to retry.
Finalization runs synchronously when the session scope exits. It covers normal
returns, startup failures, handled runtime failures, Python exceptions, and
keyboard interruption. There is no process-global session ID or atexit callback.
SIGTERM, SIGKILL, power failure, and storage failure can still leave an incomplete journal or no summary. An intent can exist even if the process died before the POST began. A file flush is not a transactional guarantee across the journal, observation files, and game service. Readers must treat incomplete records and truncated final lines as incomplete evidence, not as successful execution.
Compatibility and dataset boundaries
The root decisions.jsonl retains event: decide rows for unsuppressed actions
and previews. These carry the same IDs as their canonical proposals. Wait and
suppressed proposals appear only in the session journal. Do not count both a
proposal and its compatibility decide row as separate policy decisions.
The root session feed retains the outcome-file list and points to its journal
through recording_path. The historical outcome association remains a filename
set difference; it is not proof that every new game history file belongs to this bot.
An attribution failure is recorded as outcome_error without replacing the session's exit status.
Compatibility feeds are not transactional or coordinated across writers. Use per-session journals for reliable joins when multiple processes share a capture root. Full observations and file flushes add storage and I/O cost intentionally.
Existing captures and dataset/ are unchanged. The current dataset migrator
still reads historical inputs and flat captures; it does not import these new
journals automatically. A journal importer and semantic reconciliation labels
can be added separately without rebuilding data during development.
Validation
- The backend-only recording bookmark passed 294 assertions: 68 facts, 110 policy, and 116 runner assertions. With the separate upgrade-selection fix applied, the integrated workspace passed 296 assertions (118 runner assertions).
- Runner flows verify hashes, unchanged observation contents, all join keys, preflight/terminal actions, rejection, unknown transport outcomes, waits, duplicate suppression, repeated invocations, and keyboard interruption.
- Twenty-five distinct temporary whole-process scenarios passed with real HTTP clients and local fixture servers. The server checked that each action intent existed on disk before it received the action POST.
- Injected journal failures before and after POST verify conservative stopping.
- All prior semantic code bookmarks passed their three Python test scripts.
- Dataset integrity, shell syntax, and offline snapshot replay passed.
No live game actions, paid model calls, new dependencies, or permanent standalone unit-test files were needed.