Sessions & briefings
How an agent gets your knowledge injected before it does anything - ambient sessions, the briefing's packing order, and what never gets dropped.
A Seamless session is one agent's stretch of work. A briefing is what
that agent gets handed at the start of it, before it has called a single tool:
a SessionStart hook - installed for Claude Code or
Codex - resolves the working directory to a project, assembles
constraints, plan rollups, and recent findings inside a token budget, and
injects the result into the agent's context. A client without hooks gets a
briefing only by calling session_start itself.
This is the mechanism that makes Seamless ambient rather than opt-in. An agent does not have to remember to ask what it knows; it begins already knowing your constraints.
Ambient vs. explicit sessions
| Ambient | Explicit | |
|---|---|---|
| Opened by | The SessionStart hook, automatically | session_start |
| Named | cc/<prefix>-<digest> (Claude Code) or cx/<prefix>-<digest> (Codex) |
Whatever you pass, or generated |
| Gets | The short injected briefing | The full briefing, returned by the call |
The ambient handle keeps the first eight external-ID characters readable and
adds 64 stable SHA-256 bits. Seamless resolves lifecycle activity by the full
external ID plus client, so two UUIDv7 sessions sharing a timestamp prefix never
share scope, findings, or provenance. Pre-upgrade handles keep their old names
when resumed. The handle is a display label, not an API: treat cc/... and
cx/... as opaque and never parse or construct them.
They are not two competing sessions. session_start adopts the sole ambient
session for the same working directory rather than opening a second one - that
adoption rule exists because the alternative was double-counting every agent.
Call session_start when the work is non-trivial: it returns the full briefing
(the injected one is deliberately shorter), and it binds the connection so
everything afterward inherits the project scope.
That distinction also appears in knowledge provenance. A write attributed by an
ambient hook stores the session's cc/... or cx/... name in
source_session; a write made through a bound MCP connection stores the
session's ULID. Readers resolve both forms. Do not infer client, scope, or
liveness from the spelling of source_session.
An actual briefing, annotated
This is a real briefing, in the order the assembler packs it:
Seam project: seamless -- 4 constraints, 58 memories, 3 recent findings.
CONSTRAINT: errcheck-check-blank-two-category-rule: errcheck runs with check-blank ...
CONSTRAINT: llm-degradation-remote-vs-local: llm errors split remote ...
STAGE: deep-audit-f15-f18-landed -- status unknown
PLAN: marketing -- 2/3 done, 1 claimable, 0 in flight
PLAN (awaiting approval): seamless-documentation-site -- (presented, 2m)
Memories (seamless):
- gofmt-must-scope-to-tracked-files: gofmt walks the filesystem ...
- shared-worktree-concurrent-agents-verify: Agents share the main worktree ...
- (+34 older -- use recall)
Recent findings:
Recall on demand with recall; read a memory with memory_read.
Seam session: cc/8dd2fd5b-55d96b8d15ff0104 (ambient)
</seam-briefing>
Line by line:
- The header counts what exists, so an agent knows how much it is not being shown.
CONSTRAINT:lines come first and are never dropped for budget. A constraint is a rule the project cannot violate; a briefing that omitted one to fit a token budget would be worse than no briefing at all.STAGE:lines are pinned right after constraints, for the same reason: a gated stage's status is load-bearing for the whole session. The pin belongs to the gate: a stage whose body does not open with a liveStatus: open|in_progress|blockedheader only renders (asstatus unknown) for thebriefing.stage_unknown_max_age_daysgrace window (default 7) after its last update, then leaves the briefing rather than squatting in it forever.PLAN:rollups follow, also pinned. The counts (2/3 done, 1 claimable) tell the next agent what work it can pick up right now.- The memory index is
name: descriptiononly - the description is the only text an index ever shows, which is why writing a good one matters more than writing a good body. (+34 older -- use recall)is the honest tail: the index was trimmed, and the briefing says so instead of pretending it is complete.- Recent findings are what previous sessions learned, harvested at their end.
The budget, and what survives it
Sections are packed against budgets.max_briefing_tokens (default 1500), then
the whole thing is hard-capped at briefing.hard_cap_multiplier times that
(default 2x).
The never-drop invariant: constraints, pinned stages, and active-plan rollups
are counted first and are exempt from budget dropping. Recent findings reserve
their (small, findings_count-capped) share of the budget next - they are the
"what just happened here" section, and without the reservation a fat memory
index would silently evict all of them while the header still claimed they were
present. Everything else - the memory index, sibling findings, sibling memories,
ready tasks - then packs in render order until the budget runs out; the header
counts only the findings that actually rendered.
The memory index's own order starts as newest-first - but each memory also
carries a utility score, a time-decayed
record of actual demand (reads, recall hits, prompt matches; being briefed
counts for nothing), and once a project's demand history matures the index is
ranked by the blend (1-w)·recency + w·utility, both on a 14-day half-life,
with briefing.utility_weight as w (default 0.4, 0 restores pure recency).
The switch is deliberate, not silent: in briefing.utility_mode: auto the
gardener latches it per project only once the project's first demand is at
least 14 days old and it has shown 20+ demand events and 10+ memories touched
in 30 days - a young project keeps the recency order, because a utility signal
with no history behind it is noise. on/off force it everywhere, and the
console Settings page shows each scope's progress toward the latch, with a
per-scope force.
Every knob is tunable in Configuration, and the
briefing: block is also editable live in the console. Those runtime edits are
stored in the database and win over both the file and the environment, applying
from the next session start without a daemon restart. If a briefing setting seems
to be ignored, check the console before you check the YAML.
Codex adds one client-specific safety ceiling after this packing step: every model-visible Seamless hook context is capped at 2,400 estimated tokens, below Codex's approximate 2,500-token temporary-file spill threshold. The cap runs before telemetry is recorded and preserves generated closing tags and the ambient handle. Claude Code does not use this additional cap.
Liveness
Sessions heartbeat. A session that ends cleanly cascades immediately; one that is
abandoned is caught by an idle reaper after gardener.session_idle_minutes and
marked expired. The TTL only applies when there was no end signal at all - a
crashed agent's session does not sit "live" forever, and a slow one is not
reaped out from under itself.
session_end is the explicit findings path. Claude Code's SessionEnd hook calls
the same lifecycle. Codex 0.144.6 has no SessionEnd event, so each Stop hook
updates provisional findings from last_assistant_message and the reaper later
marks the idle ambient session expired; those expired ambient findings still
surface in future briefings. Keep findings tight - briefings show a short preview
- but they are stored in full, not rejected, so a long finding is fine when it earns it.
The end of a session also harvests real model token usage - input, cached, cache-creation, and output counts read from the client's own transcript (Claude Code) or rollout file (Codex), not estimated from injected bytes. The session's console page shows them as its "Model tokens" panel; a session whose client exposes no transcript simply records none.