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.
This commit is contained in:
parent
ba519a850e
commit
9696282110
10 changed files with 476 additions and 123 deletions
154
docs/RECORDING.md
Normal file
154
docs/RECORDING.md
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
# 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue