TRAAVIIS TRAAVIIS/TRAAVIIS_EPISODE_KERNEL_MEMO.md
Slice: the kernel extracted before the transport, exactly in that order. Status: shipped.

Episode Kernel Closure — memo

Slice: the kernel extracted before the transport, exactly in that order. Status: shipped.

Battery. K1–K18 with the engine present: 18 passed / 0 skipped / 0 failed; engine absent: 17 passed / 1 skipped / 0 failed (only K14 packs a real template — see §3). Whole tree: 492 passed / 0 skipped / 0 failed across 27 files, up from 474/26.

Superseded in one place. The seventh GPT-5.6 ruling accepted these semantics but found that finalize was not linearizable: two concurrent callers could both run the verifier plan for one session. That is closed by the follow-on slice — see `TRAAVIIS_FINALIZE_LINEARIZATION_MEMO.md` and laws K19–K28. Everything below still holds; the session lifecycle described here now has four states rather than two.

Ruled by the sixth GPT-5.6 ruling, second half:

EpisodeKernelV1 { list_tasks() start(task_id) observe(session_id)
                  step(session_id, action) reset(session_id)
                  finalize(session_id, run_result) close(session_id) }

with session_id an ephemeral process handle, Residency v1 supporting only start + finalize, the local command runner demoted to an adapter that reproduces the current eval-one receipt byte for byte, and the process model one kernel = one admitted environment, many ephemeral sessions, one shared registry, one shared engine seam, no process-wide lock over a session lifetime.

1. Why the order is the whole point

A kernel written after a server is a description of what that server happened to need. A kernel written before one is a statement about what an episode is, which the server then has to translate into. The two artifacts can look identical and mean opposite things, and there is no test that distinguishes them after the fact — only the order does.

So this slice adds no capability. Nothing trvs can do today it could not do yesterday; there is no new verb, no new rung, no new receipt field. What changed is that "an episode is admission, then a run, then scoring" is now a shape with a name, rather than the order of statements inside one function.

The single most useful consequence is already visible and is not about servers at all: because observe is a refusal under Residency, a remote client cannot read a Residency session. So trvs serve --ors over Residency v1 can honestly expose only start + finalize. That is now a consequence of the kernel rather than a decision the server author would have had to make — and would probably have made the other way, by inventing a plausible observation.

2. What was built

filechange
traaviis/kernel.pynew. EpisodeKernelV1, ResidencyKernelV1, SessionV1, TaskEntryV1, KernelError, local_kernel, environment_kernel, run_episode.
traaviis/evalone.pypipeline split into _admit_episode / _invalid_config_run / _finish_episode; evaluate rewritten as the local command adapter. Public surface unchanged.
traaviis/evalsplit.pyeval_split opens one kernel for the environment; _run_one drives one ephemeral session per task.
test/test_kernel.pynew. K1–K18.
RFC_TRAAVIIS_ARTIFACTS.mdnew §4a freezes the kernel; §4 and §7 point at it.
README.md"the environment surface" is no longer a roadmap section.

Where the cut was made

At the exact runner.run_agent line inside evaluate. Everything above it is what an episode is — admission, cross-binding, policy honesty, verifier-version sealing, the F4 configuration preflight. Everything below is what came back. Lifting the subprocess out from between those two halves, without reordering either, is why the receipt is byte-identical by construction rather than by a comparison that happened to pass.

_admit_episode(task, content, reward_spec, …)  →  plan
        │                                            (no process launched yet)
        ├── plan["unresolved"]  →  _invalid_config_run(plan)      status=invalid
        │
        └── runner.run_agent(…)  →  _finish_episode(plan, run, …) → EvaluationRunV1

The interface is a declaration

EpisodeKernelV1 implements none of the seven operations and refuses all of them; support is supported_operations and nothing else. A base class that quietly implemented one would make "this substrate supports X" a property of which method a subclass remembered to leave alone. describe() reports {kernel_version, substrate_profile, operations}, so a client can ask what it is talking to before it asks for anything.

A session is not an identity

session-<hex>, freshly random per start. identity.py mints nothing for it, no artifact references it, and K5 proves it reaches neither the run document, nor the canonical receipt bytes, nor any file name or content inside a written episode bundle. Two start calls on one task return two handles and one episode-….

