fix(session): honor config.session.metadata on session:start in the Rust kernel - #103
Conversation
…ust kernel
`docs/specs/CONTRIBUTION_CHANNELS.md` lists `session.metadata` as a runtime
passthrough channel; `tests/test_session_metadata.py` specifies it; and
`python/amplifier_core/session.py` implements it. The Rust switchover shipped
without it.
Production does `from amplifier_core import AmplifierSession`, which is
`RustSession` (`python/amplifier_core/__init__.py`). Its lifecycle emit built
the payload from `{session_id, parent_id}` only, so configured metadata was
dropped before any hook saw it. The pure-Rust kernel had the same omission.
`session:fork` was unaffected -- it is emitted by the shared Python helper
`_session_init.py`, which already honored the contract. So one contract had
three payload constructions and two of them disagreed with it.
The metadata tests were green throughout, because they import the *pure-Python*
`amplifier_core.session.AmplifierSession` -- the class no runtime consumer
executes. That blind spot is what let this ship dead.
- `session_metadata_passthrough()` in `crates/amplifier-core/src/session.rs`:
one shared reader, so the Rust emit paths cannot drift apart again. Mirrors
the Python kernel's `if session_metadata:` guard, so absent *or* empty
metadata leaves the payload byte-identical to before.
- Merged into the bindings emit (cached at construction alongside
`session_id`/`parent_id`) and into the pure-Rust emit.
- `bindings/python/tests/test_session_metadata_rust.py`: the parallel suite
against `RustSession` that the existing tests could not provide.
Additive only. No new event, no new top-level payload key, no schema change.
Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
|
PARKED at the maintainer's request (2026-08-20) — amplifier-core cannot change right now. Work is complete and live-proven (see body); converted to draft to prevent accidental merge. Unpark order: this PR first, then amplifier-app-cli #278. Tracking: microsoft/amplifier-bundle-attractor#308. |
|
This PR's head The carrier retains the complete change and passed the combined-state native, consumer, and default Docker smoke gates after CLI companion microsoft/amplifier-app-cli#358 landed. See the combined validation and source receipt and the migration notes in #109. No package was published by this review lane. |
Parity bug fix, not a new feature
session.metadatais an already-specified, already-tested, already-used channel. It isdead at runtime.
docs/specs/CONTRIBUTION_CHANNELS.md:76—session.metadata – runtime metadata snapshots.tests/test_session_metadata.py— "CP-SM: Kernel readsconfig.session.metadataand includes it as optional'metadata'key in event payloads. Pure passthrough." Green today.python/amplifier_core/session.py:154-165— implements it.bindings/python/src/session.rs:389-392— builds{session_id, parent_id}. Metadata dropped.crates/amplifier-core/src/session.rs:318-324— same omission.session:forkpython/amplifier_core/_session_init.py:274-291— already correct (shared Python helper, both session classes delegate to it).Production does
from amplifier_core import AmplifierSession, which isRustSession(
python/amplifier_core/__init__.py:16). So one contract has three independent payloadconstructions, and two of them disagreed with it — on
session:startandsession:resumespecifically, while
session:forkquietly kept working.Why the tests didn't catch it:
tests/test_session_metadata.py:13importsfrom amplifier_core.session import AmplifierSession— the pure-Python class. Themetadata tests have been passing, green, on code that no runtime consumer executes. That
switchover blind spot is the actual defect; the missing merge is just its first casualty.
Dead-channel evidence
Measured on a real corpus on one machine (77,553
events.jsonlfiles under~/.amplifier/projects/): across a 3,000-file random draw, 2,983session:startrecords were sampled and 0 carried any
metadatakey. Observeddatakey-sets:amplifier-app-clihas been writingsession.metadatafor spawned children sincesession_spawner.py:440-449("enables kernel CP-SM passthrough on session:start/fork").It has been landing on
session:forkand vanishing onsession:start.The change (~15 LOC of product code)
crates/amplifier-core/src/session.rs—session_metadata_passthrough(), one sharedreader for both Rust emit paths so they cannot drift apart again. Its emptiness guard
mirrors the Python kernel's
if session_metadata:truthiness test, so absent or emptymetadata leaves the payload byte-identical to today.
bindings/python/src/session.rs— cachesession.metadatainnew()alongsidecached_session_id/cached_parent_id, merge it intopre_event_data.crates/amplifier-core/src/session.rs— same merge on the pure-Rust emit.No new event. No new top-level payload key. No change to
crates/amplifier-core/src/events.rs.Tests
bindings/python/tests/test_session_metadata_rust.py(new, 8 tests) — the parallelsuite against
RustSessionthat the existing tests could not provide: start/resume withand without metadata, empty-metadata parity with the Python guard, verbatim nested
passthrough,
session_id/parent_idnot displaced, andsession:forkpinned.crates/amplifier-core/src/session.rs(4 new Rust tests) — the passthrough reader(configured / absent /
{}/[]/""/null) plussession:startemitted with andwithout metadata.
CI that will run on this PR (
.github/workflows/rust-core-ci.yml):rust-tests(cargo test / check / fmt / clippy),
node-tests, andpython-testson 3.11 / 3.12 / 3.13.proto-check.ymlis untouched by this diff (no.protochanges).Back-compat
(
test_session_metadata.py,test_session_metadata_rust.py) and in Rust(
execute_omits_metadata_when_not_configured). Empty metadata is treated exactly likeabsent, matching the Python guard.
config.session.metadatais non-empty, so callers that configure nothing get today's payload byte-for-byte.
data.metadatabecausemetadatais notin hooks-logging's promoted-key list — no hooks-logging change is required or included.
events.jsonlstays exactly as itis. This improves future data only.
Live proof
Built from this branch (
maturin build --release), installed into a scratch venv togetherwith the companion app-cli change (microsoft/amplifier-app-cli#278), then one real
amplifier run --mode single "..."session. The persisted~/.amplifier/projects/<slug>/sessions/<id>/events.jsonlsession:startline, verbatimapart from formatting:
{"ts":"...","lvl":"INFO","schema":{"name":"amplifier.log","ver":"1.0.0"}, "event":"session:start","redaction":{"applied":true,"rules":["secrets","pii-basic"]}, "session_id":"a0122cbb-6438-4014-99cb-c63fefef9985", "data":{"metadata":{"invocation":{"launched_by":"cli","launched_by_session_id":null, "mode":"single","schema":1,"stdin_isatty":false,"stdout_isatty":false}},"parent_id":null}}Negative control — same app-cli, released
amplifier-core==1.6.0swapped back in, samecommand:
{"ts":"...","lvl":"INFO","schema":{"name":"amplifier.log","ver":"1.0.0"}, "event":"session:start","redaction":{"applied":true,"rules":["secrets","pii-basic"]}, "session_id":"bbbbd7c4-8a1b-4b26-ba73-edfcbf9dc577","data":{"parent_id":null}}Metadata configured on the mount plan, dropped by the kernel — the bug, reproduced on
demand. Note also that hooks-logging needed zero changes:
metadataauto-nests underdata.Notes for review
any kernel payload contract, not just this one. Fixing it broadly — e.g. parametrizing
the payload suites across both classes — is out of scope here, but worth a decision.
the same trap armed for the next field added to
session:start.Co-Authored-By: Amplifier 240397093+microsoft-amplifier@users.noreply.github.com