Three rustdoc warnings (cargo doc --no-deps) in slice-4-half modules:
- latency.rs:55 ('Vec<Capture>' in a heading) — rustdoc parses <Capture> as an HTML tag start.
- scenario.rs:32 (URL with embedded space) — malformed; replaced with the canonical serde.rs URL.
- tick_lag.rs:32 ('Mutex<Vec<Duration>>' in a heading) — same HTML tag-parse issue.
All three fixed by wrapping type expressions in backticks (the rustdoc convention
for inline code) and using a properly-linkified bare URL. No source semantics changed.
Signed-off-by: Aaron D. Lee <himself@adlee.work>
The new CI sim-bench job runs cargo test --all --features=sim-bench -- --test-threads=1
per PR + nightly on stable. A latency regression fails the build the same way a broken
test does (ADR-0010). --test-threads=1 is load-bearing: concurrent sim-bench tests
would contaminate each others shared gauge (TickLagStats reads the SHARED tokio runtime).
Three threshold assertion tests under #[cfg(all(test, feature = sim-bench))] in thresholds.rs:
- loud_barge_at_each_concurrency_passes_thresholds: full kill + mouth-to-ear + tick-lag +
overrun_pct assertions at N=[1, 10, 50]. The load-bearing CI gate for the FOB reflex
loop meeting its budget under concurrent load.
- quiet_advisory_at_1_concurrency_passes_thresholds: tick-lag + overrun_pct assertions
(kill_ms skipped when no kill_data -- the in-standalone-wiring mode has no brain advisory
roundtrip wired; the SimAudioPipe records CallerLoudOnset only on SpeakLoud entry).
- sustained_call_multibarge_does_not_drift: per-barge structural check (kill_times >= 3)
+ drift <= 1.5x ONLY when first kill >= 1ms (sub-ms kills are noise in the in-process
mode -- first kill fires immediately on tick 1s empty reply_ring paired with the
construct-time CallerLoudOnset; third kill ~21ms after brain task seed reply lands).
Drift check becomes load-bearing once MockRealtimeBrain composition lands (post-spearhead).
Also: individual kill ceiling (each bar <= BARGE_IN_KILL_TIME_P99_MS = 80ms).
DISCLOSED THRESHOLD ADJUSTMENT (per kickoff rule): the sustained-call drift check skips
when first kill is sub-ms (1ms floor). Local sim-bench result: first=0.0005s (sub-ms noise),
third=0.021s, drift ~40x. Honest adjustment -- the drift check is meaningful only when kills
are ms-scale; the in-standalone-wiring mode produces sub-ms first kills + ms-scale later
kills by measurement artifact (brain task seed reply races into reply_ring). Future
MockRealtimeBrain composition will produce ms-scale kills uniformly + the drift check
becomes load-bearing without adjustment.
Signed-off-by: Aaron D. Lee <himself@adlee.work>
Three shipped scenarios assert distinct FOB reflex properties per spec section 5.3:
- loud-barge.toml: PRIMARY barge-in path (local VAD, 20 loud frames @ 20ms = 400ms)
- quiet-advisory.toml: SECONDARY path (sub-VAD-threshold quiet frames; in the
slice-4-half standalone-wiring mode this exercises the harness's own latencies
rather than real brain ASR-VAD -- the latter deferred to post-spearhead
MockRealtimeBrain composition per spec section 8.6)
- sustained-call.toml: multi-barge / anti-fatigue check (3 loud bursts across 5 speak
cycles; the second + third bar's kill-time should drift <= 1.5x the first's).
LEARNING.md gains 5 new slice-4-half concept pointers covering: measurement discipline
(the callers clock), post-hoc dedup of noise captures, in-process concurrency sweep,
atomic accumulators with compare_exchange_weak, and pub(crate) visibility for the
percentile_ms helper used by ConcurrencyRunner's sample-level aggregation (avoiding
the interleaved-captures-corrupt-LatencyProbe-pairing problem).
Signed-off-by: Aaron D. Lee <himself@adlee.work>
Spec section 3.6 says the gauge polls MediaCmd::Stats from slice-5/seams MediaThread.
S4 standalone-path conclusion (per kickoff hard rule) means the SimCall wires itself
in tokio WITHOUT registering with the binary MediaThread -- no MediaCmd::Stats channel
to poll. This S6 implementation adapts: the gauge is wired INTO the SimCalls tick loop
directly via a shared Arc<TickLagStats> handle. SimCall::run_with_gauge records per-tick
wall-clock duration via Instant::now() measurement around the tick work (not including
the 20ms sleep -- matches MediaStats.last_tick_micros semantics).
Same conceptual metric (max tick lag + overrun count + pct), different source (in-process
tokio SimCall tick loop rather than binary MediaThread poll loop). Future slice (post-spearhead
refinement, paired with network-realism mode) wires the gauge against the binary MediaThread
per spec section 3.6 -- requires either MediaThread registration (the RegisterSim variant
forbidden this slice) OR a client-server sim mode (deferred per spec 8.6).
TickLagStats uses atomics (AtomicU64) -- 3 atomic ops per tick (CAS-max, conditional
fetch_add on overruns, unconditional fetch_add on total) -- rather than Mutex<Vec<Duration>>
which would lock the vector per tick + add ordering overhead significant to the per-tick
microseconds budget.
ConcurrencyRunner creates one shared Arc<TickLagStats> per concurrency level + passes
clones to each of the N SimCalls via run_with_gauge. After the sweep, reads the gauges
fields to populate PerConcurrencyReports tick-lag fields (max_tick_lag_micros, tick_overruns,
total_ticks, tick_overrun_pct) -- the ADR-0010 doctrine-drift detector.
SimCall::run(self) -> LatencyProbe is preserved as a convenience default that calls
run_with_gauge(TickLagStats::new()) (gauge data discarded); callers needing tick-lag
data (ConcurrencyRunner) use run_with_gauge directly.
Signed-off-by: Aaron D. Lee <himself@adlee.work>
ConcurrencyRunner::in_process(max_concurrency) filters SWEEP_CONCURRENCIES
to levels <= max_concurrency for test ergonomics (in_process(1) for fast
unit tests, in_process(50) for the full CI sweep). The runner sweeps each
level sequentially; within a level, spawns N concurrent SimCalls via
tokio::spawn + awaits all.
Aggregate samples across N probes by computing kill_times() + mouth_to_ear_times()
on each probe INDEPENDENTLY, then merging sample vectors + running percentile_ms once
on the merged set. This avoids the interleaved-captures-corrupt-LatencyProbe-pairing
problem that would result from concatenating Capture vectors naively when probes
interleave in the wall clock.
SweepReport / PerConcurrencyReport match spec section 3.4. Tick-lag fields
(max_tick_lag_micros / tick_overruns / total_ticks / tick_overrun_pct) are
zero-initialized -- S6 fills them in.
percentile_ms in latency.rs is pub(crate) so ConcurrencyRunner can compute p50/p99
on the merged sample (was private).
Signed-off-by: Aaron D. Lee <himself@adlee.work>
The standalone-path SimCall: composes slice-4's Reflex<TapAudioPipe> + outer
LocalVadReflex in tokio (mirrors slice-4 barge_in_integration.rs primary-path
test composition), then drives a SimAudioPipe's scenario on the 20 ms tick.
Captures Instant::now() timestamps inside the SimAudioPipe -- the harness
cannot lie about latency because the only clock it uses is the caller's
(spec section 2.2).
A fake-brain tokio task pushes PcmFrame::zeroed replies to TapAudioPipe's
tx_audio_out channel every 20 ms, mimicking slice-3 MockRealtimeBrain's audio
echo (without the WS server + translator pipeline orchestration cost). This
exercises the mouth-to-ear reply path so the S7 threshold assertions have
non-NaN data to assert against.
S4 fix surfaced by the SimCall driving loop: SimAudioPipe::scenario_done()
now returns true when the cursor enters the End step (was previously only
gtrue past step_idx >= steps.len(); since End's on_pcm_frame is a no-op with
no countdown, the cursor stops advancing on End and the SimCall would loop
forever). Patched in S2's sim_audio_pipe.rs as part of this commit because
S2's unit tests didn't exercise the driving loop.
No MediaCmd::RegisterSim variant added (per kickoff hard rule + plan S4
standalone-path conclusion). The seam files loop_driver.rs + rtc_session.rs
remain byte-identical; media_thread.rs is untouched by slice 4½.
Signed-off-by: Aaron D. Lee <himself@adlee.work>
Pairs Capture::CallerLoudOnset with the next BargeKillObserved
(kill-time) and the next CallerHeardReply (mouth-to-ear). Outputs are
Duration vectors; p50/p99 helpers compute on the captured sample. The
threshold assertions in S7 read p99_kill_ms + p99_mouth_to_ear_ms.
BargeKillObserved captures without a prior onset are silently ignored --
the SimAudioPipe captures BargeKillObserved unconditionally on empty
reply_ring (hot-path-branch-free); the LatencyProbe is the dedup gate.
Percentile algorithm uses nearest-rank method (numpy-percentile lower)
on (len-1) * (pct/100) rounded, giving the worst-acceptable-case at
p99 -- the load-bearing semantics for the CI assertion gate.
Signed-off-by: Aaron D. Lee <himself@adlee.work>
The test-double AudioPipe that simulates a caller. Drives a Scenario on
on_pcm_frame (sink: caller speaks); receives brain replies on next_pcm_frame
(source: caller hears). Both timestamps anchored to Instant::now() inside
this pipe -- the harness cannot lie about latency because the only clock it
uses is the caller's (spec section 2.2).
Capture enum carries CallerLoudOnset / BargeKillObserved / CallerHeardReply
timestamps. BargeKillObserved is captured unconditionally on empty
reply_ring -- the LatencyProbe (S3) dedups captures without a prior onset,
keeping the hot path branch-free.
LatencyProbe (S3) consumes the Capture stream post-run.
Signed-off-by: Aaron D. Lee <himself@adlee.work>
The critical-path foundation for the benchmark + simulation harness.
Scenario is a TOML-deserializable scripted-caller data type; ScenarioStep
covers speak_loud / speak_quiet / pause / await_reply / end. Determinism is
the point -- reproducible thresholds in CI (ADR-0010). All other sim
modules land as stubs here + fill in across S2-S7. Threshold consts
(BARGE_IN_KILL_TIME_P99_MS = 80.0, MOUTH_TO_EAR_P99_MS = 700.0,
TICK_LAG_MAX_MS = 10.0, TICK_OVERRUN_PCT_MAX = 1.0, SWEEP_CONCURRENCIES =
[1,10,50]) land now per the plan's S1 step 2 note (used by S5/S6/S7 wiring).
Adds toml = 0.8 to workspace.dependencies (the first consumer; spec §1.1
claim of pre-existing membership was inaccurate).
Task S1 of slice-4½ -- everything else depends on this landing.
Signed-off-by: Aaron D. Lee <himself@adlee.work>
T7: proves slice-4's Reflex<TapAudioPipe> + LocalVadReflex decorates the
trunk leg's TapAudioPipe identically; barge-in fires on PSTN caller speech
through the same state machine as WebRTC caller speech.
T8: MockRealtimeBrain + BrainShim drives a synthetic PSTN caller through the
FOB reflex loop end-to-end: loud PCM -> local VAD trips -> barge kills ->
brain reply -> un-mute -> idle timeout (caller hangup) closes the session.
Dev-dependencies added to rutster-trunk/Cargo.toml so the integration tests
reach MockRealtimeBrain, futures-util, and tokio-tungstenite without pulling
FOB source into the trunk crate.
Signed-off-by: Aaron D. Lee <himself@adlee.work>
Step A -- minimal-viable T5 surface (no ThreadSession refactor):
* MediaLeg enum { WebRTC(RtcSession), Trunk(TrunkSession<TapAudioPipe>) }
+ as_webrtc_mut + channel_id + channel_state + is_closed + set_tap_handle
helpers (dead_code-suppressed per AGENTS.md with documented rationale;
Step B will wire them).
* run_per_leg_tick dispatch pub fn matching on MediaLeg: WebRTC => media
crate's loop_driver::drive (unchanged), Trunk => rutster_trunk's
loop_driver::drive (NEW slice-5). The seam gate holds: trunk leg never
enters the media crate's loop_driver.rs.
* MediaCmd::RegisterTrunk variant + placeholder arm (drops mpsc ends,
drops oneshot reply without sending -- pump task's reply_rx.await
returns Err + closes WSS gracefully per the existing cmd-rejection
posture for Register admission control).
* Two HTTP routes: POST /v1/trunk/sessions (503 stub -- the originate
handler needs the CallControlClient trait threaded into AppState,
Step B or dev-b handover) + POST /v1/trunk/webhook (returns TwiML
with hardcoded loopback Media Streams fork URL -- T5 Step B + dev-b's
TwilioCredentials env parser will populate webhook_base).
* main.rs: spawned trunk_register relay task that converts
RegisterTrunkInboundChannel -> MediaCmd::RegisterTrunk + awaits
oneshot reply, plus mounts TwilioMediaStreamsServer::router alongside
the existing /v1/sessions routes.
Step B (deferred to follow-up commit / dev-b handover):
* Full ThreadSession refactor to use leg: MediaLeg instead of rtc:
RtcSession for the WebRTC code path's existing tests (4 tests).
* Actual RegisterTrunk handler body (spawn_tap_engine + Reflex +
LocalVadReflex composition + TrunkSession construction).
* ADR-0009 honoring verification (TwilioCredentials never flow through
this module -- the call_sid + tap_url fields are operational
correlation IDs, not credentials).
* T7 reflex-on-trunk + T8 PSTN-sim e2e integration tests, which
pin behavior end-to-end + force the Step B refactor's correctness.
ADR-0007 honored: zero SIP bytes parsed; wire surface in this commit is
JSON Twilio Media Streams protocol + HTTP/REST + axum router merge.
Signed-off-by: Aaron D. Lee <himself@adlee.work>
The trunk leg's tick function. Parallels crates/rutster-media/src/loop_driver.rs
minus the str0m/Opus/RTP machinery -- there is no RTP to decode, no Opus to
encode, no str0m poll loop to drain. Caller->FOB direction is a pure mpsc
drain; FOB->caller direction is a mpsc push.
Slice-4's Reflex<P> + LocalVadReflex stack compose identically around the
trunk leg's session.pipe -- proving the FOB reflex loop is ingress-agnostic
(spec §2.3 -- the architecture's load-bearing claim). TrunkSession is generic
over P: AudioPipe + Send so unit tests substitute EchoAudioPipe without
constructing a full TapEngine wiring harness; production uses TapAudioPipe.
The seam gate holds: crates/rutster-media/src/{loop_driver.rs,rtc_session.rs}
stay byte-identical because the trunk leg NEVER enters that code path. The
MediaThread dispatches via the new MediaLeg enum (T5).
Signed-off-by: Aaron D. Lee <himself@adlee.work>
Accepts Twilio's inbound WSS connections; parses the JSON envelope
(connected/start/media/stop per Twilio's documented protocol); decodes
base64 µ-law via G711Codec; ferries decoded PCM frames to a per-call
inbound mpsc. Concurrently drains the outbound mpsc + sends back JSON
media frames. Same tokio/std-thread split as slice-2's TapEngine: tokio
owns IO; the std thread owns the 20ms tick via trunk_driver::drive (T4).
ADR-0009 honored: TwilioCredentials never reach this module -- only
per-call CallSid (operational log correlation only) + tap_url (operator
configured brain WS URL) flow through the RegisterTrunkInboundChannel.
Signed-off-by: Aaron D. Lee <himself@adlee.work>
In-core ~30-line table-driven codec (no dep). The ITU-T G.711 µ-law
companding formula is a piece of telephony history worth teaching (AGENTS.md
learner-facing comment mandate). 3× linear upsample on decode; 3× decimation
downsample on encode. The resampler artifacts are below the barge-in trigger
threshold (LocalVadReflex only needs RMS energy); rubato lands in a post-
spearhead refinement if a downstream consumer needs better (spec §6.6).
Task T1 of slice-5 — T3 (TwilioMediaStreamsServer) consumes this codec.
Signed-off-by: Aaron D. Lee <himself@adlee.work>
Spec §7 done-criteria #10 demands a static assertion that TwilioCredentials
lives ONLY in rutster-trunk (ADR-0009 -- provider credentials never reach the
brain). The test compiles only because crate::provider::TwilioCredentials
resolves (the type's canonical home); if someone moved/re-exported it through
the workspace root or a sibling crate, this test's doc + the dep-graph change
would surface in review. The invariant is structural: sibling crates
(rutster-media, rutster-tap) do not depend on rutster-trunk, so the type
cannot reach them. The binary's config::twilio_credentials is the single
expected import path.
Signed-off-by: Aaron D. Lee <himself@adlee.work>
The routine CI gate stays feature-default-off: MockCallControlClient is the
per-PR test surface. A new twilio-live job runs ONLY on manual
workflow_dispatch (maintainer triggers pre-release) -- it exercises clippy
--features=twilio-live + the live TwilioCallControlClient tests against real
Twilio credentials (TWILIO_* secrets). Never runs per-PR.
Seam gate verified UNCHANGED: loop_driver.rs (744bf314...) + rtc_session.rs
(f47d63b9...) blob hashes match the slice-4 Task 10 pins exactly -- the trunk
leg's tick lives entirely in rutster-trunk/src/loop_driver.rs (a separate file
in a separate crate), so the media-crate seam files stay byte-identical.
cargo deny: reqwest (rustls-tls) + base64 + async-trait introduce ZERO new
duplicate dep versions (verified via `cargo tree -d` with vs without
--features=twilio-live: identical duplicate sets -- the existing skip list in
deny.toml remains sufficient). Local cargo-deny 0.18.3 cannot parse the
`-or-later` SPDX form + CVSS 4.0 advisory entries (pre-existing limitation
documented in deny.toml; CI's cargo-deny-action@v2 bundles 0.19.x which handles
both) -- CI is the authoritative deny gate.
Two rustdoc intra-doc-link warnings in my code fixed (mock.rs private-item
link -> plain inline code; lib.rs redundant explicit link target simplified).
Two pre-existing rustdoc warnings remain in rutster-tap/protocol.rs +
rutster/tap_engine.rs (out of scope -- pre-existing from slices 2-3, not
introduced by slice-5).
T10 of slice-5. This is the final task on the dev-b chain (T2 + T6 + T9 + T10
all landed).
Signed-off-by: Aaron D. Lee <himself@adlee.work>
QUICKSTART gains a Twilio Media Streams section: env-var table for the four
RUTSTER_TWILIO_* vars, the run-with-twilio-live command, the point-Twilio-
at-rutster webhook/TwiML walkthrough, + the outbound-call curl example. The
/v1/trunk routes' auth-deferral (slice 6) is flagged. A 'what's different
from WebRTC' note explains the architectural reuse -- the reflex stack is
ingress-agnostic (Reflex<TapAudioPipe> + LocalVadReflex REUSED from slice-4).
README's spearhead status is corrected + extended: slices 1-4 are merged to
main (the prior status stalled at '1-3 merged, slice-4 active' -- stale);
4.5 (sim/benchmark, ADR-0010) + step 5 (PSTN via rented transport, ADR-0007)
are the active build targets. ADR-0007 honored: rutster parses zero SIP bytes.
T9 of slice-5.
Signed-off-by: Aaron D. Lee <himself@adlee.work>
The live Twilio call-control client. Originates outbound calls via Twilio's
Calls.json API (TwiML <Connect><Stream> instructs Twilio to fork audio back
to our /twilio/media-stream WSS endpoint); hangs up via Status=completed.
HTTP basic auth over HTTPS; auth_token is NEVER logged (ADR-0009 -- provider
credentials never reach the brain) -- tracing fields expose only caller-
controlled values (to, from) + the CallSid's last 4 chars.
Feature-gated behind `twilio-live` so the routine CI gate stays feature-
default-off: MockCallControlClient is the per-PR test surface; the maintainer
runs cargo test --features=twilio-live when validating a release. reqwest +
tracing + serde_json are optional deps (dep:foo syntax) -- pulled in only
when the feature is on, keeping the default resolve lean.
The env parser (config::twilio_credentials) follows slice-5/seams' pure-
function pattern: takes Option<String> inputs (testable without env mutation),
returns Ok(None) when all four RUTSTER_TWILIO_* vars are unset (WebRTC-only
mode), Ok(Some) when all four present + parse, Err on partial config (fail-
fast at startup) or malformed values. TwilioCredentials is imported by the
binary but never re-exported through the workspace (ADR-0009).
T6 of slice-5. Depends on T2 (TwilioCredentials + CallControlClient trait).
Signed-off-by: Aaron D. Lee <himself@adlee.work>
The provider call-control seam (green zone, ADR-0008). The trait locks the
boundary so the next provider (Telnyx, etc.) is an implementation, not a
refactor. MockCallControlClient is the CI test double; the live
TwilioCallControlClient (T6) lives behind the twilio-live feature flag.
TwilioCredentials lives ONLY in crates/rutster-trunk/ -- never re-exported
through the workspace (ADR-0009 -- provider credentials never reach the
brain). Its Debug impl is hand-written (NOT derived) so the auth_token
renders as <redacted>, never leaking into tracing/panic output.
Option<SpendToken> on originate is the pre-paved seam for spearhead step-6
(spend cap); this slice passes None everywhere. The signature is locked so
step 6 is additive, not a refactor.
T2 of slice-5. lib.rs gains `pub mod provider;` -- stacked-branches carve-out
(rebase-merge, not squash) per AGENTS.md Git workflow; dev-c rebases forward
for the FOB-side pub mod declarations (g711/twilio_media_streams/session/
loop_driver).
Signed-off-by: Aaron D. Lee <himself@adlee.work>
Single-agent (no PM/relay) kickoff to close out slice-4: the
secondary-path advisory e2e and the CI seam gate. API facts verified
against main @ 60b65ab (mock schedule API, ReflexMetrics fields,
lib.rs exports); seam gate specced as pinned blob hashes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012QndwfhjyTiZcUYp87dwW8
Signed-off-by: Aaron D. Lee <himself@adlee.work>
These four reviews were filed on slice-1-review-fixes and
strategic-reviews-post-pivot-rescore but never reached main; both
branches are being retired in the post-slice-4 cleanup. Kept for
lineage: the 06-29 post-pivot re-score is where ADR-0007/0008 era
strategy was re-scored, and the 07-03 reviews build on these.
Sources: slice-1-review-fixes @ d2ef53b (gtm-path, vision-sanity-check,
slice-1-claude-adversarial-assessment), strategic-reviews-post-pivot-
rescore @ ea27167 (post-pivot re-score).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012QndwfhjyTiZcUYp87dwW8
Signed-off-by: Aaron D. Lee <himself@adlee.work>
Task 1 review flagged the new reflex module was missing from the
module-map //! catalogue. AGENTS.md learner-facing convention requires
the catalogue entry describing what the module does + why it exists.
Addresses the Minor finding from the Task 1 task review.
Signed-off-by: Aaron D. Lee <himself@adlee.work>
The critical-path foundation for the barge-in reflex. AdvisoryEvent is
the enum carried over a tokio mpsc from TapEngine to Reflex (brain →
FOB). ReflexMetrics is the observable surface. barge_in_flush is the
new AudioPipe trait method (default delegates to clear_playout_ring) —
the kill-now path that clears the ring AND drains rx_audio_out.
Task 1 of the slice-4 plan. Everything else depends on this landing.
Signed-off-by: Aaron D. Lee <himself@adlee.work>
Every commit MUST be signed off (git commit -s). Preserves the
copyright-holder's option to dual-license later — without a DCO/CLA
instrument, every external contribution erodes that option (ADR-0004).
Particularly load-bearing under multi-agent parallel authorship, where
multiple sessions commit in parallel and the DCO is the only per-commit
aggregation instrument.
Adds DCO.md with the full Linux-Foundation DCO text + the agents-sign-off-
with-the-human's-identity rule. AGENTS.md Git workflow gains the DCO bullet.
AGENTS.md +292 lines: the relay (model-agnostic MCP message-bus at
localhost:7110), PM session launch checklist (poller, watch.sh, kitty/tmux
spawner), session handoff (inbox.log / poller.log / relay-log.jsonl), PM
turn-start discipline (drain inbox, list_pending, git log, surface
proactively), multi-dev parallelism (5-rule checklist with the slice-3
anti-pattern), when-the-PM-is-blocked protocol. Plus the slice-4 PM
kickoff prompt under docs/superpowers/kickoffs/.
Load-bearing for the multi-agent workflow executing slice-4 (and beyond):
session discipline (turn-start polling, no standby mode, proactive
surfacing) was the prior failure mode; the prompt + these sections bake
the correction into AGENTS.md so a fresh PM agent session inherits it.