Refusals, not no-ops

codewhen
KERNEL_OPERATION_UNSUPPORTEDobserve / step / reset on Residency; anything on the base
KERNEL_TASK_UNKNOWNstart / entry for a task outside the closure
KERNEL_SESSION_UNKNOWNa released or never-issued handle
KERNEL_SESSION_STATEfinalizing a finalized session
KERNEL_RUN_RESULT_MISSINGa runnable session finalized with None
KERNEL_RUN_RESULT_UNEXPECTEDa non-runnable session finalized with a RunResult
KERNEL_SUBSTRATE_UNSUPPORTEDenvironment_kernel over trvm.world.v1
KERNEL_REWARD_UNRESOLVEDa task whose reward is not in the package
KERNEL_TASK_UNIDENTIFIEDlocal_kernel over a task no task-… can be derived from

KernelError inherits from both substrates.AdmissionError (typed: code, message, detail) and admission.AdmissionError (the plain one), so every pre-existing except clause keeps its meaning. That matters concretely: evalsplit._run_one catches admission.AdmissionError in order to record a failed episode rather than abandon the split, and a kernel refusal must not change one bad task into an aborted run.

3. K1–K18

#lawtest
K1the interface is the frozen seven; the base refuses all of themtest_k1_the_interface_is_the_frozen_seven_and_the_base_refuses_all_of_them
K2Residency supports list_tasks/start/finalize/close, refuses the resttest_k2_residency_supports_start_and_finalize_and_refuses_the_rest
K3the interactive trio are refusals, never no-opstest_k3_the_interactive_operations_are_refusals_never_no_ops
K4a session id is an ephemeral handle, not an artifact idtest_k4_a_session_id_is_an_ephemeral_handle_not_an_artifact_id
K5a session id is never persisted into an episodetest_k5_a_session_id_is_never_persisted_into_an_episode
K6a session is ephemeral and close is idempotenttest_k6_a_session_is_ephemeral_and_close_is_idempotent
K7finalize refuses an unknown or already-finalized sessiontest_k7_finalize_refuses_an_unknown_or_already_finalized_session
K8finalize refuses a missing or an unexpected run resulttest_k8_finalize_refuses_a_missing_or_unexpected_run_result
K9start refuses an unknown task and opens no sessiontest_k9_start_refuses_an_unknown_task_and_opens_no_session
K10three paths to an episode produce the identical receipttest_k10_every_path_to_an_episode_produces_the_identical_receipt
K11a kernel episode replays to the identical receipttest_k11_a_kernel_episode_replays_to_the_identical_receipt
K12the subprocess boundary has exactly one doortest_k12_the_subprocess_boundary_has_exactly_one_door
K13an invalid-config session opens, runs nothing, and still scorestest_k13_an_invalid_config_session_opens_runs_nothing_and_still_scores
K14one kernel serves one admitted environmenttest_k14_one_kernel_serves_one_admitted_environment
K15many ephemeral sessions may be open at oncetest_k15_many_ephemeral_sessions_may_be_open_at_once
K16no process-wide lock is held over a session lifetimetest_k16_no_process_wide_lock_is_held_over_a_session_lifetime
K17a substrate with no episode semantics is refused by nametest_k17_a_substrate_with_no_episode_semantics_is_refused_by_name
K18the ladder, the CLI and the earlier laws are untouchedtest_k18_the_ladder_the_cli_and_the_earlier_laws_are_untouched

Only K14 needs a Forge checkout, because it is the only law that is genuinely about a packed environment: it scaffolds and packs the real residency-repair template, spies environment_kernel and ResidencyKernelV1.start, and asserts one kernel object, one session per task, distinct handles, and an empty session table at the end. Everything else runs against the deterministic stub agent with injected verifiers.

K10 is the ruled byte-for-byte law, taken from three independent directions: the adapter (evaluate), the receipt-only wrapper (eval_one), and a hand-driven start → run_agent → finalize. K11 is a fourth and better one, because it goes through code this slice never touched: verify_episode_bundle rebuilds a fresh receipt from saved evidence through the same build_receipt_v1, with no kernel anywhere in the path, and it comes back identical.

