sts2-bot/docs/RECORDING.md
0xrsydn 9696282110 feat(recording): link session observations proposals and action results
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.
2026-09-22 15:53:14 +07:00

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.