InteractionSession<Paths>
Class: InteractionSession<Paths>
Defined in: src/traverse/nav-session.ts:135
Extends
Type Parameters
Paths
Paths extends string = string
Constructors
Constructor
new InteractionSession<
Paths>(map,opts?,liveSources?):InteractionSession<Paths>
Defined in: src/traverse/nav-session.ts:174
Parameters
map
opts?
liveSources?
readonly LiveSource[]
Live graph sources to attach to THIS session (createSession passes the graph's).
Returns
InteractionSession<Paths>
Overrides
Accessors
focus
Get Signature
get focus():
string
Defined in: src/traverse/nav-session.ts:617
Returns
string
focusHistory
Get Signature
get focusHistory(): readonly
FocusMove[]
Defined in: src/traverse/nav-session.ts:830
The focus moves of this session, oldest first.
Read-only and complete: this is evidence, so it is not filtered by whether the cursor actually changed. See FocusMove.moved.
Returns
readonly FocusMove[]
graphId
Get Signature
get graphId():
string
Defined in: src/traverse/session.ts:984
The compiled graph's id (namespaces MCP tool names).
Returns
string
Inherited from
node
Get Signature
get node():
string
Defined in: src/traverse/session.ts:979
Returns
string
Inherited from
requiresHumanApproval
Get Signature
get requiresHumanApproval():
boolean
Defined in: src/traverse/session.ts:1000
Whether this session ENFORCES human approval on high-effect agent fires (SessionOptions.requireHumanApproval). Read by the serving layer so the instruction text it hands a model says what is actually true of this session — a tool description promising a gate that is off would be the same class of lie this option exists to remove.
Returns
boolean
Inherited from
stateVersion
Get Signature
get stateVersion():
number
Defined in: src/traverse/session.ts:1042
D18 version split — version stays the single total-order cursor; these
two say WHAT moved. A scrolling list must never staleness-fail a plan the
way a closing modal must; consumers watching for re-render/replan can
subscribe to the axis they care about.
Returns
number
Inherited from
structureVersion
Get Signature
get structureVersion():
number
Defined in: src/traverse/session.ts:1046
Returns
number
Inherited from
version
Get Signature
get version():
number
Defined in: src/traverse/session.ts:989
The one CAS/sinceVersion cursor: total order over ALL world motion.
Returns
number
Inherited from
Methods
acknowledgements()
acknowledgements():
StaleAcknowledgement[]
Defined in: src/traverse/session.ts:6584
EVERY PROTOCOL STEP THIS SESSION HAS RECORDED, oldest first — append-only, as copies.
Rows are never edited and never rewritten. One the world has moved past stops AUTHORIZING a fire and stays here as what it always was: a thing somebody did at a moment, at a state version this list still names.
BOUNDED, and the bound is answerable. The list holds the most recent
maxAcknowledgements (default 500); older receipts are dropped, counted
(Session.acknowledgementsDropped) and warned about once. A fire that
cites a dropped one is told why: 'evicted' — this library's own limit,
never the caller's mistake.
Returns
Inherited from
acknowledgementsDropped()
acknowledgementsDropped():
number
Defined in: src/traverse/session.ts:6598
HOW MANY RECEIPTS THIS SESSION HAS DROPPED to stay inside
maxAcknowledgements.
The bound made visible, exactly as Session.offersDropped is: nonzero
here is the reason an ACKNOWLEDGEMENT_REQUIRED refusal said 'evicted'
about a step the caller really did perform, and the reason
Session.acknowledgements is shorter than the history it looks like.
The fix is a bigger cap, not anything the caller did.
Returns
number
Inherited from
Session.acknowledgementsDropped
acknowledgeStale()
acknowledgeStale(
actionId,keys?,opts?):object
Defined in: src/traverse/session.ts:6530
I HAVE SEEN IT — the acknowledgement door, and the only one that takes a caller's word for anything.
WHAT IT RECORDS AND WHAT IT DOES NOT CLAIM. It records that this caller said, of this control, that it has dealt with these keys having moved. It does not claim the caller read the value, understood the consequence, or made a good decision — none of which this session can see. It stops the library repeating a fact to somebody who has answered it, which is the whole of the promise.
WHY THERE HAS TO BE A DOOR AT ALL. The alternative is to infer acknowledgement from a look, and a look is exactly what the caller was already doing on every turn the stamp was silently disarming itself. This library never serves a value, so it cannot know a value was read; the only honest acknowledgement is one somebody performs.
With no keys, everything outstanding for that control is answered. Named
keys clear only themselves, and a key nobody is carrying clears nothing —
cleared is what was actually being carried, never an echo of the request.
AND IT IS NOW A ROW SOMEBODY CAN CITE. Every call appends a
StaleAcknowledgement to session.acknowledgements() and hands back
its acknowledgementId — the pointer a 'require-ack' fire presents
(FireOptions.acknowledgementId). cleared means exactly what it
always meant; the id is new beside it.
A ROW IS WRITTEN EVEN WHEN NOTHING WAS CARRIED, and that is deliberate. The carried ledger is about stamps this session SERVED; a freshness policy can refuse over a key that was never served on a row (a fire made without looking, an axis the row does not disclose). Writing the row only when bookkeeping happened to be outstanding would make the protocol step unperformable exactly where it is required.
const { acknowledgementId } = session.acknowledgeStale('ledger.settle', ['claim.total'], { offerId });
session.fire('ledger.settle', { source: 'agent', offerId, acknowledgementId });APPEND-ONLY, AND BOUNDED — SessionOptions.maxAcknowledgements,
default 500, oldest dropped first. The ACKNOWLEDGEMENT_REQUIRED →
acknowledge → refire loop writes one row per turn, so an unbounded trail is
a session-lifetime leak on exactly the protocol this feature asks callers to
run. Bounding a RECEIPT ledger owes three things, and all three are paid:
evictions are counted (Session.acknowledgementsDropped), the
integrator is warned once, and a fire citing a dropped receipt is refused
ACKNOWLEDGEMENT_REQUIRED with why: 'evicted' — a step this caller really
did perform, dropped by this library's own limit, and never reported as a
pointer they made up. Nothing is ever edited or retracted: the cap drops the
oldest rows whole, and a retained row says exactly what it always said.
SAY IT AGAIN, BECAUSE THE NAME INVITES THE BIGGER CLAIM: this records that a protocol step was PERFORMED. It is not evidence that anything was understood, and no field on the row says otherwise.
Parameters
actionId
string
keys?
readonly string[]
opts?
by?
Who performed it. Defaults to 'agent', never 'user': filing a
machine's act under a person is the one mistake a ledger must not make
on its own — the same law Session.fire keeps.
offerId?
string
The offer being answered — a join, and the row a 'require-ack' fire will cite.
Returns
object
acknowledgementId
acknowledgementId:
string
cleared
cleared:
string[]
Inherited from
alwaysApprove()
alwaysApprove(
affordanceId,opts):ApprovalResult
Defined in: src/traverse/session.ts:5768
RECORD A DURABLE ALWAYS ALLOW — a scoped standing policy, not an approval of
one action. Every fire it authorizes lands its own 'used' row, so the
journal shows how many times the standing yes was exercised. That visible
count is the auditable price of a durable grant, and it is not optional.
SCOPED TO THE ACTION (plus an optional instance) and deliberately NOT to the input — a grant that required identical inputs would be indistinguishable from a single ALLOW and therefore useless. Tell the human the truth in those words: "always allow Add to cart — any item, for the next hour." A reader who assumes ALWAYS ALLOW inherits ALLOW's input binding has been misled by us.
A durable grant with no off switch is a permanent hole, so Session.revokeAlwaysApprove ships with it rather than after it.
Parameters
affordanceId
string
opts
by
string
expiresInMs?
number
instance?
string
note?
string
Returns
Inherited from
approveAsk()
approveAsk(
askId,opts):ApprovalResult
Defined in: src/traverse/session.ts:5616
RECORD THE HUMAN'S ALLOW — wire your Approve button to this.
Writes an 'approved' row with principal: 'user' BEFORE any fire, and the
fire that spends it must present its askId. That ordering is the whole
change: the row named 'approved' used to be minted by the very fire it
claimed to authorize, stamped with that fire's own principal.
SINGLE-USE, on purpose: one yes authorizes one action. A second fire under the
same askId refuses APPROVAL_SPENT, and the spend lands its own 'used' row —
so an auditor can count approvals against executions.
NO principal PARAMETER, and that is the unforgeable shape rather than an
omission: this door stamps 'user' unconditionally, so there is no argument
to lie with. Its twin Session.declineAsk has the same shape for the
same reason — a fabricated NO is a forgery too. It authorizes nothing, but it
puts a human decision in the auditable journal and takes the question off the
person's screen, and neither of those may be reachable by asking politely.
(Session.declineConfirm does take a principal, and under
enforcement it therefore closes nothing at all.)
by is REQUIRED: an approval whose decider is unknown is exactly the
claim-as-fact this closes. It is a string YOUR host supplies — the library
proves a human-principal row exists, never that a particular person
authenticated.
Parameters
askId
string
opts
by
string
note?
string
Returns
Inherited from
asAgent()
asAgent():
PrincipalPort
Defined in: src/traverse/session.ts:2446
THIS SESSION'S DOORS, WITH THE PRINCIPAL ALREADY ON THEM.
const agent = session.asAgent();
agent.fire('checkout.place-order'); // no `source` to forget, or to get wrongThe problem it removes is small and constant: provenance was an argument on
every call, and an argument on every call is an argument that gets omitted
(a JS caller's fire(id) files a machine's action under this library's
default) or copied (one relay's source: 'user' pasted into the line that
fires for the model). Said once, at the boundary where the caller's identity
is actually known, it cannot drift call by call.
IT IS THE SAME ASSERTION, NOT A STRONGER ONE. A port fire stamps
attribution.basis: 'caller-asserted', exactly as fire({ source }) does —
the same caller making the same claim with less repetition. Recording
ergonomics as evidence would be laundering convenience into proof, which is
the one thing this release exists to stop.
WHAT IS DELIBERATELY NOT ON IT: every human-side authority door
(approveAsk, alwaysApprove, revokeAsk, declineConfirm) and
updateState (see PrincipalPort). fire({ source }) stays fully
supported and is not deprecated; this is a second door onto the same law,
not a replacement for it.
Returns
Inherited from
asHuman()
asHuman():
PrincipalPort
Defined in: src/traverse/session.ts:2451
The same door, speaking as a person. Files its acts under 'user'.
Returns
Inherited from
asks()
asks():
AskStatus[]
Defined in: src/traverse/session.ts:5959
THE ASK BOOK — every high-effect ask this session is holding, and what became of each (copies, oldest first).
The read that answers "is anything waiting on a person?". A paused ask is not a transition and never joins Session.pending or Session.awaitingSettlement, so before this door a caller asking about a paused action was answered by the two lists that structurally could not contain it — and an empty list reads as nothing is happening, which is the confident emptiness this library keeps closing.
NOT named openAsks: under SessionOptions.requireHumanApproval
answered cards STAY in the book (an approval must be spendable once, a
decline refusable for the session's life), so a name promising only open
ones would be wrong on its own rows. Read answer for the fate — absent
means the person has not decided.
A LIVE READ, and callers must keep it that way: ask it at answer time, never once at construction. The whole value of the arm it feeds is that the fate it reports is the fate right now.
Structural facts only (AskStatus) — the receipts stay on the ask. Session.confirms remains the auditable journal; this is the derivation the library owes its own serving layer, because deriving these fates from journal rows means re-implementing the gate's law beside the gate.
Returns
Inherited from
asSystem()
asSystem():
PrincipalPort
Defined in: src/traverse/session.ts:2456
The same door, speaking as the app itself.
Returns
Inherited from
available()
available():
AvailableSlice
Defined in: src/traverse/nav-session.ts:778
Returns
Overrides
availableJourneys()
availableJourneys():
object
Defined in: src/traverse/session.ts:1960
Journey-level disclosure for the planning LLM (descriptions + feasibility, no tool detail).
Returns
object
journeys
journeys:
AvailableJourney[]
node
node:
string
version
version:
number
Inherited from
awaitingSettlement()
awaitingSettlement():
string[]
Defined in: src/traverse/session.ts:3991
The fires whose settlement question is still OPEN — every id Session.settlementOf has an answer coming for, in fire order.
NOT the same list as Session.pending, and the difference is the
reason this door exists. pending() names fires awaiting the app's STATE
report, which a fire declaring no writes NEVER joins — it still has a
handler running and a settlement coming. So every pending fire is awaiting
a settlement, and not every fire awaiting a settlement is pending. Asked
"what is still live?", pending() alone answers "nothing" about an action
that is at that moment running.
Ids, not rows: a runtimeStageId already carries the affordance that made it
(catalog.go-checkout#0), so nothing has to be looked up — or guessed —
to say which action is still out there.
Returns
string[]
Inherited from
beginWork()
beginWork(
label?,opts?):WorkHandle
Defined in: src/traverse/session.ts:4639
SAY THE APP IS WORKING ON SOMETHING, and get back the handle that closes it.
The imperative sibling of AvailableEdge.busy. busy is a fact about
a CONTROL — the spinner in the button, standing until the app changes it.
This is a fact about a PIECE OF WORK: it opens where the work starts, closes
where the work ends, and while it is open the readers can say so about the
FIRE it belongs to. A fire can come to rest while the app is still working
(the delta is reported, the upload continues), and before this ledger every
list answered "nothing is live" about exactly that.
const work = session.beginWork('Uploading the photo');
try {
await upload(file);
} finally {
work.done();
}WHERE IT LANDS IS DECIDED AT CALL TIME, and never revisited — three homes:
{ transitionId }— the exact fire, and EXPLICIT WINS, the same order Session.updateState keeps. An id this session does not know as a fire binds to nothing (see below).- Inside a handler's synchronous portion — the fire whose handler is
running, read from the same call window
updateState()reads. No id to pass, no correlation to get wrong. - Neither — an UNBOUND row at principal
'system', plus one dev warning. Work never runs silently: the row is still opened and still served, it simply does not claim a fire nothing named. The warning exists so an app cannot believe unbound work is bound.
TWO CAVEATS ABOUT HOME 2, and both are the window's shape rather than a bug:
- Call it before the first
await. The window is open for the handler's SYNCHRONOUS portion only. Past an await the handler is no longer "the call we are inside of" — another fire may be mid-flight — so a later call is unbound rather than bound to a record that is merely the most recent. (A handler that must open work late passes{ transitionId }.) - App code around
fire()is outside the window. Callingfire()and thenbeginWork()on the next line is home 3: the handler is deferred, so nothing is running yet. Bind it with thetransitionIdthe fire result just handed you.
NOTHING ABOUT THIS IS A GATE OR A CLOCK. Opening work refuses no fire, changes no served row, and does not bump the session version (a plan made before it is not stale — nothing an agent can act on changed). No timer expires a row, and a row that outlives everyone's patience keeps saying the one true thing: the app said it was working and has not said otherwise.
Parameters
label?
string
opts?
Returns
Inherited from
carriedStale()
carriedStale(
actionId):string[]
Defined in: src/traverse/session.ts:6471
WHAT THIS SESSION IS STILL CARRYING FOR ONE CONTROL — the key names it has served as stale and nobody has acknowledged, oldest first.
A pure question: asking never adds, never clears, and never advances any clock. The ledger is inspectable on purpose — a caller that is being told something on every turn is owed a way to ask why, without that asking being mistaken for an answer.
Parameters
actionId
string
Returns
string[]
Inherited from
carryStale()
carryStale(
actionId,keys):void
Defined in: src/traverse/session.ts:6455
CARRY THESE UNTIL SOMEBODY ACKNOWLEDGES THEM — the record that a staleness was SERVED on this control's row.
Called by the layer that actually hands the row over, with exactly the keys that went onto it. Saying it is what creates the obligation to keep saying it: a stamp computed and not served is carried by nobody, because nobody can be asked to acknowledge a fact they were never told.
Idempotent, and order is FIRST-SERVED: telling a caller the same thing twice does not make it two facts, and a key that has been outstanding for four turns stays where it was rather than jumping to the end of the list whenever it is re-served.
An app may call it too — a surface that showed the fact in its own words has told the caller as much as this library's row would have.
Parameters
actionId
string
keys
readonly string[]
Returns
void
Inherited from
commitJourney()
commitJourney(
journeyId,opts?):CommitJourneyResult
Defined in: src/traverse/session.ts:1992
Commit to a journey: opens a frame so toMCPTools()/contextBrief() serve ONLY that journey's currently-fireable steps plus escape tools — the token win (journeys for planning, tools on commit). One frame at a time in v0.
Never-trap invariant: an agent commit whose entry step cannot materialise right now is refused ENTRY_NOT_MATERIALIZED instead of opening a frame that could never act (see the gate below).
Parameters
journeyId
string
opts?
expectedVersion?
number
source?
Returns
Inherited from
commitLog()
commitLog():
CommitBundle[]
Defined in: src/traverse/session.ts:4940
The footprintjs commit log: one bundle per SETTLED/stimulus/sync transition.
Returns
Inherited from
confirmAsk()
confirmAsk(
affordanceId,opts?):object
Defined in: src/traverse/session.ts:5369
Record a high-effect confirm ask and assemble its RECEIPTS from what the session already knows — the guard evidence that made the edge fireable, the declared (honesty-tagged) effect, the current position, and a compact tail of the fire journal. No new capture: this is a pure read over live state.
A serving layer calls this the moment it decides to gate a high-effect edge
on human consent, then relays the returned receipts to the person. The
returned askId is the chain key: the confirmed fire closes it as
'approved' automatically (linked by transitionId), or declineConfirm
closes it as 'declined'. Asking twice for the same edge while an ask is
still open SUPERSEDES it (the human is still deciding) — one open ask per
affordance. Never throws: an unknown affordance yields a minimal receipt
(a serving layer relies on this mid-turn).
Parameters
affordanceId
string
opts?
input?
unknown
What the confirmed fire will SEND — recorded on the receipts as
willUse.input, so the human approves an object and not just a verb.
Optional: an ask told nothing shows nothing, and under enforcement a fire
carrying an input the card never showed is refused.
DETACHED THE MOMENT IT ARRIVES (bound-input.ts): keep your reference and change it after the yes, and the fire is refused APPROVAL_MISMATCH rather than compared against itself.
instance?
string
Which row/instance the card is about (an order id).
source?
Returns
object
askId
askId:
string
receipts
receipts:
ConfirmReceipts
Inherited from
confirms()
confirms():
ConfirmRecord[]
Defined in: src/traverse/session.ts:5924
The confirm journal (DEEP copies): every high-effect ask and how it was
answered — an auditable ask → decision → fire chain (join transitionId
back to the commit log, askId across the three rows). Export it to your
audit sink like gaps(); it grows for the session's life.
Returns
Inherited from
contextBrief()
contextBrief(
opts?):ContextBrief
Defined in: src/traverse/nav-session.ts:1013
Token-lean, prompt-ready session context for the next chat turn: current
position, the open frame, and who did what since sinceVersion (the
agent's last look). Built from AUTHORED strings and structural facts only
— state values and payloads never enter the text.
Parameters
opts?
Returns
Overrides
decisions()
decisions():
DecisionStatus[]
Defined in: src/traverse/session.ts:4524
EVERY DECISION IN THIS GRAPH THAT BELONGS TO A PERSON, and whether it has been made — read at the moment you ask.
The sibling of Session.asks: that one answers "is anything waiting on a person?", this one answers "is anything a person's to DECIDE?". Two different questions with two different next moves — wait for a card to be answered, or present options and stop — so they are two lists and they share no vocabulary. Nothing here mints an ask, an askId, a card or a receipt, and nothing here ever will.
GRAPH-WIDE, like the ask book. A decision on another page still holds a journey, so every declaring control has a row wherever it lives — the row is about a declaration, not about where the cursor happens to be.
A LIVE READ. made is evaluated fresh against projected state on every
call, and madeBy is served only beside made: true and only from the
decisions book. Nothing is cached, nothing is timed, and nothing here fires:
made: true is a state reading, not a command.
No per-instance rows: the declaration is action-level and doneWhen reads
flat projected-state keys. An app modelling per-row decisions models them in
its own keys — a stated limit, not a roadmap promise.
Returns
Inherited from
declareHolds()
declareHolds(
affordanceId,read): () =>void
Defined in: src/traverse/session.ts:1677
Hand over a reader for what one control HOLDS — the per-element
DECLARATION door, and the one the sensor forwards ControlDeclaration.value
into. Returns the release, token-identity like every other declaration pair
in this library: it drops the reader it was handed and nothing else, so
attach → attach → detach nets to the surviving declaration rather than to
silence.
PRECEDENCE, stated once: this door OUTRANKS the registration-time holds:
reader for the same action. A declaration is the more specific statement —
it is about the element on screen, not about the tool — which is the same
rank order the sensor's own two evidence levels keep.
An id no affordance answers to is filed and simply never served. Not a refusal, and deliberately not a warning: a control can be handed over before the tool that declares it is mounted, and shouting at a mount race would teach nothing true.
A READING, NOT A BINDING — see AvailableEdge.holds. Nothing here changes what a fire sends; the fire reads its own payload at act time. And the reader must BE a read: it runs once per served row, on a path every refused fire also walks.
Parameters
affordanceId
string
read
() => unknown
Returns
() => void
Inherited from
declineAsk()
declineAsk(
askId,opts):ApprovalResult
Defined in: src/traverse/session.ts:5645
RECORD THE HUMAN'S NO for one ask — the unambiguous twin of Session.approveAsk, keyed by askId because several asks for the same action can be open at once under enforcement.
Terminal and permanent: a fire naming this askId refuses APPROVAL_DECLINED for
the session's life, and approveAsk on it returns ASK_ALREADY_ANSWERED.
Nothing here ever deletes a row — a re-ask after a no mints a NEW askId, so
an agent grinding a person toward yes leaves a countable trail.
Parameters
askId
string
opts
by
string
note?
string
Returns
Inherited from
declineConfirm()
declineConfirm(
affordanceId,opts?):ConfirmRecord
Defined in: src/traverse/session.ts:5514
Close a high-effect ask as DECLINED — the human said no. Records the
decision so the chain closes honestly instead of the ask dangling forever
(the v1 reality: a declined action was simply never re-called, an invisible
event). Closes the open ask for affordanceId when one exists; with none
open (a pre-emptive decline) it mints a standalone decline row — a refusal
is worth recording either way. Returns the row (a deep copy).
UNDER requireHumanApproval IT CLOSES NOTHING, whatever principal says.
The row is marked relayed, the card stays open, and the person still gets
asked. principal is an argument, and an argument is a claim: a caller that
could close a card by saying 'user' could bury the question before the
human ever saw it. A human's no goes through
Session.declineAsk(askId, { by }) — keyed to the card they answered,
with no principal argument to lie with.
Parameters
affordanceId
string
opts?
by?
string
note?
string
principal?
Returns
Inherited from
detachSources()
detachSources():
void
Defined in: src/traverse/nav-session.ts:205
Release every live-source binding this session's graph attached (the
counterpart of sources: [fromLiveStore(...)]). Idempotent: the ledger is
drained on the first call. A source whose detach throws is isolated with a
warning — consumer store code must never break the session (recorder rule).
Returns
void
explain()
explain(
affordanceId):Explanation
Defined in: src/traverse/session.ts:1939
Why an affordance is (or is not) available right now — per-condition evidence.
Parameters
affordanceId
string
Returns
Inherited from
fire()
fire(
affordanceId,opts?):FireResult
Defined in: src/traverse/nav-session.ts:834
Returned transition records are LIVE views — settlement updates them in
place. effectStatus is the opposite: a reading taken at return time, and
because the handler is always deferred it can never say 'performed' here.
whenSettled carries the later truth, once, as a snapshot.
opts is optional at RUNTIME and required in TypeScript: a JS caller's
fire('page.tool') is answered instead of crashing on opts.source,
while a typed caller is still made to name the principal. An omitted
source reads as 'agent' — never 'user', which would file a machine's
action in the ledger under a human and disarm the never-trap gate below.
THE CONFIRM BOUNDARY, stated here because this is the signature an
integrator reads. There is no confirm field on FireOptions and
there never will be: a boolean the caller controls is not evidence, so the
door has no slot for one. confirm is a MODE B TOOL ARGUMENT
(serve/modes.ts), which means a fire arriving here directly is not gated by
confirm at any layer — the app's own code owns its session, and 'user' /
'system' / invoke: false are the app reporting motion that really
happened.
What SessionOptions.requireHumanApproval adds is keyed on the PRINCIPAL rather than the door, so an AGENT-sourced high-effect fire is held wherever it comes from — the Mode B port, the MCP server, the testing harness, or this method called directly — and the proof it must present is FireOptions.askId, a pointer to a row a human-side door recorded. See THE APPROVAL GATE below.
Parameters
affordanceId
string
opts?
FireOptions = UNATTRIBUTED_FIRE
Returns
Overrides
frames()
frames():
JourneyFrame[]
Defined in: src/traverse/session.ts:2082
Frame history: every closed frame (completed / cancelled / demoted), oldest first.
Returns
Inherited from
gaps()
gaps():
GapRecord[]
Defined in: src/traverse/session.ts:5006
The unmet-demand ledger (DEEP copies) — export it to your analytics/triage pipeline.
Returns
Inherited from
groundTruth()
groundTruth(
opts?):GroundTruth
Defined in: src/traverse/session.ts:6710
What this session ACTUALLY did, in words a model is told outrank the conversation: where it is, every attempt and how each came to rest, what a human is still deciding, and what the app has not answered yet.
The sibling of Session.contextBrief, and separate from it on purpose. The brief serves position + options + narrative, and the field exposed a structural hole in that: a REFUSED fire is a gap-ledger row, not a transition, so failed attempts never appeared in it. With the failures invisible and nothing else grounding it, a model narrated an entire flow — "name set, recipe selected" — having called zero tools. Its own prose had become its context. This block merges BOTH ledgers so a refusal is as visible as a success, and states the anti-narration sentence outright when nothing has happened at all.
Facts only. Options are whats_here's job, values and payloads belong to the data channel, and nothing here interprets: one line per occurrence.
Parameters
opts?
Returns
Inherited from
howToReach()
howToReach(
pageId):RouteStep[] |null
Defined in: src/traverse/session.ts:2106
HOW DO I GET THERE — the fewest declared hops from where the cursor is to
pageId, each naming the action whose claim makes the hop.
[] when you are already there; null when nobody declares a route — the
honest absence, not "it cannot be reached", because an app may navigate in
ways it never declared.
DERIVED from effect.navigatesTo, which exists for other reasons. Pages
declare no edges to each other; an action's claim IS the edge, so there is
nothing to author and nothing that can drift.
A ROUTE IS NOT A PLAN, and not a permission. It reports declared hops in fewest-hops order — arithmetic, not preference; a preferred order toward a goal is a journey, which is declared. And it does not promise the hops are open: a guard may be closed or a control greyed. Availability is answered on the row of the action you are about to reach for, and is deliberately NOT guessed here for pages you have not arrived at, because the state at a page this session has never seen is a thing it cannot honestly speak to.
Parameters
pageId
string
Returns
RouteStep[] | null
Inherited from
journeyFrame()
journeyFrame():
JourneyFrame|null
Defined in: src/traverse/session.ts:2077
The open journey frame (snapshot), or null.
Returns
JourneyFrame | null
Inherited from
journeyPlan()
journeyPlan(
journeyId):JourneyPlan
Defined in: src/traverse/session.ts:2187
The DERIVED intra-journey dependency DAG with live status. Dependencies are computed, never authored: step B depends on step A when A's declared effect.writes overlap B's guard keys — the guard×effect atoms already encode the ordering, so it cannot drift from the graph.
Parameters
journeyId
string
Returns
Inherited from
journeyStanding()
journeyStanding(
journeyId):JourneyStanding
Defined in: src/traverse/session.ts:2297
WHERE ONE JOURNEY STANDS — one settled word for the whole chain, and the facts behind it.
The question a reader actually has between turns is not "what may I fire
next" (that is the plan's, and whats_here's) but "whose turn is it, and is
this thing moving". Answering it from the plan alone means re-implementing
library law outside the library — which rows close which card, what a
relayed decline does NOT close, when a refusal is a failure and when it is
nothing of the kind — and two surfaces that re-derive it can disagree about
one chain, which reads to a model as "the human already answered".
A PURE FOLD over the plan, the ask book, the decisions book, retained settlements and frame history. No state of its own, no cache, no timer, and it never fires: computed fresh on every call, so the word is true about NOW rather than about the last time somebody asked. Two calls in a row change nothing and agree with each other.
The walk, stated as the walk it is:
- An OPEN frame for this journey governs. Otherwise a latest-closed
'completed'frame answers'done'; a cancelled or demoted one contributes history and never a verdict — abandonment is not completion and not failure — so the live plan is walked instead. - Walk the steps in chain order. The FIRST step that is not done is the
GOVERNING step, and its hold names the standing: an open card
(
'awaiting-human'), the human's own no ('declined'), a decision that belongs to a person ('with-the-human'), a last attempt that came to rest badly ('failed'), an evaluated failing guard ('blocked'), else'in-progress'. - Every step done →
'done'. A journey nobody has started walks arm 2 like any other, and'in-progress'withstepsDone: 0is the honest reading of "open, and nothing holds it" — the counts say plainly that nothing has fired.
'failed' IS NEVER MINTED FROM A PAUSE. Not from needs-confirm, not from a
relayed decline, not from any approval refusal, not from a guard, disabled
or materialization refusal. A refusal is not an execution: nothing ran, so
nothing failed. 'failed' requires a fire that actually came to rest badly,
and the evidence carries a POINTER to it — the receipt itself stays
did_it_work's to serve, once.
Throws on an id this graph does not have, exactly as Session.journeyPlan does and through that method's own refusal — serving layers resolve names first.
Parameters
journeyId
string
Returns
Inherited from
keysChangedSince()
keysChangedSince(
sinceVersion?,opts?):string[]
Defined in: src/traverse/session.ts:6420
THE STATE KEYS THIS SESSION HAS COMMITTED SINCE sinceVersion — names, in
first-changed order, each named once.
The same fact contextBrief narrates per transition ("user push changed:
claim.total"), answered as data so a caller can JOIN it to something. The
serving layer does exactly one thing with it: intersect it with an edge's
declared Effect.reads and stamp the overlap as staleReads. Before
that join existed a reader was handed a changed key and a list of controls
and left to connect them by eye.
sinceVersion omitted means the whole session. The version filter is the
transition's own cursorVersion, the same comparison the brief makes, so
the two can never disagree about what "since" means.
NAMES ONLY, and no claim beyond presence: a key here was written, which is not a statement that its value is different from the one you saw, that any control is now wrong, or that anything should be re-read.
opts.for — ANSWER IT FOR ONE CALLER, AND YOUR OWN WRITE IS NOT NEWS.
Omit it and this is the session's own answer: every key this session committed in that window, whoever moved it, byte for byte what it has always been. It is the honest answer to "what has this session committed", and a caller joining the log to something else still wants exactly that.
Name a principal and the question changes to the one a SERVED ROW is asking: has this key moved under you? A key is stale to a caller when it moved since that caller last acted on it. A caller's own committed write is the caller acting, so it is not a disturbance to itself — and anybody else's write to that same key, afterwards, is.
WHY THIS IS NOT A COSMETIC FILTER. Before 1.7.0 the stamp built on this
lived for one turn, so a self-write was one line of noise and then gone.
Carried until answered, it became a loop with no exit: the agent's fire
clears the control's ledger, the fire's own commit re-arms it on the next
look, and the only act that clears it re-arms it again. Measured, off a real
campaign: a control served with staleWrites naming keys its own last fire
had written, every turn, and fired four times against a change it had made
itself.
WHAT IT IS BOUNDED BY, so it can never quietly grow into "trust the agent": an ACT, at a VERSION. Only a committed motion this session filed under that principal un-marks a key, and only until the next motion filed under anybody else. There is no identity here that outlives one commit.
AND WHAT IT STILL DOES NOT SAY: no principal is served, on this list or on any row built from it. WHO moved a key stays where it always was — on the transition record, beside the Attribution that grades how the library came to believe it.
Parameters
sinceVersion?
number
opts?
for?
Returns
string[]
Inherited from
leaveJourney()
leaveJourney(
opts?):JourneyFrame|null
Defined in: src/traverse/session.ts:2058
Close the open frame. Default reason: 'completed' if every step was committed while the frame was open, else 'cancelled'. Returns the closed frame, or null when none was open.
Parameters
opts?
reason?
"completed" | "cancelled"
Returns
JourneyFrame | null
Inherited from
observationsOf()
observationsOf(
transitionId):ExternalObservation[]
Defined in: src/traverse/session.ts:4114
EVERY EXTERNAL REPORT ABOUT ONE FIRE, oldest first — copies, so a reader cannot rewrite the trail. Empty for a fire nobody reported on, and for an id this session does not know: this is a question, and asking it refuses nothing.
Parameters
transitionId
string
Returns
Inherited from
observeEffect()
observeEffect(
transitionId,report):ObserveEffectResult
Defined in: src/traverse/session.ts:4034
SOMETHING THIS CLIENT CANNOT SEE HAS REPORTED — the external-observation
door, and the other half of observability: 'external'.
A payment clears at a processor, a job finishes on a queue, a letter is posted. The browser sees none of it, so before this door the honest answer for such a fire was 'unobservable' forever — the library declining to guess, and an app holding the answer with nowhere to put it. Now it can hand it in.
WHAT IS RECORDED IS THE REPORT, NEVER THE FACT. The row says a source the app named said this happened, with a REFERENCE to evidence this library never fetches, dereferences or interprets. Nothing here is proof the effect occurred, and no sentence anywhere in this library will say it is.
const fired = session.fire('checkout.pay', { source: 'agent' });
// …the webhook arrives, minutes later…
session.observeEffect(fired.transition.id, {
source: 'stripe-webhook',
status: 'performed',
evidenceRef: 'evt_1P2x…',
});FIRST REPORT SETTLES, EVERY REPORT IS KEPT. The first one answers the fire's
open question (whenSettled, settlementOf) exactly as a state report or a
handler completing would; a later one — a reversal, a second source — is
APPENDED to TransitionRecord.observations and settled: false says
the receipt it did not rewrite. That is the append-only law this library
keeps everywhere: a record taken at rest is never edited, and new facts are
new rows beside it.
IT DOES NOT MOVE STATE. The delta is updateState's job and this door
writes none: an effect nobody here can see is exactly the effect whose state
consequences this library has no business inventing. So a settled fire's
effectVerified stays honestly 'unobservable' — no report exists to check
the declared writes against, and that has not changed because somebody said
the work was done.
Parameters
transitionId
string
report
Returns
Inherited from
offerFor()
offerFor(
offerId):OfferRecord|undefined
Defined in: src/traverse/session.ts:6609
WHAT WAS TRUE WHEN THAT ROW WENT OUT — one OfferRecord by id, or nothing where this session is not holding it (see Session.offersDropped).
A copy: a caller sorting a key list in place must not reach the ledger.
Parameters
offerId
string
Returns
OfferRecord | undefined
Inherited from
offers()
offers():
OfferRecord[]
Defined in: src/traverse/session.ts:6614
Every offer this session still holds, oldest first, as copies.
Returns
Inherited from
offersDropped()
offersDropped():
number
Defined in: src/traverse/session.ts:6627
HOW MANY OFFERS THIS SESSION HAS DROPPED to stay inside maxOffers.
The bound made visible. A citation can expire, so the number that says how
often that has happened is a fact a caller is owed rather than an internal
detail: nonzero here is the reason a OFFER_NOT_ON_RECORD refusal said
'evicted', and the fix is a bigger maxOffers rather than anything the
caller did wrong.
Returns
number
Inherited from
on()
on<
N>(event,listener): () =>void
Defined in: src/traverse/session.ts:1080
Subscribe to a session event. Returns an unsubscribe function.
Type Parameters
N
N extends keyof SessionEvents
Parameters
event
N
listener
(payload) => void
Returns
() => void
Inherited from
onConfirm()
onConfirm(
listener): () =>void
Defined in: src/traverse/session.ts:5929
Live export hook: fires once per new confirm row. Sugar for on('confirm', …).
Parameters
listener
(record) => void
Returns
() => void
Inherited from
onGap()
onGap(
listener): () =>void
Defined in: src/traverse/session.ts:5011
Live export hook: fires once per new gap row. Sugar for on('gap', …).
Parameters
listener
(gap) => void
Returns
() => void
Inherited from
openAskFor()
openAskFor(
affordanceId,opts?):string|undefined
Defined in: src/traverse/session.ts:5475
The id of the ask this session is holding for exactly this action and input — the pointer a caller passes as FireOptions.askId, or hands to Session.approveAsk.
It exists so the serving layer never has to re-derive the library's own identity rules (which values compare, which decline): normalization happens once, here, and the port and the gate can therefore never disagree about which card a fire belongs to. Under enforcement it matches on the input and instance; in the default mode it answers with the open ask for the action. A pure read — nothing is minted, nothing is recorded.
PREFERENCE ORDER, and it exists so a refusal can still teach: a usable approval first, then a card the human has not answered, then an ALREADY ANSWERED one. Presenting a spent or declined pointer looks odd until you see what it buys — the gate answers APPROVAL_SPENT or APPROVAL_DECLINED instead of the blank "nobody approved this", so the caller learns that the yes was used, or that the person said no, rather than being sent to ask again.
Parameters
affordanceId
string
opts?
input?
unknown
instance?
string
Returns
string | undefined
Inherited from
openWork()
openWork():
WorkRow[]
Defined in: src/traverse/session.ts:4671
Work the app has open RIGHT NOW, oldest first — the third "what is still live?" door, beside Session.pending (fires awaiting the app's state report) and Session.awaitingSettlement (fires that can still be asked about), and the cousin of Session.asks (cards awaiting a person).
OPEN ONLY, which is what the name promises: a closed row leaves this list
the moment done() runs and never comes back. Copies, so a caller holding
one cannot edit the ledger.
Every row here is the APP'S CLAIM about itself. Nothing in this library checks that work is running, measures it, or ends it.
Returns
WorkRow[]
Inherited from
pending()
pending():
PendingInfo[]
Defined in: src/traverse/session.ts:4572
Fired transitions still awaiting their state report (oldest first).
Returns
Inherited from
producedFor()
producedFor(
transitionId):unknown
Defined in: src/traverse/session.ts:4971
Data the given transition's handler RETURNED (search results, a looked-up record) — a fresh snapshot, safe to serialize into a tool result. Available once the handler has resolved, so read it AFTER awaiting the settlement. Returns undefined when the handler returned nothing (or capture is off).
Parameters
transitionId
string
Returns
unknown
Inherited from
readsByStep()
readsByStep():
ReadonlyMap<string,string[]>
Defined in: src/traverse/session.ts:4961
runtimeStageId → tracked read keys (feed to causalChain's keysRead lookup).
Returns
ReadonlyMap<string, string[]>
Inherited from
registerAction()
registerAction(
path,actionId,def):ActionHandle
Defined in: src/traverse/nav-session.ts:352
Register ONE action on a node (convenience over registerActions). def
either binds an existing declared action ({ handler }) or declares a new
leaf here ({ does, handler }). Returns a single-action handle.
Parameters
path
Paths
actionId
string
def
RegisteredActionDef & object
Returns
registerActions()
registerActions(
path,opts?):ActionGroupHandle
Defined in: src/traverse/nav-session.ts:248
Register a component's handlers/actions ON a node when it renders. You never
name a group — this RETURNS an ActionGroupHandle that is the identity (with a
generated id). Hold it in a ref; call handle.unregister() on unmount.
handle.setEnabled(actionId, false) greys one action out (a disabled button).
Parameters
path
Paths
opts?
Returns
registerHandlers()
registerHandlers(
opts):RegisteredHandlers
Defined in: src/traverse/session.ts:1855
Bind the app's existing handlers to declared actions on a FLAT graph (no
node tree). Takes a caller group string; the tree API
(InteractionSession.registerActions) is preferred where you have a node
path — it returns a handle so you never invent a group name.
Parameters
opts
Returns
Inherited from
reject()
reject(
transitionId,opts?):TransitionRecord
Defined in: src/traverse/session.ts:4838
The app rejected/rolled back a transition's effect (optimistic UI). Works on a PENDING transition (effect never landed → no bundle) and on an already-SETTLED one (server rejected after the optimistic report): the record flips to rolled-back and the app's compensating delta should follow via updateState — the commit log keeps both writes, honestly.
Parameters
transitionId
string
opts?
outcome?
"rejected" | "rolled-back" | "superseded"
Returns
Inherited from
reportGap()
reportGap(
opts):GapRecord
Defined in: src/traverse/session.ts:4986
Report an ask that no available action or journey could serve (typically called by the agent's report_gap tool before it apologizes). The row is token-lean by design: the ask plus NAME lists, never descriptions.
Parameters
opts
Returns
Inherited from
requiresHumanApprovalFrom()
requiresHumanApprovalFrom(
principal):boolean
Defined in: src/traverse/session.ts:1019
Whether a high-effect fire STAMPED WITH THIS PRINCIPAL has to present a recorded human approval — the question a serving layer must actually ask before it promises a model that this app enforces one.
The gate keys on the principal, not the door (fire(), THE APPROVAL GATE): the
app-self-report tier — 'user', 'system', and the record-only sensor —
passes, because that motion really happened. So requiresHumanApproval alone
is the wrong question for a PORT: a port that stamps 'user' serves a model
whose fires this session never gates, and a tool description promising the
gate would be a lie told in the library's own voice.
Here rather than re-derived at the port, so the rule lives in one place: the thing that answers "is this fire gated?" is the thing that gates it.
Parameters
principal
Returns
boolean
Inherited from
Session.requiresHumanApprovalFrom
revokeAlwaysApprove()
revokeAlwaysApprove(
affordanceId,opts):ApprovalResult
Defined in: src/traverse/session.ts:5806
WITHDRAW a standing grant. It stops authorizing immediately, and each grant
withdrawn lands its own 'revoked' row carrying that grant's id.
Omitting instance revokes EVERY grant for the action, instance-scoped ones
included. Revocation over-reaches on purpose: the failure mode of a too-broad
revoke is one extra trip past the human, and the failure mode of a too-narrow
one is a hole the person believed they had closed.
Parameters
affordanceId
string
opts
by
string
instance?
string
note?
string
Returns
Inherited from
revokeAsk()
revokeAsk(
askId,opts):ApprovalResult
Defined in: src/traverse/session.ts:5694
WITHDRAW A YES THE PERSON ALREADY GAVE, before anything spends it — the ask
book's third word. The ordinary human act of changing one's mind had no
door: declineAsk refuses an answered card (a decision is never
overwritten), so a withdrawal was caught only by the app's own rules and
was INVISIBLE on the served surface, which kept holding a yes the person
had taken back.
APPEND-ONLY, like everything in this journal: the 'approved' row is
NEVER rewritten. The withdrawal is a NEW 'revoked' row referencing the
askId — principal, timestamp, by — and the ask book carries the fact as
data (AskStatus.revoked) beside the answer it does not touch. A
fire that then presents the pointer refuses APPROVAL_REVOKED, through
every door the gate guards; the cure is a fresh ask.
THE BOUNDARIES, each a typed refusal rather than a throw:
- an UNANSWERED card refuses
REVOKE_UNANSWERED— there is no yes to withdraw, and the right verb for answering no is Session.declineAsk; - a DECLINED card refuses
ASK_ALREADY_ANSWERED— the no already refuses every fire, and needs no withdrawal; - a SPENT yes refuses
ASK_ALREADY_SPENT— revoking cannot un-fire the past, and the honest record of what happened is the'used'row; - a card already revoked refuses
ASK_ALREADY_ANSWERED— the withdrawal is recorded once, never doubled.
ONLY THE HUMAN SIDE REVOKES, in either direction. Like its siblings this
door stamps principal: 'user'; unlike them it accepts an optional
principal CLAIM so an honest relay (a port built with source: 'agent',
a scripted driver) can state what it is — and any claim other than
'user' is refused WRONG_PRINCIPAL. An agent must never be able to
withdraw a human's decision: honouring an agent's revoke would let it
cancel a yes it dislikes as surely as forging one it wants.
Parameters
askId
string
opts
by
string
note?
string
principal?
Returns
Inherited from
sense()
sense(
affordanceId,declaration): () =>void
Defined in: src/traverse/session.ts:3487
SENSE-ONLY — declare that this action lives at that element, with no handler to wrap. The L0 on-ramp, and the human-interleave path: an app whose button does its own thing still gets its people into the record.
A TRUSTED click inside the anchor opens a record-only fire (the browser has
already run the app's code — a fire that also invoked would run one human
click twice) stamped cause.inferred, carrying the correlation rule on the
record. Nothing here performs anything, and nothing here reads a value.
Returns the release, token-identity like every other declaration pair in this library. An id no affordance answers to is filed and simply reports whatever refusal its own fire earns — a control can be handed over before the action that declares it is mounted, and refusing at this door would only shout at a mount race.
Parameters
affordanceId
string
declaration
Returns
() => void
Inherited from
sensedTrail()
sensedTrail(
transitionId): readonlySensedEvent[]
Defined in: src/traverse/session.ts:3518
The whole event trail behind one contextful fire — the door the record points at when the trail was too long to ride inline.
ONE DOOR FOR BOTH SHAPES: an inline trail is answered off the record itself, so a caller never has to branch on which shape it got (the record still SAYS which, because a reader deserves to know whether they are holding everything or a pointer to it). Copies, never the live arrays.
Throws for an id this session cannot answer for — the same stance
Session.settlementOf takes, and for the same reason: a silent []
would read as "nothing happened" about an action that may have had three
hundred events.
Parameters
transitionId
string
Returns
readonly SensedEvent[]
Inherited from
settlementIfKnown()
settlementIfKnown(
transitionId):FireSettlement|undefined
Defined in: src/traverse/session.ts:3970
The same answer WITHOUT waiting: the settlement if this fire has already
come to rest, undefined while its question is still open. Refuses an
unknown id exactly as Session.settlementOf does — one law, two
doors.
This is the door a SYNCHRONOUS caller needs (Mode B's did_it_work tool
answers a model inside one tool call and must never block it). undefined
means "no settlement yet", never a guessed outcome — the caller reports
still-pending, which is the truth at that instant.
Parameters
transitionId
string
Returns
FireSettlement | undefined
Inherited from
settlementOf()
settlementOf(
transitionId):Promise<FireSettlement>
Defined in: src/traverse/session.ts:3949
How a fire came to rest — asked at ANY time by anyone holding its
transitionId. fire() hands ITS caller whenSettled; this is the same
answer for everyone else, and it is the hole the field report named: a
promise cannot cross a wire, so a remote agent (or the relay in front of
it) held the id and had no way to learn the truth.
- Still open → that fire's own latch promise. Same law as
whenSettled: one answer, first settlement wins, never rejects. - Already at rest → resolves immediately with a detached copy.
- NEVER REPORTED (a fire whose app report never arrives) → the promise
honestly stays open, exactly as
whenSettleddoes. There is no timeout arm on purpose: FireSettlement excludes 'pending' by construction, so a timed-out answer could only be a guessed 'unobservable'. When you need an answer that cannot wait, ask the non-blocking doors — Session.pending, Session.settlementIfKnown, or Mode B'sdid_it_worktool. - Unknown id, or a stimulus/sync/structure-swap row → THROWS synchronously. A promise that could never resolve is precisely the failure this method exists to prevent: a mistyped id that waits under someone's ceiling and then reports a fabricated outcome.
Parameters
transitionId
string
Returns
Promise<FireSettlement>
Inherited from
setVisible()
setVisible(
path,visible):void
Defined in: src/traverse/nav-session.ts:583
Parameters
path
Paths
visible
boolean
Returns
void
show()
show(
path):void
Defined in: src/traverse/nav-session.ts:590
Show a node; for a tab this also hides its tab siblings (at most one shown).
Parameters
path
Paths
Returns
void
state()
state():
Record<string,unknown>
Defined in: src/traverse/session.ts:1391
Detached snapshot of the projected state (live state is immutable-after-swap; never hand out references).
Returns
Record<string, unknown>
Inherited from
sync()
sync(
observedNode,opts?):SyncResult
Defined in: src/traverse/nav-session.ts:976
The observed node is runtime input from the world, so an unauthored page is NOT an error: the cursor follows reality (off-graph), available() honestly serves zero edges there, and the hop is still recorded.
Parameters
observedNode
string
opts?
principal?
stimulus?
Returns
Overrides
toMCPTools()
toMCPTools(
opts?):MCPToolDescription[]
Defined in: src/traverse/session.ts:6341
Per-edge MCP tool descriptors for the CURRENT slice. Regenerated per call — never cached. With a journey frame open, serves ONLY the frame's currently-fireable steps + escape tools (authored cancel/back roles and a synthetic leave-journey) — the on-demand disclosure contract.
Parameters
opts?
lossySchemas?
boolean
Returns
Inherited from
transitions()
transitions(): readonly
TransitionRecord[]
Defined in: src/traverse/session.ts:4950
The interaction log. Settled/stimulus/sync rows join 1:1 to commitLog() bundles by TransitionRecord.id; pending and rejected/rolled-back rows exist only here (their effects never touched state). Rows are snapshots — live records are the ones returned by fire()/updateState()/reject().
Returns
readonly TransitionRecord[]
Inherited from
tryJourneyPlan()
tryJourneyPlan(
journeyId):TryJourneyPlanResult
Defined in: src/traverse/session.ts:2244
journeyPlan() for an id the caller did not author — a model's, a URL's, a config file's — answering with a value instead of a throw. Same plan; the failure arm is the UNKNOWN_JOURNEY shape commitJourney() already returns.
journeyPlan() keeps throwing, deliberately. Every caller inside the library passes an id the spec itself just yielded, and there an unknown id is a bug that should stop the program, not a branch someone forgets to write. This is the door for ids that arrive from outside, where not-a-journey is an ordinary answer.
Membership is Object.hasOwn rather than a truthiness lookup BECAUSE the ids
here are untrusted: journeys['constructor'] is truthy on any plain object,
so a lookup would sail past the guard and fail downstream reading .steps
off Object's constructor — a TypeError where the caller asked for exactly
the honest "no such journey" this method exists to give.
Parameters
journeyId
string
Returns
Inherited from
unregisterGroup()
unregisterGroup(
group):string[]
Defined in: src/traverse/session.ts:1920
Remove every live binding currently owned by group (component unmount).
Parameters
group
string
Returns
string[]
Inherited from
updateState()
updateState(
delta,opts?):UpdateResult
Defined in: src/traverse/session.ts:4192
Report a projected-state delta from the app (router/store tap).
Attribution, in priority order:
opts.transitionId— settles that pending transition precisely (preferred).opts.stimulus/opts.principalset — recorded as a stimulus transition, NEVER attributed to a pending fire (explicit attribution wins; a server push must not hijack a pending action's provenance).- Otherwise: FIFO to the oldest pending fired transition. With several pendings and out-of-order app handlers this can mis-attribute — pass transitionId from your tap when you can; effectVerified=false is the designed detector for key mismatches.
- No pendings, no hints: recorded as a
stimulus:'unknown'transition — state never moves silently.
Undefined-valued entries are dropped from the report before anything else
(uniformly — new and existing keys): a report cannot store undefined, and
a declared write reported as undefined counts as missing
(effectVerified: false).
Parameters
delta
Record<string, unknown>
opts?
Returns
Inherited from
warn()
warn(
message):void
Defined in: src/traverse/session.ts:1069
The session's dev-warning sink (SessionOptions.onWarn, console.warn by default) — the seam subclass layers already warn through.
PUBLIC because the serving layer is a separate module by design: it consumes
only this surface, and a warning it could not route through the host's own
sink is a warning a host that captures onWarn would never see. It says
nothing about state, so nothing can be forged with it.
Parameters
message
string
Returns
void
Inherited from
whatUnblocks()
whatUnblocks(
affordanceId):DependencyEdge[]
Defined in: src/traverse/session.ts:2158
WHAT WOULD FREE THIS ACTION — the actions whose declared writes touch a key this one is waiting on, each with the specific keys.
The same rule journeyPlan runs over a journey's steps, widened to every
declared action: DERIVED, never authored. Both halves already exist for
other reasons (writes powers verification; guard and enabledWhen
power availability), so nothing new is declared and nothing can drift.
IT ANSWERS A QUESTION A GREYED CONTROL OTHERWISE CANNOT. enabled: false
says a control is off; this says what the app itself claims would change
that — so a reader stops re-firing a dead button to find out.
FOUR HONESTY LIMITS, each a test:
- Only the conditions that did NOT hold. The keys are evaluated against live state, never read off the declaration. A control is offered at all only once its guard HOLDS, so naming actions that write a satisfied condition's keys would answer with the actions that DESTROY the thing the control is standing on — "discard the draft", "log out" — and read as advice to fire them. Inverted, and inverted toward the highest-effect actions in the app. What is not holding it back is not an answer to what would free it.
- A claim, not a promise.
writesis the app's claim that an action changes a key. This reports the claim; firing that action is not promised to free this one. - Silence over guessing. An action nobody claims to write a key for
returns
[]— the honest "nothing here knows what would change it", never an invented suggestion. A condition the library could not evaluate is dropped by the same law: it is not evidence of a block. - Never a plan. The list is unordered and unranked. Ordering intent is a journey, which is declared, not derived.
Scope is every declared action, not just this node's: the control that frees a greyed button often lives on another page, and hiding a true answer to keep the list short would be the wrong trade.
Parameters
affordanceId
string
Returns
Inherited from
whenPageChanges()
whenPageChanges(
listener): () =>void
Defined in: src/traverse/session.ts:1168
Run something every time the app REPORTS that it is on a different page — after the hop is recorded, the version has moved and observers have seen it.
FIRES ON Session.sync ONLY, and only when the reported node differs from where the cursor was. Not on a claimed navigation: that moves the cursor on the app's word, before the app's own handler has run, so anything read there would describe the page the app has not left yet. Not on a structure change either — the cursor did not move, and that flush is usually a listener here seeing its own mount come back around.
The door a LIVE SOURCE needs and on('transition') cannot be: the transition
event fires BEFORE the version bump, and its listeners are documented passive
(they never change what the session does), while the whole point here is a
reaction that DOES — re-reading an action store and mounting or releasing
bindings. So this is its own surface, and the two contracts stay true.
ONE SYNC IS THE LIBRARY'S OWN: when a fire that moved the cursor on a claim
is rolled back by its handler failing, the session walks the cursor home with
a sync(fromNode, { principal: 'system' }). That is a position report like
any other — the cursor really is back — so this fires for it too, and a live
source re-reads the page it is actually on.
Listeners run in registration order and are ISOLATED (a throw is caught and warned, never aborting the hop). A listener that moves the cursor again does not recurse into this broadcast; the move is remembered and the pass runs again, so no listener is left holding a page the session has left. Returns the unsubscribe.
Parameters
listener
() => void
Returns
() => void
Inherited from
why()
why(
key):string
Defined in: src/traverse/session.ts:4955
"Why does this state key hold its value?" — footprint backward slice, formatted.
Parameters
key
string
Returns
string