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.
154 lines
8 KiB
Markdown
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.
|