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

154 lines
8 KiB
Markdown

# 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.
```text
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
```text
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](RUN_STATE.md). 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.