K16 is the process-model law with teeth: it acquires the kernel's lock non-blockingly from a second thread while a session is open, and additionally counts with self._lock: blocks in the source — four, all of them around table bookkeeping, none around admission, scoring, or a subprocess.

4. A defect this slice created and then closed

Three K-laws about where code may appear were first written as textual source scans, and all three were wrong for one reason: a module is allowed to name a seam it deliberately does not cross. evalone.evaluate's docstring says it drives runner.run_agent; kernel.py's docstring says a receipt is built by exactly one piece of code, build_receipt_v1. Those sentences are the statement of the boundary. A "run_agent" not in source test called them crossings, so K10, K12 and K18 all failed against code that was correct.

Closed by parsing rather than grepping. _run_agent_calls walks the AST and reports ast.Call nodes only; _identifiers collects the names a module actually references. The rewritten laws say what they meant, and are strictly stronger than the scans they replace: a call spelled across two lines, or reached through an alias, would have walked straight past a line-oriented "run_agent(" in line test that reported itself green.

Some textual checks survive on purpose, because for them the text is the subject. K4 and K18 assert that the strings session-, session and kernel appear nowhere in identity.py, and that kernel appears nowhere in cli.py — the point is precisely that the ladder module and the command surface must not so much as mention the new construct, including in a comment somebody later uncomments. K16 counts with self._lock: blocks and reads the head of each, which is a statement about lexical scope and has no behavioral phrasing.

This is the same shape as the two previous slices' defects: a claim checked against a rendering of the code rather than against the code. One unrelated harness error was fixed alongside — K14 asked the scaffolded template for a train split, which it does not declare; the template declares all.

5. Autonomous decisions (flagged for review)

  1. `list_tasks` and `close` are first-class kernel operations, not helpers.

The ruling names all seven; RFC §4's older sketch listed five. §4a states the built seven and says so explicitly.

  1. `KernelError` inherits from both `AdmissionError` classes. The package has

two, and a refusal that only satisfied one would silently change evalsplit's per-task failure handling into an aborted split.

  1. An invalid configuration still opens a session (runnable = False) and is

scored at finalize(sid, None). Refusing to open it would turn a scored status=invalid outcome into a crash, which is a different claim about the task.

  1. The kernel never learns the agent command. start(task_id) matches the

ruled signature exactly; the kernel hands out content + policy and consumes a RunResult. This is what leaves serve --ors a translation layer rather than a second runner.

  1. **run_episode(kernel, task_id, agent_command) is a module function, not a

method.** It is the adapter, and the ruling puts the runner outside the kernel; making it a method would put the subprocess back inside.

  1. **evalone imports nothing from kernel at module scope; kernel imports

the three phase functions from evalone.** The dependency points one way, and evaluate takes the lazy in-function import. The alternative — a fourth module holding the phases — moves more code for the same graph.

  1. `eval_split` now emits every task's wiring note at kernel construction,

before the first agent runs, rather than interleaved with episodes. This is a side effect of admitting the environment once. It is arguably an improvement (all configuration problems are visible before any agent starts) and it is consistent with the module's own fail-before-launch doctrine, but it is an observable change in output order, so it is flagged.

  1. `ResidencyKernelV1.close` returns a bool (was this handle open?) rather

than raising on an unknown id. Releasing a handle twice is not an error, and an adapter that closes in a finally must not raise over an already-released session.

6. Cost

None measurable: the same work happens in the same order. The refactor costs one extra object per episode and one dictionary insertion and deletion per session. eval_split now holds all task entries for the environment in memory for the duration of the split rather than assembling each in turn — for the environments that exist, that is a task document, a reward spec and a snapshot per task.

7. Not started (deferred by ruling)

Next is `trvs serve --ors` as a translation layer over this kernel — which, per §1, can honestly expose only start + finalize for Residency v1 and must relay KERNEL_OPERATION_UNSUPPORTED for the rest.

Still deferred: MCP, batch-evidence distribution identity, eval-…, agent-…, the REPL, EvaluationV2.

Open in the interactive atlas