Ground truth — facts the model cannot argue with
groundTruth() is the app's own record of what was attempted and how each attempt came to rest, in words a model is told outrank the conversation — and it rides every whats_here result as `facts`.
Never feed the model its own prose
Reported from a production integration, with no library bug in it: given nothing to check itself against, the model narrated an entire flow — name set, recipe selected — having called zero tools. Its own earlier sentences had become its context, and on the next turn it read them as history.
That is the failure mode this page exists for, and it is structural rather than careless. A model weighs everything in its window; unless something in there says which source wins, the conversation competes with the app on equal terms. And the friendly narrative beside it could not have prevented it either: a refused fire is a gap-ledger row, not a transition, so a narrative built from transitions can never show a failed attempt. The model saw successes and silence, and filled the silence.
session.groundTruth() is the counter: one block, both ledgers, every attempt — refusals
included — under a header that states the ranking outright.
The block, anatomy first
const = .({ : 7 }); // → { node, version, text }
.;text reads, in this order:
- the header — the sentence that makes it a floor rather than another opinion: every line below is the app's own record, and where anything said in the conversation disagrees with it, these lines are what actually happened; the conversation is a claim about them;
- where you are — the cursor (and, on the tree API, the focus below page level);
- every attempt, one line each, oldest first;
- where a human decision stands — one line per confirm ask, by action and
askId, in three shapes: Awaiting the human's decision, Approved by the human, not yet done, and The human declined. The two answered shapes exist because marking an ask answered would otherwise go silent in exactly the window that matters — a recorded approval nothing has acted on yet, and a no an agent might quietly re-ask around; - what the app has not answered — any fire still awaiting its state report.
The awaiting lines obey the same maxAttempts dial as the attempts list, because under
enforced approval an agent mints a
new ask per distinct input and could otherwise inflate the one block a model is told to trust above
its own account. The oldest cards are kept — burying the question a person is actually looking
at under forty fresh ones is the move this refuses — and the omitted count is stated outright.
Answered lines are never capped: only a human-side door writes one, so hiding a recorded decision
is the one thing this block must never do. by and note are not rendered here; they are the
audit fields, and this block carries structural facts only.
With nothing attempted it says so, in as many words:
No actions have been performed in this app this session.That line is said outright because a silence is exactly what a model fills with invention. It
claims the whole session, so it is only used where that is true: behind a sinceVersion
cursor that would hide real earlier attempts, the block reports the cursor's own truth and
counts what it hid instead. A block a model is told to trust above its own memory can never be
the thing that denies what happened.
Nothing is rounded up
Every attempt is graded, and the grades are deliberately unglamorous:
| the line reads | when |
|---|---|
| DID happen | committed, and the declared effect was actually observed |
| ran, but the declared effect was NOT observed | committed; the delta did not carry the keys it claimed |
| ran, but the effect was unobservable | committed; nothing reported an effect to check it against |
| did NOT happen | refused at the gate (with the typed reason), rejected, rolled back, a tour no-op — or the app's own verify contract did not hold |
| not yet known | the app has not reported back yet |
A fire nobody could check says so rather than borrowing the stronger word — that grading is the whole reason the block is worth trusting. Attribution rides along too: a step attributed by effect-signature inference is marked attributed by inference, not observed.
A blocked forgery lands here as a fact, not as silence. Under enforced approval a high-effect fire carrying no approval the library can prove is refused, and the refusal reads
did NOT happen — agent's fire of checkout.place-order was refused: APPROVAL_REQUIREDso the next turn cannot narrate the order as placed, which is the exact failure this page exists for.
Attempts are ordered by cursor version, which is a proof rather than a heuristic: a refusal never bumps the version and a recorded fire bumps it immediately, so every row carrying version V sits inside the window that one transition closed — refusals first, the transition last. Timestamps could only tie at millisecond grain and invent an order.
What it leaves out, and why
As deliberate as what it holds:
- state values and payloads — the two-string-class invariant, extended to history;
- produced data — that is the data channel (
producedFor), not the facts channel; - available actions and journeys — options are
whats_here's other half. Facts are what happened; keeping the two non-overlapping is what keeps both lean; reportedgap rows and all runtime free text — a user's own words are data, never something to hand a planner as instruction. With one exception, and it carries no free text either: a row markedactionsMayBeStaleprints one authored line saying the app's list of actions could not be re-read. Every other line here is about what happened; that one is about whether the room being described is still the room, which is the one thing a reader of this block cannot afford to be missing;- interpretation — one line per occurrence, no advice, no summary.
An id the graph does not have renders as a constant — (an action this app does not have) — rather than echoing a model's own invention back at it in the one block it is told to trust.
A name is evidence captured at its moment
That constant guards a real attack, and for a while it also caught the wrong thing.
Every render here used to answer is this a real action? by looking the id up in the graph as it
stands when you read. In an app whose controls are all declared up front that is the same question
as was it real when it happened. In an app where a component
declares its own actions as it mounts, it is not. A compose pane declares
send, an agent fires it, the pane unmounts — and the facts block called a genuinely-fired action
(an action this app does not have). The app authored it; the library forgot.
So the library stops asking later. When a row is minted for an action the graph has at that
moment, the affordance's authored does is frozen onto the row right then, and every render
prefers what the row carries:
.(); // the compose pane closes
.()[0]..; // → 'Send the message', still
.().; // → "… agent fired inbox.compose.send …"The field is called does, the same word the app declares it with, and it sits beside the
affordanceId it belongs to on TransitionRecord's Cause, on PendingInfo (a
fire still awaiting the app's report), on WorkRow (work the app says it is still
doing), on AskStatus (a card waiting on a
person), and on the fire-rejected rows of GapRecord.
Three rules make it worth trusting:
- Presence-only. Absent means this row's action was not declared at that moment — the honest
answer for a fire of a name a model invented, and the reason that name still cannot reach an
authored sentence: with nothing captured, the render falls through to the constant exactly as
before. A
TOOL_DISABLEDrefusal of a real control keeps its name; anUNKNOWN_AFFORDANCErefusal keeps nothing. - From the graph and nowhere else. Never from a fire's arguments, never from a payload, never from a caller's string — this is the authored channel, and caller text entering it is the injection the constant exists to refuse.
- Never retroactive, and never re-read. A row is not backfilled when the action mounts later, and it keeps the sentence the app was showing then: re-mount the same id with new wording and history still quotes what was on screen when it happened.
None of this changes what is on OFFER. The unmounted action is gone from available() and gone from
the tool array the moment it unmounts — a record is a result, never a tool. What changed is that
history can still say its name.
An attempt is a fire the session was asked to make
Every line here comes from one of the two ledgers, and fire() writes both — the transition
it recorded, or the gap row it refused. A reach that never gets that far leaves no line, and
Mode B has exactly one such reach: do_action resolves an
action name against what is being served, so a name matching none of them is refused by
the port before any fire. That refusal teaches in the turn it happens — it names whether the
app has the action elsewhere or on this page with its conditions unmet, with the same evidence
a GUARD_FAILED fire carries — but it is not an attempt, and the ledgers hold what the
session was asked to do. A name it could not resolve was never something it could act on.
Wiring it per turn
Over Mode B there is nothing to wire: every whats_here result already carries facts,
first in the result, on the call a model makes anyway. Pass { sinceVersion } and it narrows
with the rest of the result.
Driving a loop yourself, inject it once per turn, and label it so the ranking survives the trip:
const = .({ : });
const = .({ : });
= .;
void ([{ : 'user', : `${.}\n\n${.}\n\n${}` }]);Two blocks, two jobs. contextBrief is the friendly narrative — position,
options, what happened lately. groundTruth is the record. Send both; they do not overlap.
Three rules make the difference between a floor and more noise:
- Send it every turn. Grounding a model once grounds it for one turn.
- Send it verbatim. The header is the part that ranks it; paraphrasing it away leaves you with an unlabelled list of sentences competing with the model's own.
- Never feed the model its own prose as history. A summary of the assistant's earlier turns is a claim; this block is the record. If they disagree, the block is right — which is exactly what its header tells the model.
It is not a permission system
The facts block is accountability for a cooperating model, not a boundary. It tells a model that is trying to be right what is true, and it makes a model that narrates instead of acting visibly wrong on the next turn. Enforcement lives where it always did: the guard, the confirm gate, and your server.
Worked end to end — with a refusal, a silent no-op and a real success in the same block — in Guarded journeys.
When the app is still working
The work ledger — your app says what it is still doing, bound to the fire it belongs to. A row, never a latch: closing it settles nothing, and no clock here will ever close it for you.
Tree-shaking & packaging
True-ESM, sideEffects false, leaf modules — importing one helper ships half a kilobyte, and the repo's own test suite bundles the shipped dist to keep it that way.