agentfootprint 9.82.0 → 9.84.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/CHANGELOG.md +718 -504
  2. package/CLAUDE.md +1 -0
  3. package/README.md +15 -15
  4. package/ai-instructions/claude-code/SKILL.md +1 -1
  5. package/dist/core/Agent.js +43 -1
  6. package/dist/core/Agent.js.map +1 -1
  7. package/dist/core/agent/AgentBuilder.js.map +1 -1
  8. package/dist/core/agent/evidence/evidenceIndex.js +96 -11
  9. package/dist/core/agent/evidence/evidenceIndex.js.map +1 -1
  10. package/dist/core/agent/evidence/frames.js +15 -6
  11. package/dist/core/agent/evidence/frames.js.map +1 -1
  12. package/dist/core/agent/evidence/gate.js +66 -6
  13. package/dist/core/agent/evidence/gate.js.map +1 -1
  14. package/dist/core/agent/outputEnforcement.js +7 -4
  15. package/dist/core/agent/outputEnforcement.js.map +1 -1
  16. package/dist/core/agent/selfCallNotice.js +174 -0
  17. package/dist/core/agent/selfCallNotice.js.map +1 -0
  18. package/dist/core/agent/stages/route.js +71 -12
  19. package/dist/core/agent/stages/route.js.map +1 -1
  20. package/dist/core/agent/stages/toolCalls.js +87 -2
  21. package/dist/core/agent/stages/toolCalls.js.map +1 -1
  22. package/dist/core/agent/window/currentRequest.js +11 -13
  23. package/dist/core/agent/window/currentRequest.js.map +1 -1
  24. package/dist/core/agent/window/notice.js +9 -9
  25. package/dist/core/agent/window/notice.js.map +1 -1
  26. package/dist/core/agent/window/summarize.js +9 -9
  27. package/dist/core/agent/window/summarize.js.map +1 -1
  28. package/dist/doors/skill-graph.js +2 -1
  29. package/dist/doors/skill-graph.js.map +1 -1
  30. package/dist/esm/core/Agent.d.ts +4 -0
  31. package/dist/esm/core/Agent.js +43 -1
  32. package/dist/esm/core/Agent.js.map +1 -1
  33. package/dist/esm/core/agent/AgentBuilder.d.ts +15 -0
  34. package/dist/esm/core/agent/AgentBuilder.js.map +1 -1
  35. package/dist/esm/core/agent/evidence/evidenceIndex.d.ts +91 -5
  36. package/dist/esm/core/agent/evidence/evidenceIndex.js +96 -11
  37. package/dist/esm/core/agent/evidence/evidenceIndex.js.map +1 -1
  38. package/dist/esm/core/agent/evidence/frames.d.ts +9 -4
  39. package/dist/esm/core/agent/evidence/frames.js +13 -5
  40. package/dist/esm/core/agent/evidence/frames.js.map +1 -1
  41. package/dist/esm/core/agent/evidence/gate.d.ts +27 -0
  42. package/dist/esm/core/agent/evidence/gate.js +66 -6
  43. package/dist/esm/core/agent/evidence/gate.js.map +1 -1
  44. package/dist/esm/core/agent/evidence/types.d.ts +12 -0
  45. package/dist/esm/core/agent/outputEnforcement.d.ts +1 -2
  46. package/dist/esm/core/agent/outputEnforcement.js +4 -2
  47. package/dist/esm/core/agent/outputEnforcement.js.map +1 -1
  48. package/dist/esm/core/agent/selfCallNotice.d.ts +140 -0
  49. package/dist/esm/core/agent/selfCallNotice.js +169 -0
  50. package/dist/esm/core/agent/selfCallNotice.js.map +1 -0
  51. package/dist/esm/core/agent/stages/route.d.ts +6 -1
  52. package/dist/esm/core/agent/stages/route.js +69 -10
  53. package/dist/esm/core/agent/stages/route.js.map +1 -1
  54. package/dist/esm/core/agent/stages/toolCalls.d.ts +6 -0
  55. package/dist/esm/core/agent/stages/toolCalls.js +87 -2
  56. package/dist/esm/core/agent/stages/toolCalls.js.map +1 -1
  57. package/dist/esm/core/agent/types.d.ts +66 -0
  58. package/dist/esm/core/agent/window/currentRequest.d.ts +9 -3
  59. package/dist/esm/core/agent/window/currentRequest.js +11 -13
  60. package/dist/esm/core/agent/window/currentRequest.js.map +1 -1
  61. package/dist/esm/core/agent/window/notice.d.ts +1 -4
  62. package/dist/esm/core/agent/window/notice.js +5 -6
  63. package/dist/esm/core/agent/window/notice.js.map +1 -1
  64. package/dist/esm/core/agent/window/summarize.d.ts +1 -4
  65. package/dist/esm/core/agent/window/summarize.js +5 -6
  66. package/dist/esm/core/agent/window/summarize.js.map +1 -1
  67. package/dist/esm/doors/skill-graph.d.ts +1 -1
  68. package/dist/esm/doors/skill-graph.js +1 -1
  69. package/dist/esm/doors/skill-graph.js.map +1 -1
  70. package/dist/esm/events/payloads.d.ts +15 -0
  71. package/dist/esm/index.d.ts +2 -0
  72. package/dist/esm/index.js +16 -0
  73. package/dist/esm/index.js.map +1 -1
  74. package/dist/esm/integrity/disposition/lifecycle.d.ts +17 -1
  75. package/dist/esm/integrity/disposition/lifecycle.js +26 -1
  76. package/dist/esm/integrity/disposition/lifecycle.js.map +1 -1
  77. package/dist/esm/integrity/finding/types.d.ts +12 -2
  78. package/dist/esm/integrity/finding/types.js.map +1 -1
  79. package/dist/esm/integrity/prior-turn-evidence/check.d.ts +145 -0
  80. package/dist/esm/integrity/prior-turn-evidence/check.js +200 -0
  81. package/dist/esm/integrity/prior-turn-evidence/check.js.map +1 -0
  82. package/dist/esm/lib/injection-engine/index.d.ts +1 -0
  83. package/dist/esm/lib/injection-engine/index.js +6 -0
  84. package/dist/esm/lib/injection-engine/index.js.map +1 -1
  85. package/dist/esm/lib/injection-engine/skillToolDescriptors.d.ts +29 -0
  86. package/dist/esm/lib/injection-engine/skillToolDescriptors.js +90 -7
  87. package/dist/esm/lib/injection-engine/skillToolDescriptors.js.map +1 -1
  88. package/dist/esm/lib/injection-engine/types.d.ts +54 -0
  89. package/dist/esm/lib/injection-engine/types.js +28 -0
  90. package/dist/esm/lib/injection-engine/types.js.map +1 -1
  91. package/dist/esm/lib/saidByPerson.d.ts +99 -0
  92. package/dist/esm/lib/saidByPerson.js +104 -0
  93. package/dist/esm/lib/saidByPerson.js.map +1 -0
  94. package/dist/esm/lib/trace-toolpack/traceToolpack.js +3 -2
  95. package/dist/esm/lib/trace-toolpack/traceToolpack.js.map +1 -1
  96. package/dist/index.js +23 -3
  97. package/dist/index.js.map +1 -1
  98. package/dist/integrity/disposition/lifecycle.js +26 -1
  99. package/dist/integrity/disposition/lifecycle.js.map +1 -1
  100. package/dist/integrity/finding/types.js.map +1 -1
  101. package/dist/integrity/prior-turn-evidence/check.js +204 -0
  102. package/dist/integrity/prior-turn-evidence/check.js.map +1 -0
  103. package/dist/lib/injection-engine/index.js +9 -2
  104. package/dist/lib/injection-engine/index.js.map +1 -1
  105. package/dist/lib/injection-engine/skillToolDescriptors.js +92 -8
  106. package/dist/lib/injection-engine/skillToolDescriptors.js.map +1 -1
  107. package/dist/lib/injection-engine/types.js +30 -1
  108. package/dist/lib/injection-engine/types.js.map +1 -1
  109. package/dist/lib/saidByPerson.js +111 -0
  110. package/dist/lib/saidByPerson.js.map +1 -0
  111. package/dist/lib/trace-toolpack/traceToolpack.js +3 -2
  112. package/dist/lib/trace-toolpack/traceToolpack.js.map +1 -1
  113. package/dist/types/core/Agent.d.ts +4 -0
  114. package/dist/types/core/Agent.d.ts.map +1 -1
  115. package/dist/types/core/agent/AgentBuilder.d.ts +15 -0
  116. package/dist/types/core/agent/AgentBuilder.d.ts.map +1 -1
  117. package/dist/types/core/agent/evidence/evidenceIndex.d.ts +91 -5
  118. package/dist/types/core/agent/evidence/evidenceIndex.d.ts.map +1 -1
  119. package/dist/types/core/agent/evidence/frames.d.ts +9 -4
  120. package/dist/types/core/agent/evidence/frames.d.ts.map +1 -1
  121. package/dist/types/core/agent/evidence/gate.d.ts +27 -0
  122. package/dist/types/core/agent/evidence/gate.d.ts.map +1 -1
  123. package/dist/types/core/agent/evidence/types.d.ts +12 -0
  124. package/dist/types/core/agent/evidence/types.d.ts.map +1 -1
  125. package/dist/types/core/agent/outputEnforcement.d.ts +1 -2
  126. package/dist/types/core/agent/outputEnforcement.d.ts.map +1 -1
  127. package/dist/types/core/agent/selfCallNotice.d.ts +141 -0
  128. package/dist/types/core/agent/selfCallNotice.d.ts.map +1 -0
  129. package/dist/types/core/agent/stages/route.d.ts +6 -1
  130. package/dist/types/core/agent/stages/route.d.ts.map +1 -1
  131. package/dist/types/core/agent/stages/toolCalls.d.ts +6 -0
  132. package/dist/types/core/agent/stages/toolCalls.d.ts.map +1 -1
  133. package/dist/types/core/agent/types.d.ts +66 -0
  134. package/dist/types/core/agent/types.d.ts.map +1 -1
  135. package/dist/types/core/agent/window/currentRequest.d.ts +9 -3
  136. package/dist/types/core/agent/window/currentRequest.d.ts.map +1 -1
  137. package/dist/types/core/agent/window/notice.d.ts +1 -4
  138. package/dist/types/core/agent/window/notice.d.ts.map +1 -1
  139. package/dist/types/core/agent/window/summarize.d.ts +1 -4
  140. package/dist/types/core/agent/window/summarize.d.ts.map +1 -1
  141. package/dist/types/doors/skill-graph.d.ts +1 -1
  142. package/dist/types/doors/skill-graph.d.ts.map +1 -1
  143. package/dist/types/events/payloads.d.ts +15 -0
  144. package/dist/types/events/payloads.d.ts.map +1 -1
  145. package/dist/types/index.d.ts +2 -0
  146. package/dist/types/index.d.ts.map +1 -1
  147. package/dist/types/integrity/disposition/lifecycle.d.ts +17 -1
  148. package/dist/types/integrity/disposition/lifecycle.d.ts.map +1 -1
  149. package/dist/types/integrity/finding/types.d.ts +12 -2
  150. package/dist/types/integrity/finding/types.d.ts.map +1 -1
  151. package/dist/types/integrity/prior-turn-evidence/check.d.ts +146 -0
  152. package/dist/types/integrity/prior-turn-evidence/check.d.ts.map +1 -0
  153. package/dist/types/lib/injection-engine/index.d.ts +1 -0
  154. package/dist/types/lib/injection-engine/index.d.ts.map +1 -1
  155. package/dist/types/lib/injection-engine/skillToolDescriptors.d.ts +29 -0
  156. package/dist/types/lib/injection-engine/skillToolDescriptors.d.ts.map +1 -1
  157. package/dist/types/lib/injection-engine/types.d.ts +54 -0
  158. package/dist/types/lib/injection-engine/types.d.ts.map +1 -1
  159. package/dist/types/lib/saidByPerson.d.ts +100 -0
  160. package/dist/types/lib/saidByPerson.d.ts.map +1 -0
  161. package/dist/types/lib/trace-toolpack/traceToolpack.d.ts.map +1 -1
  162. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -7,6 +7,203 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [9.84.0] - 2026-09-03
11
+
12
+ ### Fixed
13
+
14
+ - **`read_skill` refused the skill the model was already in.** A turn routed
15
+ decisively to `X`, the model called `read_skill("X")` to find out where it
16
+ stood, and the gate answered _"`read_skill("X")` is not reachable from here.
17
+ Reachable skills: …"_ — about the one skill whose body was in that call's
18
+ system prompt and whose tools were in that same call's tool list. The cursor
19
+ is in neither half of `hops ∪ open` by construction: `makeReachableSkills`
20
+ filters it out of its own successor set (a move to where you already are is
21
+ not a move) and `openSkillIds()` excludes every graph-wired skill. Nobody had
22
+ written the case for _"you asked for the room you are standing in."_ Read as a
23
+ claim about AVAILABILITY — which is how a model reads _"not reachable"_ — it
24
+ says the opposite of the request it arrived in. A field report recorded the
25
+ consequence three times in one day: the model concluded its capability was
26
+ gone and answered that it could not help, while the skill's tools sat on the
27
+ wire, loaded and callable.
28
+
29
+ A self-call now gets the truth instead of a refusal. It names where the model
30
+ stands and which tools it could call, taken from the merged wire list the LLM
31
+ stage actually sent, intersected with the skill's own declared tools — never
32
+ from the declaration alone. Every configuration that would make that false has
33
+ its own wording: a skill declaring no tools says so, a step or park hold-out
34
+ says the tools were withheld rather than naming them, and a call whose wire
35
+ cannot be established says nothing about tools at all. Mechanically it is still
36
+ a rejection — no activation, no cursor move, and the refusal budget still
37
+ counts it, because a self-call _loop_ is exactly the stuck model that budget
38
+ exists to escalate.
39
+
40
+ **Every clause is anchored to one named call.** A tool result is composed on
41
+ one iteration and re-read on every call after it, including the out-of-budget
42
+ wrap-up that carries no tools under _"Do not request tools."_ So the notice
43
+ makes no forward-looking claim at all: no exhortation to act, no offer of a
44
+ move, no clause conditioned on a budget or a posture that can change after the
45
+ sentence is written. Deixis counts as forward-looking — _"the call you just
46
+ made"_ denotes a different call on each re-read, so the anchor is named once
47
+ in the opening sentence and every later clause refers back to it.
48
+
49
+ - **The `read_skill` description said the same thing in the other channel.** The
50
+ current skill was listed under _"Not reachable from here (read_skill for these
51
+ will be refused)"_, where it appeared purely as an artefact of that same
52
+ filter. It is in neither column now, and the description names the cursor on
53
+ every call that has one — the positive signal whose absence was the root of the
54
+ field failure, since the system prompt carried the skill's body with nothing
55
+ saying which skill it was. A genuinely unreachable skill is still named there,
56
+ and a genuinely unreachable hop keeps its refusal word for word.
57
+
58
+ - **The description no longer predicts what `read_skill` will do.** Naming the
59
+ cursor is the fix; every sentence tried alongside it turned out false
60
+ somewhere. _"read_skill MOVES you to a DIFFERENT skill"_ is false at compose
61
+ time under `strictness: 'rails'` (every model hop refused) and under `'guard'`
62
+ off an outstanding menu, where the posture arm contradicts it head-on. Its
63
+ replacement — _"You do not need read_skill to go on using it"_ — was argued to
64
+ be a claim about necessity that no posture, budget or hold-out could falsify,
65
+ and the **park** falsifies it: a parked map member keeps the cursor, loses its
66
+ body and its tools, and `read_skill` is then the only door back. The
67
+ description is composed before the hold-outs run, so it cannot know when such a
68
+ claim would be lying. Outside an outstanding menu it now states the name and
69
+ stops. The menu's stay clause is unchanged.
70
+
71
+ - **Role visibility now covers the cursor.** The description read the cursor id
72
+ past the `hiddenIds` filter, so a role denied `skill_read` on the skill the
73
+ graph had routed to was still told _"You are in '\<that skill\>'"_ — leaking
74
+ the name of a capability no cursor move would ever grant it. A hidden cursor is
75
+ named nowhere: not as reachable, not as refusable, not as the cursor, and not
76
+ in the menu's stay clause. The security suite's _"a hidden skill is never
77
+ named"_ property is now driven on the `.skillGraph()` path as well as
78
+ `.skill()`; it stayed green through the leak because its agents used `.skill()`
79
+ only, so the leaking line never executed.
80
+
81
+ ### Added
82
+
83
+ - **`saidByPerson(ctx)` / `isSaidByPerson(msg)` — telling what a person said from
84
+ what the library wrote.** Five classes of `role: 'user'` message are authored
85
+ by this library, not by a person: the compaction frame, the drop notice (whose
86
+ text names tools), the schema-check and evidence-check corrections, and any
87
+ injection-delivered message. The window's own refusal engine has always applied
88
+ that rule; a `when` predicate could not, because `InjectionContext.history`
89
+ exposes only `{ role, content, toolName? }`. An author writing an entry rule
90
+ that reads history was silently matching on our own bookkeeping. One
91
+ implementation, reused by both — the rule cannot drift between routing and the
92
+ window.
93
+
94
+ - **`SkillRejectedPayload.reason`** — `'self-call' | 'unreachable' | 'posture'`,
95
+ optional and additive, so a consumer can tell a self-call from a genuine
96
+ unreachable hop without comparing two fields.
97
+
98
+ ### Documentation
99
+
100
+ - **`strictness` says what a posture governs, exactly.** A posture governs the
101
+ model's `read_skill` door and nothing else. Two doors stay open under all three
102
+ postures: OPEN skills, already stated, and a tool's `propose-transition`, now
103
+ stated with its reason and its reachability check. `'rails'` means _the model
104
+ never routes_ — never _"nothing but my declared edges routes"_; a tool of yours
105
+ that proposes is a route you declared in code instead of in the graph. No
106
+ behaviour changed: the exemption is recorded in three places and pinned by a
107
+ test whose title is the argument.
108
+
109
+ - **The injection-engine README's runtime picture matched an older engine.** Its
110
+ diagram drew one box that "evaluates triggers"; the engine is a four-stage
111
+ footprintjs subflow — Gather, Evaluate, Route, Delta — and the cursor, the step
112
+ pointer, the instruction leases and map engagement all advance inside Evaluate.
113
+ The events table placed `context.evaluated` at subflow exit; the code emits it
114
+ in stage 2 of 4. Both corrected, along with three counts that had drifted.
115
+
116
+ ## [9.83.0] - 2026-09-03
117
+
118
+ ### Fixed
119
+
120
+ - **The evidence gate claimed a boundary it did not measure.** Both of its
121
+ user-facing sentences — the correction it sends the model
122
+ (`buildEvidenceCorrection`) and the warning it prints an operator
123
+ (`evidenceRefusalSentence`) — said the flagged values _"appear in NO tool
124
+ result **from this turn**"_. The index behind them has never been turn-scoped:
125
+ it walks every `role: 'tool'` turn in the history. The library was asserting a
126
+ scope it could not honour, in the two places that assertion is read.
127
+
128
+ Both now say what the check really reaches — _"appear in no tool result this
129
+ run read"_ — which is both true and the stronger claim, and the operator
130
+ sentence adds the two facts a reader needs: that the corpus is the LIVE
131
+ WINDOW (a window strategy rewrites `scope.history` in place, so a dropped
132
+ result is not in it), and that `noticePriorTurnEvidence` is what answers the
133
+ recency question. The frame PREFIX is unchanged, so
134
+ `isLibraryAuthoredTurn` and every consumer matching on it are untouched.
135
+
136
+ ### Added
137
+
138
+ - **`noticePriorTurnEvidence` — the answer is grounded, and nothing this turn
139
+ fetched grounds it.** Default off.
140
+
141
+ The measured failure: a consumer's agent answered a data question with **zero
142
+ tool calls**, and the gate approved it — `LLM calls 1 · Tool calls 0 ·
143
+ Iterations 1`, then _"All 7 values in the answer were found in what the tools
144
+ returned — the answer stands."_ They were found: in an inventory result from
145
+ four turns earlier, fetched for a different question. The user had asked about
146
+ array performance; the answer recommended enabling a collector that had been
147
+ running for months. Two turns did it back to back. Every rail passed honestly
148
+ — the gate measures GROUNDEDNESS and had no notion of WHEN a value was
149
+ grounded.
150
+
151
+ Every indexed form now carries the turn that last served it — one number,
152
+ stamped during the walk the index was already doing (`EvidenceCorpus.values`
153
+ became a `Map<form, turn>`; a TURN starts at each `role: 'user'` message the
154
+ library did not author). When at least one value in the answer is grounded and
155
+ **not one of them** came from the turn being answered, one `advisory` finding
156
+ is filed at the claim seam:
157
+
158
+ ```ts
159
+ const agent = Agent.create({ provider, model, noticePriorTurnEvidence: true })
160
+ .tool(arrayInventory)
161
+ .namesAndNumbersFromEvidence() // ← the other half: it owns the extractor
162
+ .build();
163
+
164
+ await agent.run('what arrays are there?'); // fetches, answers, files nothing
165
+ await agent.followUp('how is array performance?'); // no tool call, answers from turn 1
166
+ // → prior-turn-evidence: 3 grounded value(s), all last served in turn 1,
167
+ // and this turn called no tool at all.
168
+ ```
169
+
170
+ **The corpus is deliberately NOT narrowed to this turn.** That would have made
171
+ the old sentence true and been the wrong fix: _"and what about that disk?"_
172
+ leans on the previous turn's rows legitimately, and a check that cries wolf is
173
+ a check somebody switches off. ONE grounded value from this turn's own results
174
+ files nothing — not a threshold to tune, but the falsification of the claim
175
+ being tested. A follow-up that calls a tool usually gets that for free,
176
+ because a lookup keyed on an earlier identifier echoes it back.
177
+
178
+ A turn that served no tool results at all is the SAME finding with a stronger
179
+ witness, not a second kind: it is a cheaper proof of the identical fact.
180
+
181
+ **The ceiling** ships as `PRIOR_TURN_EVIDENCE_CEILING`, exported and quoted
182
+ verbatim into every message: referring back is indistinguishable, by evidence
183
+ alone, from going stale; the ordinals count only the turns still in the live
184
+ window, so the distance is a FLOOR (the boundary itself is exact — the
185
+ current request is un-droppable); and values that reached the model through
186
+ `.memory()` recall or RAG are exempt from grounding and invisible to it, so it
187
+ can under-report and never over-report.
188
+
189
+ **Two halves arm it**, and the second is structural rather than a policy
190
+ companion: the dial AND `.namesAndNumbersFromEvidence()`, whose extractor
191
+ decides which tokens in an answer are values at all. It REPORTS — whether an
192
+ answer is advised or refused stays the gate's own `posture` decision, and
193
+ nothing here blocks, revises or rewrites anything. Absent, a run is
194
+ byte-identical save the registered `prior-turn-evidence` row filed
195
+ `not-applicable`, which is the family's law rather than an exception to it.
196
+
197
+ Three terminal exits reach a caller without the gate ever producing a
198
+ grounding reading — an empty answer, a middleware denial, and an answer the
199
+ output schema rejected — and each files its disposition rather than leaving
200
+ the armed row untouched. An untouched armed row is what `assertAlive` reads
201
+ as wiring rot, so without this an empty answer under
202
+ `integrityPosture: 'dev'` would have failed a healthy run with
203
+ `CheckerDeadError`.
204
+
205
+ Docs: [Prior-turn evidence](https://agentfootprint.dev/docs/monitor/prior-turn-evidence).
206
+
10
207
  ## [9.82.0] - 2026-08-30
11
208
 
12
209
  ### Added
@@ -15,7 +212,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
15
212
  is generated from what the run itself said: the branch descriptions the chart
16
213
  declared, and the rule labels this run's `decide()` evidence carried. For one
17
214
  branch, both sources are silent by construction — the DEFAULT. It is the
18
- branch chosen by *no rule* (it fires exactly when every rule failed, so no
215
+ branch chosen by _no rule_ (it fires exactly when every rule failed, so no
19
216
  `label` describes it), and when the decider lives inside a dynamically
20
217
  generated fan-out branch the branch chart does not exist at build time either,
21
218
  so there is no declared description to fall back on.
@@ -92,7 +289,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
92
289
  sdk: { Client, StreamableHTTPClientTransport },
93
290
  transport: { transport: 'http', url: '/py/mcp' },
94
291
  });
95
- const tools = await sidecar.tools(); // the same readonly Tool[], _meta and all
292
+ const tools = await sidecar.tools(); // the same readonly Tool[], _meta and all
96
293
  ```
97
294
 
98
295
  **`connection?: McpConnection`** — the full escape hatch: you connect the
@@ -166,8 +363,8 @@ arms through a real socket against the real SDK.
166
363
 
167
364
  **What none of that proves: a browser.** This repo has no browser test
168
365
  environment, so nobody has yet driven initialize/listTools/callTool from an
169
- actual page. The honest status is *proven in Node, fenced at the graph, not gated
170
- in a browser.* Three costs land on the app, not here: your server must send CORS
366
+ actual page. The honest status is _proven in Node, fenced at the graph, not gated
367
+ in a browser._ Three costs land on the app, not here: your server must send CORS
171
368
  headers (every MCP request preflights, and `Mcp-Session-Id` must be in
172
369
  `Access-Control-Expose-Headers`) — `mcpServe` sends none; SSE through a dev or
173
370
  production proxy is unproven; and the SDK's client path adds roughly 260 KB
@@ -186,8 +383,8 @@ first time a tool with an `outputSchema` is validated.
186
383
  had a standing rule against exactly that. The library was overruling it.
187
384
 
188
385
  The instruction was ours. Every verdict projection shipped `table`
189
- pre-rendered with `VERDICT_RENDER_NOTE`: *"table is PRE-RENDERED over the
190
- same rows as `verdicts` — output it VERBATIM."* That note is RIGHT wherever
386
+ pre-rendered with `VERDICT_RENDER_NOTE`: _"table is PRE-RENDERED over the
387
+ same rows as `verdicts` — output it VERBATIM."_ That note is RIGHT wherever
191
388
  the model's words are the rows' only surface — a chat client, a log line, an
192
389
  email — because the alternative there is retyping, and a retyped identifier
193
390
  that looks right and matches nothing is the failure the note exists to stop.
@@ -200,7 +397,7 @@ first time a tool with an `outputSchema` is validated.
200
397
  it is in. So the caller says, in one word:
201
398
 
202
399
  ```ts
203
- runbookAsTool({ /* … */ presentation: 'panel' }); // default: 'prose'
400
+ runbookAsTool({ /* … */ presentation: 'panel' }); // default: 'prose'
204
401
  ```
205
402
 
206
403
  - **`'prose'` (the default)** — today's envelope, key for key: `table`
@@ -266,7 +463,7 @@ first time a tool with an `outputSchema` is validated.
266
463
  `recording_note`. The wire ops that already redeem the walk redeem this with
267
464
  **zero new operations**.
268
465
 
269
- **OPT-IN, and that is the honest default.** A walk carries *sentences about*
466
+ **OPT-IN, and that is the honest default.** A walk carries _sentences about_
270
467
  what happened and no payload from it — values are off by construction
271
468
  (`narrative({ includeValues: false })`). A recording is the run: shared
272
469
  state, the whole commit log, every attached recorder's data — **whatever the
@@ -302,7 +499,7 @@ first time a tool with an `outputSchema` is validated.
302
499
  the spine exists to prevent.
303
500
 
304
501
  **`events` is empty by construction, and says so.** It is the typed
305
- *agentfootprint* stream, fired by an agent turn; what ran here is a
502
+ _agentfootprint_ stream, fired by an agent turn; what ran here is a
306
503
  footprintjs chart on its own executor, which fires none. All three keys are
307
504
  present (that is what a viewer reads), the empty array is the honest count,
308
505
  and the note states it so nobody reads it as a dropped stream — the walk's
@@ -321,8 +518,8 @@ first time a tool with an `outputSchema` is validated.
321
518
 
322
519
  - **`Tool.resultColumns` + `checkColumnTypes` — the column-type contract: a
323
520
  tool declares what its rows contain, and the library checks the rows against
324
- it at the boundary.** Three recorded failures, and they are one shape — *a
325
- number became something else, and nothing noticed at the seam*:
521
+ it at the boundary.** Three recorded failures, and they are one shape — _a
522
+ number became something else, and nothing noticed at the seam_:
326
523
 
327
524
  1. A mapping report wrote `str(m.get("logical_unit_number") or "")`. **LUN 0
328
525
  is falsy**, so LUN 0 was stored as an EMPTY STRING on 2,094 mappings, and
@@ -351,19 +548,19 @@ first time a tool with an `outputSchema` is validated.
351
548
  `CostBudget` two-spellings pattern, normalized once).
352
549
 
353
550
  **THE CEILING**, exported as `COLUMN_TYPE_CEILING` and quoted verbatim into
354
- every finding, the `EMPTY_LOOKUP_CEILING` law: *"This judges TYPE, never
551
+ every finding, the `EMPTY_LOOKUP_CEILING` law: _"This judges TYPE, never
355
552
  MEANING — it can see that a column declared `number` holds a string, and it
356
553
  can never see that the string should have been 0, or that a 0.0 should have
357
554
  been an 8; a column whose every value has its declared type passes here and
358
- can still be wrong."* Failures 1 and 3 are caught. **Failure 2 is not, and
555
+ can still be wrong."_ Failures 1 and 3 are caught. **Failure 2 is not, and
359
556
  never will be** — `0.0` is a perfectly good number — and the check says so
360
557
  out loud rather than letting a green row imply otherwise.
361
558
 
362
559
  **TWO finding kinds, because the field bug turned on the difference.** New
363
560
  `ContextErrorKind`s at the **write seam**: **`column-type-mismatch`** (the
364
561
  column is THERE and holds the wrong thing) and **`missing-column`** (the
365
- declared column is in NONE of the rows). *"The value is not what it should
366
- be"* sends a person to the mapping code; *"the column was never delivered"*
562
+ declared column is in NONE of the rows). _"The value is not what it should
563
+ be"_ sends a person to the mapping code; _"the column was never delivered"_
367
564
  sends them to the query. A checker that said only "something is off with
368
565
  logical_unit_number" would have helped with neither. Each finding names the
369
566
  column, the offending value quoted, the rows affected of the rows read, and
@@ -433,7 +630,7 @@ first time a tool with an `outputSchema` is validated.
433
630
  it came back empty.** A triage agent's reverse-lookup tool filtered a column
434
631
  before a pivot, so the column did not exist yet and EVERY reverse lookup
435
632
  returned an empty result — for every identifier, always. The tool then
436
- answered *successfully* with an empty list, and the agent reported in a
633
+ answered _successfully_ with an empty list, and the agent reported in a
437
634
  table, with confidence, that the device was not logged in to any port on any
438
635
  collected switch, advising a check of the physical cabling. It was logged in
439
636
  the whole time. Every rail passed, and passed honestly: nothing errored,
@@ -457,9 +654,9 @@ first time a tool with an `outputSchema` is validated.
457
654
  for the broken filter and for the honest absence. The bound ships as one
458
655
  exported string, `EMPTY_LOOKUP_CEILING`, quoted verbatim into every message
459
656
  so it cannot drift out of one doc and leave a reader thinking the library
460
- knows more than it does: *"An empty result can be perfectly true — the thing
657
+ knows more than it does: _"An empty result can be perfectly true — the thing
461
658
  may exist and simply have nothing to show right now — so this is a place to
462
- look, never a verdict that anything is wrong."*
659
+ look, never a verdict that anything is wrong."_
463
660
 
464
661
  Deliberately NOT `dangling-reference`, whose meaning is the opposite: there
465
662
  the ground has left reach; here the ground IS in reach and the lookup found
@@ -475,7 +672,7 @@ first time a tool with an `outputSchema` is validated.
475
672
  make impossible.
476
673
 
477
674
  **Armed by two halves**, and the second one is why: `noticeEmptyLookups:
478
- true` on `Agent.create` **and** at least one tool declaring `argumentsFrom`.
675
+ true` on `Agent.create` **and** at least one tool declaring `argumentsFrom`.
479
676
  The declaration alone is not enough — it already arms two other checks, and
480
677
  an advisory that armed itself off a declaration made for something else
481
678
  would not be opt-in at all. **Default off is byte-identical**: no finding, no
@@ -546,7 +743,7 @@ first time a tool with an `outputSchema` is validated.
546
743
  pre-rendered table, and the recorded walk that lets a reader CHECK the
547
744
  verdict instead of trusting it. `runbookAsTool` is that envelope as one
548
745
  declaration bag; the smallest legal call is `{ name, description,
549
- procedure }` and it still yields the honest spine.
746
+ procedure }` and it still yields the honest spine.
550
747
 
551
748
  **The mandatory honesty spine**, on every answer whatever the runbook's
552
749
  shape: `af_coverage` (three lists + a sentence naming the rule set and
@@ -582,7 +779,7 @@ first time a tool with an `outputSchema` is validated.
582
779
  tools are invisible — there is no build-time list, the stated 9.72.0
583
780
  caveat), so stages compose registered sources instead of importing modules
584
781
  and building a second query stack. Inner calls run with `hasArtifacts:
585
- false` (one answer, one ticket — never competing chips), a derived
782
+ false` (one answer, one ticket — never competing chips), a derived
586
783
  toolCallId naming the outer call, `needs` resolved on the fail-closed
587
784
  non-interactive path, and `checkIn`/`wants` tools refused BY NAME (an inner
588
785
  call cannot pause, and must never silently skip a consent gate).
@@ -606,8 +803,8 @@ first time a tool with an `outputSchema` is validated.
606
803
  run: four tool results carried real numbers, a compute tool that could sum
607
804
  them was registered — with `wants` declared over the staged dataset kind —
608
805
  and the app's prompt said to use it. The model summed the numbers in its
609
- head anyway and stated the total; the evidence gate recorded *"appears in no
610
- tool result"* and the answer shipped, because the posture only observed. The
806
+ head anyway and stated the total; the evidence gate recorded _"appears in no
807
+ tool result"_ and the answer shipped, because the posture only observed. The
611
808
  app patched it with more prose. The library-shaped fix is two mechanisms it
612
809
  already owns, on the one dial it already has:
613
810
 
@@ -641,9 +838,9 @@ first time a tool with an `outputSchema` is validated.
641
838
  `'guard'` allows the one bounded revision then delivers with both attempts
642
839
  on the record, `'rails'` refuses with `UnsupportedValuesError`. What the
643
840
  revision gains: when the flagged turn holds staged refs a served `wants`
644
- tool can spend, the correction now names them — *"pass 'art_…'
841
+ tool can spend, the correction now names them — _"pass 'art\_…'
645
842
  (dataset/rows) to `compute` — compute the number there and answer with what
646
- it returns"* — inside the authored frame, so the quoted values still come
843
+ it returns"_ — inside the authored frame, so the quoted values still come
647
844
  last and the exempt-corpus fence is untouched. The `revision-asked`
648
845
  `evidence_checked` event carries the same facts additively (`stagedRefs`,
649
846
  `spenderTools`). Absent everything — no gate, `nudge` unset, or no
@@ -671,7 +868,7 @@ first time a tool with an `outputSchema` is validated.
671
868
 
672
869
  **`foundry()` — the project-endpoint provider** (`agentfootprint/providers`).
673
870
  The JS answer to Microsoft's `FoundryChatClient(project_endpoint, model,
674
- credential)`: point it at a Foundry project endpoint (or let the hosted
871
+ credential)`: point it at a Foundry project endpoint (or let the hosted
675
872
  platform's auto-injected `FOUNDRY_PROJECT_ENDPOINT` supply it), name the
676
873
  deployment (`AZURE_AI_MODEL_DEPLOYMENT_NAME ?? MODEL_NAME`), and auth is an
677
874
  Entra `TokenCredential`, an api key, or — given neither — the platform's own
@@ -797,7 +994,6 @@ first time a tool with an `outputSchema` is validated.
797
994
  tests fail against the previous source (proven by restoring it); no public
798
995
  API change; `OllamaUnavailableError` is byte-identical.
799
996
 
800
-
801
997
  ## [9.73.0] - 2026-08-27
802
998
 
803
999
  ### Fixed
@@ -834,13 +1030,15 @@ first time a tool with an `outputSchema` is validated.
834
1030
 
835
1031
  ```ts
836
1032
  const agent = Agent.create({
837
- provider, model,
1033
+ provider,
1034
+ model,
838
1035
  // DECLARED, never ambient — this option is the only door.
839
- externalGrounds: () => viewerSelection.cells.map((cell) => ({
840
- value: cell.text, // verified by the app against the artifact
841
- source: 'viewer-selection', // the audit label that travels
842
- })),
843
- })
1036
+ externalGrounds: () =>
1037
+ viewerSelection.cells.map((cell) => ({
1038
+ value: cell.text, // verified by the app against the artifact
1039
+ source: 'viewer-selection', // the audit label that travels
1040
+ })),
1041
+ });
844
1042
  ```
845
1043
 
846
1044
  The provider is consulted once per LLM response that contains an armed call,
@@ -917,25 +1115,25 @@ first time a tool with an `outputSchema` is validated.
917
1115
  namespaced key (`MCP_TOOL_EXTRAS_KEY`, the string `agentfootprint`), and
918
1116
  `mcpClient` / `mockMcpClient` read them back onto the registered `Tool`:
919
1117
 
920
- | field | what it arms on the consuming side |
921
- |---|---|
922
- | `argumentsFrom` | the dangling-reference and unsupported-argument checks |
923
- | `resultKind` | placement's mint — a placed result a `wants` argument can spend |
924
- | `owner` | the identity edge subject-joined checks read |
925
- | `resultClass` | the per-class `check:semantics` rules |
926
- | `resultCeiling` | the author's refusing ceiling on an oversized result |
1118
+ | field | what it arms on the consuming side |
1119
+ | --------------- | --------------------------------------------------------------- |
1120
+ | `argumentsFrom` | the dangling-reference and unsupported-argument checks |
1121
+ | `resultKind` | placement's mint — a placed result a `wants` argument can spend |
1122
+ | `owner` | the identity edge subject-joined checks read |
1123
+ | `resultClass` | the per-class `check:semantics` rules |
1124
+ | `resultCeiling` | the author's refusing ceiling on an oversized result |
927
1125
 
928
1126
  ```ts
929
1127
  const fleet = await mcpClient({ name: 'fleet-mcp', transport });
930
1128
  const agent = Agent.create({ provider, model })
931
- .tools(await fleet.tools()) // backup_status declares argumentsFrom: ['fleet_report']
1129
+ .tools(await fleet.tools()) // backup_status declares argumentsFrom: ['fleet_report']
932
1130
  .build();
933
1131
  // …and the choice seam now files `unsupported-argument` for it, exactly as
934
1132
  // it would for a local defineTool — pinned end to end, disposition row included.
935
1133
  ```
936
1134
 
937
- **The inclusion bar, stated where the list lives:** *a declaration a
938
- consumer-side check or rail reads; nothing that governs execution.* `needs`
1135
+ **The inclusion bar, stated where the list lives:** _a declaration a
1136
+ consumer-side check or rail reads; nothing that governs execution._ `needs`
939
1137
  (credentials), `checkIn` (human consent) and the session hooks are excluded
940
1138
  and always will be — they decide how a tool RUNS, and the tool runs on the
941
1139
  server. A client holding a consent gate the only executor already held is
@@ -997,7 +1195,7 @@ first time a tool with an `outputSchema` is validated.
997
1195
  });
998
1196
 
999
1197
  // elsewhere — resolves now, and would have been a kind mismatch before
1000
- defineTool({ name: 'chart', wants: { dataset: 'dataset/rows' }, /* … */ });
1198
+ defineTool({ name: 'chart', wants: { dataset: 'dataset/rows' } /* … */ });
1001
1199
  ```
1002
1200
 
1003
1201
  - **The matcher is untouched.** Nothing here loosens `wants`; exact match is
@@ -1066,9 +1264,9 @@ first time a tool with an `outputSchema` is validated.
1066
1264
  that earns the port its keep —
1067
1265
 
1068
1266
  ```ts
1069
- await session.handControlTo('person'); // the automation stream stops
1267
+ await session.handControlTo('person'); // the automation stream stops
1070
1268
  // …they sign in, clear the CAPTCHA, approve the consent screen, watching live
1071
- await session.handControlTo('agent'); // and the agent carries on
1269
+ await session.handControlTo('agent'); // and the agent carries on
1072
1270
  ```
1073
1271
 
1074
1272
  Pair it with a check-in and the agent **pauses** rather than guesses: the
@@ -1086,7 +1284,7 @@ first time a tool with an `outputSchema` is validated.
1086
1284
  **What verification changed.** The `InvokeBrowser` action union, read off a
1087
1285
  real install of `@aws-sdk/client-bedrock-agentcore` 3.1118.0, is exactly
1088
1286
  `mouseClick | mouseMove | mouseDrag | mouseScroll | keyType | keyPress |
1089
- keyShortcut | screenshot` — with **no navigate member at all**. An adapter
1287
+ keyShortcut | screenshot` — with **no navigate member at all**. An adapter
1090
1288
  written from memory would have invented page verbs for a door that has none.
1091
1289
  The same pass fixed `MouseClickArguments` (`{ x, y, button?, clickCount? }`,
1092
1290
  buttons `LEFT|MIDDLE|RIGHT`), `KeyPressArguments` (`{ key, presses? }`) and
@@ -1122,6 +1320,7 @@ first time a tool with an `outputSchema` is validated.
1122
1320
  another agent (Strands, LangGraph, Google ADK, a Marketplace listing)
1123
1321
  discovers yours through its agent card and calls it. The same split as 9.65.0,
1124
1322
  for the third time:
1323
+
1125
1324
  - **`a2aWire()`** is the A2A PROTOCOL — JSON-RPC 2.0, `message/send`, text
1126
1325
  parts, artifacts, `A2A_PROTOCOL_VERSION` `0.3.0` — an open protocol with no
1127
1326
  vendor in it, exported in its own right along with
@@ -1161,8 +1360,8 @@ first time a tool with an `outputSchema` is validated.
1161
1360
  - **A capability this library claimed and could not honour.** `httpHost`
1162
1361
  declares `['streaming']` by default, so the A2A host inherited it while
1163
1362
  `message/send` has nowhere to put a chunk: `requireCapability(host,
1164
- 'streaming')` would have passed for a host that then delivered none. The
1165
- conformance suite caught it — it asserts chunks *if and only if* the
1363
+ 'streaming')` would have passed for a host that then delivered none. The
1364
+ conformance suite caught it — it asserts chunks _if and only if_ the
1166
1365
  capability is declared — and the adapter now declares `[]`.
1167
1366
 
1168
1367
  ### Changed
@@ -1180,6 +1379,7 @@ first time a tool with an `outputSchema` is validated.
1180
1379
  looking anything up** (`agentfootprint/providers`). `gatewayTransport` says of
1181
1380
  itself that nothing in it is vendor-specific, and that stays true because the
1182
1381
  four facts which ARE AgentCore's now live in one file beside it:
1382
+
1183
1383
  - **the endpoint** — `agentCoreGatewayUrl({ gatewayId, region })` builds
1184
1384
  `https://{gatewayId}.gateway.bedrock-agentcore.{region}.amazonaws.com/mcp`,
1185
1385
  a hostname nobody recalls correctly, which is why it is a function and not a
@@ -1245,6 +1445,7 @@ first time a tool with an `outputSchema` is validated.
1245
1445
  install of `@aws-sdk/client-bedrock-agentcore` **3.1118.0** — names, request
1246
1446
  shapes and enum values read off the package rather than remembered, which is
1247
1447
  the 9.4.0 law:
1448
+
1248
1449
  - **`userFlow: 'consent' | 'exchange'`** — `'exchange'` sends
1249
1450
  `ON_BEHALF_OF_TOKEN_EXCHANGE`, trading the person's existing login for a
1250
1451
  scoped downstream token with no consent screen at any point. Default stays
@@ -1401,7 +1602,7 @@ first time a tool with an `outputSchema` is validated.
1401
1602
 
1402
1603
  The field story: a tool result ended with an offer — "I can also map these
1403
1604
  ids to volume names" — and the person answered "yes please". The model bound
1404
- *that sentence* as the identifier argument and dispatched. The tool's schema
1605
+ _that sentence_ as the identifier argument and dispatched. The tool's schema
1405
1606
  DECLARED the identifier's shape, in a `pattern` that "yes please" could never
1406
1607
  match; the pre-dispatch validator simply did not read the keyword. So the
1407
1608
  call went out, failed downstream, and cost a round trip — and the consumer
@@ -1546,7 +1747,7 @@ first time a tool with an `outputSchema` is validated.
1546
1747
  check, so one busy check could carry the total while another check looked
1547
1748
  at nothing at all — the per-check rows said so underneath (`⚠ ran 0×`), but
1548
1749
  a reader who stops at the headline got the rosier story. The headline now
1549
- states coverage: either *all* registered checks ran, or it says COVERAGE IS
1750
+ states coverage: either _all_ registered checks ran, or it says COVERAGE IS
1550
1751
  PARTIAL and names the checks that checked nothing. Same law as the rows —
1551
1752
  a check that never saw a subject is silent about its seam, never a pass.
1552
1753
 
@@ -1576,38 +1777,40 @@ first time a tool with an `outputSchema` is validated.
1576
1777
  - **Six defects in the Context Integrity family, caught by an adversarial
1577
1778
  review before release** — each survived two independent attempts to refute
1578
1779
  it, and each is now pinned by a red-proved regression test.
1579
- - *A claim finding's identity ignored the FIELD*, so a contract naming two
1780
+
1781
+ - _A claim finding's identity ignored the FIELD_, so a contract naming two
1580
1782
  fields of one entity (the shape `.claims()` itself tells you to write)
1581
1783
  filed one event and swallowed the rest — while the disposition ledger
1582
1784
  counted them all, leaving the two accounts of one run disagreeing.
1583
1785
  `ContextError.predicate` now rides the identity, mirroring the
1584
1786
  substrate's own `assertionKey`; findings that never set it keep exactly
1585
1787
  the identity they had.
1586
- - *The dangling-reference check was DEAD under `reactMode:
1587
- 'dynamic-grouped'`* — `compactions` was threaded into the wrong mapper,
1788
+ - _The dangling-reference check was DEAD under `reactMode:
1789
+ 'dynamic-grouped'`_ — `compactions` was threaded into the wrong mapper,
1588
1790
  so the check saw an empty window ledger every pass and filed a healthy
1589
1791
  verdict. Chart-shape parity is now pinned by tests that run the same
1590
1792
  trap under both dynamic shapes.
1591
- - *The dev canary structurally disabled the wiring-rot theorem.* A minted
1793
+ - _The dev canary structurally disabled the wiring-rot theorem._ A minted
1592
1794
  canary proves the pure function still works; it says nothing about
1593
1795
  whether the pipeline ever calls it (`beginIntegrityRun` mints by calling
1594
1796
  the function directly). Masking theorem (i) with it meant the alarm this
1595
1797
  ledger exists for could never fire.
1596
- - *`workExisted` was hardcoded `true`* on every exit path, so a run that
1798
+ - _`workExisted` was hardcoded `true`_ on every exit path, so a run that
1597
1799
  died or paused before its first LLM call reported every registered
1598
1800
  checker as dead. It is now measured from a signal the integrity code
1599
1801
  does not itself write.
1600
- - *An answer agreeing with a settled non-reading was filed as an advisory*
1802
+ - _An answer agreeing with a settled non-reading was filed as an advisory_
1601
1803
  — `null` reported for a fact whose settled value is `null` is agreement,
1602
1804
  not doubt.
1603
- - *The claim ledger accumulated for agents that never declared a
1604
- contract*, and appended by whole-array spread. It is now gated on
1805
+ - _The claim ledger accumulated for agents that never declared a
1806
+ contract_, and appended by whole-array spread. It is now gated on
1605
1807
  `.claims()` and appends without the quadratic copy — restoring the
1606
1808
  zero-delta promise for every agent that configured none of this.
1607
1809
 
1608
1810
  - **Two honesty defects in `find_context_errors`**, both caught by the same
1609
1811
  adversarial review and both red-proved by a failing test first.
1610
- - *The green headline never read the counts it claimed to summarise.*
1812
+
1813
+ - _The green headline never read the counts it claimed to summarise._
1611
1814
  "The checkers below RAN; nothing they cover was violated" was printed on
1612
1815
  the mere absence of a finding, so a run whose every encounter was
1613
1816
  `unreachable` (the check could not see the evidence), whose checks all
@@ -1620,7 +1823,7 @@ first time a tool with an `outputSchema` is validated.
1620
1823
  all, each keep their own sentence. A checker ROW with zero `checked`
1621
1824
  stopped saying "the checker ran and found nothing at this seam" for the
1622
1825
  same reason.
1623
- - *The tool advertised defect classes no check can file.* The `kind` enum
1826
+ - _The tool advertised defect classes no check can file._ The `kind` enum
1624
1827
  was pinned to the whole `ContextErrorKind` union, so it offered
1625
1828
  `unsupported-argument` and `duplicate-execution` — classes no check in
1626
1829
  this build files — and answering one returned a negative verdict about
@@ -1685,7 +1888,7 @@ first time a tool with an `outputSchema` is validated.
1685
1888
  run's disposition rows ride along, so "the checkers ran and found nothing"
1686
1889
  and "no checker was registered for that seam" stay different answers, and
1687
1890
  a registered check with zero encounters is named as wiring rot. Honest
1688
- absence has its own sentences: no event tail is *no finding evidence*, a
1891
+ absence has its own sentences: no event tail is _no finding evidence_, a
1689
1892
  tail with no integrity events says the channel is empty and why, and rows
1690
1893
  reporting findings the tail no longer carries say **evidence missing** —
1691
1894
  none of them ever reads as "no context errors found". Synthetic canaries
@@ -1721,7 +1924,7 @@ first time a tool with an `outputSchema` is validated.
1721
1924
  - **`Tool.argumentsFrom` + the dangling-reference check** (the closure
1722
1925
  check's decidable fragment). A tool author can now declare where a tool's
1723
1926
  arguments come from — `defineTool({ name: 'screen_fire', argumentsFrom:
1724
- ['whats_here'] })` — and `callLLM` checks at request assembly that every
1927
+ ['whats_here'] })` — and `callLLM` checks at request assembly that every
1725
1928
  served tool's declared grounds still have results in the window: a ground
1726
1929
  the window ledger says was evicted (`droppedObservations`) with nothing
1727
1930
  re-established files a `dangling-reference` finding at seam `'compose'`,
@@ -1765,10 +1968,10 @@ first time a tool with an `outputSchema` is validated.
1765
1968
 
1766
1969
  - **The assertion algebra and the one visible finding type.** An
1767
1970
  `Assertion` is keyed by `(subject, predicate, epoch)` with two rules
1768
- that do the work: *serving is asserting; history is quotation* (checks
1971
+ that do the work: _serving is asserting; history is quotation_ (checks
1769
1972
  never fire across the quoted stratum — the whole stale-but-honest
1770
- false-positive class, closed structurally) and *single-valued by
1771
- default* (you declare exemptions, never rules). Unknown `Claim`s never
1973
+ false-positive class, closed structurally) and _single-valued by
1974
+ default_ (you declare exemptions, never rules). Unknown `Claim`s never
1772
1975
  participate in a comparison. `conflictsOf()` is the pure exclusion
1773
1976
  comparison; `ContextError` is the uniform finding — plain kinds
1774
1977
  (`invariant-violation`, `unsupported-argument`, `dangling-reference`,
@@ -1822,16 +2025,16 @@ when this patch was cut, and they are additive.
1822
2025
  ### Fixed
1823
2026
 
1824
2027
  - **The cache report no longer invents a cause for a turn it could not
1825
- measure.** *What changed:* when `cacheRecorder().report()` hands back an
2028
+ measure.** _What changed:_ when `cacheRecorder().report()` hands back an
1826
2029
  unknown hit rate, the sentence attached to it is now the one the calls
1827
2030
  themselves gave — most often "no CacheStrategy was given to
1828
2031
  `cacheRecorder()`, so nothing read the usage". When the calls disagree about
1829
2032
  why, the summary says they disagreed and lists the reasons (up to three, then
1830
- a count of the rest) rather than silently choosing one. *Why it was not
1831
- there:* the summary sentence was typed into the code as a fixed string, "the
2033
+ a count of the rest) rather than silently choosing one. _Why it was not
2034
+ there:_ the summary sentence was typed into the code as a fixed string, "the
1832
2035
  provider reported no cache fields" — true for the case the author had in
1833
2036
  mind, a guess for every other, and it overwrote what each call had already
1834
- stated. *How it improves:* run without a strategy against a provider that DID
2037
+ stated. _How it improves:_ run without a strategy against a provider that DID
1835
2038
  report cache traffic, and 9.59.0 told you your provider was reporting
1836
2039
  nothing — sending you off to debug a provider that was working fine, instead
1837
2040
  of naming the one line missing from your own setup. You now read the real
@@ -1854,113 +2057,113 @@ the published bytes and is unchanged.
1854
2057
 
1855
2058
  ### Fixed
1856
2059
 
1857
- - **A red gate could reach npm, and did.** *What changed:* the docs-truth
2060
+ - **A red gate could reach npm, and did.** _What changed:_ the docs-truth
1858
2061
  ratchet now runs inside the build job that the publishing job depends on,
1859
2062
  so a red gate fails the build and the publish never starts — however the
1860
- release was created. *Why it was not there:* the gate existed, but CI ran it
2063
+ release was created. _Why it was not there:_ the gate existed, but CI ran it
1861
2064
  on `push` while the publish workflow runs on `release: published`, and the
1862
2065
  release script fires the release seconds after the push. The two raced, and
1863
- npm never waited for a verdict. *How it improves:* 9.58.0 shipped with a red
2066
+ npm never waited for a verdict. _How it improves:_ 9.58.0 shipped with a red
1864
2067
  ratchet; that is now structurally impossible.
1865
- - **A generated report was being edited by hand.** *What changed:*
2068
+ - **A generated report was being edited by hand.** _What changed:_
1866
2069
  `npm run docs:truth:report` regenerates the report **without** touching the
1867
2070
  accepted-debt baseline, and a test fails if the committed report is not what
1868
- the generator produces. *Why it was not there:* the report could only be
2071
+ the generator produces. _Why it was not there:_ the report could only be
1869
2072
  regenerated by also re-recording the debt, so an author who merely wanted the
1870
2073
  numbers to match reality had to choose between accepting unrelated debt and
1871
2074
  editing the file. They edited the file: the 9.58.0 release commit changed
1872
2075
  "103 typed events" to "105" and nothing else, leaving the export count stale
1873
- and the ratchet red. *How it improves:* restating the truth and accepting
2076
+ and the ratchet red. _How it improves:_ restating the truth and accepting
1874
2077
  debt are now separate acts, and the file cannot silently drift.
1875
2078
  - **The cache meter reported 0% for turns that hit cache on every call.**
1876
- *What changed:* the strategies read the framework's normalised usage instead
2079
+ _What changed:_ the strategies read the framework's normalised usage instead
1877
2080
  of raw provider field names, and every number in the report is now a `Claim`
1878
- — a value that says how it knows itself. *Why it was not there:* the
2081
+ — a value that says how it knows itself. _Why it was not there:_ the
1879
2082
  strategies parsed `cache_read_input_tokens` off a value that has never
1880
2083
  carried it, so every field read as missing and nothing was recorded; and the
1881
2084
  report typed its totals as plain numbers, so "nobody measured" and "measured,
1882
2085
  and it was zero" looked identical. The test fixtures were themselves
1883
2086
  provider-shaped, which is how it survived a release with a green suite.
1884
- *How it improves:* a real hit rate, an unmeasured turn that renders as
2087
+ _How it improves:_ a real hit rate, an unmeasured turn that renders as
1885
2088
  unmeasured, and a rate that always states its own denominator ("3 of 20
1886
2089
  calls"). A **silent non-cache** — a prompt below the model's minimum
1887
2090
  cacheable size, which providers process without caching and without an error
1888
2091
  — is now visibly different from a turn nobody measured.
1889
- - **A meter was attached to a provider that cannot feed it.** *What changed:*
2092
+ - **A meter was attached to a provider that cannot feed it.** _What changed:_
1890
2093
  the Bedrock strategy declares itself disabled, passes requests through
1891
- untouched, and answers "not applicable" with the reason. *Why it was not
1892
- there:* it claimed full support and clamped cache markers onto a request
2094
+ untouched, and answers "not applicable" with the reason. _Why it was not
2095
+ there:_ it claimed full support and clamped cache markers onto a request
1893
2096
  field our Bedrock provider discards, reporting markers that never reached a
1894
- wire. *How it improves:* it stays registered, so a Bedrock user is told the
2097
+ wire. _How it improves:_ it stays registered, so a Bedrock user is told the
1895
2098
  truth by name instead of guessing. The same honesty is applied to OpenAI,
1896
2099
  which is the costlier gap because it caches automatically.
1897
2100
 
1898
2101
  ### Changed
1899
2102
 
1900
- - **A parked map now tells the model it is parked.** *What changed:* while
2103
+ - **A parked map now tells the model it is parked.** _What changed:_ while
1901
2104
  anything is parked, the model receives a short status card naming the cursor
1902
2105
  and the engagement as **separate** things, the reason, and the way back as a
1903
- concrete call. *Why it was not there:* every honesty signal landed on the
2106
+ concrete call. _Why it was not there:_ every honesty signal landed on the
1904
2107
  record, which the model never reads — so re-engagement was reachable in
1905
2108
  principle and unreachable in practice, because nothing told the model that
1906
- re-picking a skill it appears to already be in means anything. *How it
1907
- improves:* a door the model can see. It also learns the distinction the
2109
+ re-picking a skill it appears to already be in means anything. _How it
2110
+ improves:_ a door the model can see. It also learns the distinction the
1908
2111
  kernel is built on instead of inferring a contradiction.
1909
- - **A parked map's tools actually leave the wire.** *What changed:* parking now
2112
+ - **A parked map's tools actually leave the wire.** _What changed:_ parking now
1910
2113
  holds the parked map's tool schemas off the request on its own authority.
1911
- *Why it was not there:* `.maps()` promised parking stops "the prompt fragment
2114
+ _Why it was not there:_ `.maps()` promised parking stops "the prompt fragment
1912
2115
  and tools", but on the default posture for flat graphs only the fragment
1913
2116
  stopped — the model was shown tools for a skill whose instructions had just
1914
- vanished. *How it improves:* the promise is true on every posture. This is
2117
+ vanished. _How it improves:_ the promise is true on every posture. This is
1915
2118
  **not** a change to `scopeTools` and does not touch the 10.0.0 ledger: those
1916
2119
  dials answer different questions.
1917
2120
  - **A pick of the skill you are already on is now a legal re-engagement.**
1918
- *What changed:* a `read_skill` pick is routed by intent — of a parked map's
1919
- member it re-engages the map and does not move the cursor. *Why it was not
1920
- there:* the reachability gate refuses a pick of the node the cursor occupies,
2121
+ _What changed:_ a `read_skill` pick is routed by intent — of a parked map's
2122
+ member it re-engages the map and does not move the cursor. _Why it was not
2123
+ there:_ the reachability gate refuses a pick of the node the cursor occupies,
1921
2124
  which is right for a move and wrong for this; since parking never moves the
1922
2125
  cursor, a parked map is parked exactly where the model wants to return. For a
1923
- single-member map that made parking permanent. *How it improves:* the
2126
+ single-member map that made parking permanent. _How it improves:_ the
1924
2127
  documented recovery door is real.
1925
- - **An engagement's founding cause is never rewritten.** *What changed:* the
2128
+ - **An engagement's founding cause is never rewritten.** _What changed:_ the
1926
2129
  record now keeps three separate facts — why the map is participating at all,
1927
2130
  why the cursor is on this member, and why this contribution is being served
1928
- right now. *Why it was not there:* one field answered all three, so
2131
+ right now. _Why it was not there:_ one field answered all three, so
1929
2132
  confirming a guess silently overwrote the guess, and a record founded on a
1930
- keyword at iteration 1 later read as system-backed since iteration 1. *How it
1931
- improves:* an incident review can still ask "was this founded on a guess?"
2133
+ keyword at iteration 1 later read as system-backed since iteration 1. _How it
2134
+ improves:_ an incident review can still ask "was this founded on a guess?"
1932
2135
  and get the true answer, however much has happened since.
1933
- - **A declared route no longer forges a user request.** *What changed:* when a
2136
+ - **A declared route no longer forges a user request.** _What changed:_ when a
1934
2137
  declared edge moves the cursor to a different member, that member's
1935
2138
  eligibility is worked out from **its own** evidence rather than inherited.
1936
- *Why it was not there:* the next member inherited the previous one's
2139
+ _Why it was not there:_ the next member inherited the previous one's
1937
2140
  standing, and an explicit request never decays — so one explicit pick at the
1938
2141
  top of a turn silently warranted every skill the graph walked to afterwards.
1939
- *How it improves:* a member entered weakly can park, exactly as if it had
2142
+ _How it improves:_ a member entered weakly can park, exactly as if it had
1940
2143
  been entered that way from the start. The founding cause still says, forever,
1941
2144
  that the engagement began explicitly on the skill that was actually asked for.
1942
- - **A new turn is judged on the new turn's evidence.** *What changed:* a cursor
2145
+ - **A new turn is judged on the new turn's evidence.** _What changed:_ a cursor
1943
2146
  carried into a new turn with nothing explaining it is recorded as `assumed`
1944
2147
  — nobody said why — which is the weakest rung and expires like any guess.
1945
- *Why it was not there:* an absent explanation was recorded as system-backed,
2148
+ _Why it was not there:_ an absent explanation was recorded as system-backed,
1946
2149
  the strongest and non-decaying category, so turn one's mistaken keyword match
1947
- became a permanent warrant on turn two and every turn after. *How it
1948
- improves:* cursor continuity and engagement continuity have independent
2150
+ became a permanent warrant on turn two and every turn after. _How it
2151
+ improves:_ cursor continuity and engagement continuity have independent
1949
2152
  lifetimes, and both are now pinned by tests — half the defect was that
1950
2153
  nothing pinned either.
1951
- - **The idle test checks all three of the conditions it documents.** *What
1952
- changed:* a map only accrues idle when its contribution was actually served,
1953
- none of its tools was called, and the turn went elsewhere. *Why it was not
1954
- there:* only the last was checked, while the refusal text asserted all three.
1955
- *How it improves:* nothing claims a fact it did not check. (Making it honest
2154
+ - **The idle test checks all three of the conditions it documents.** _What
2155
+ changed:_ a map only accrues idle when its contribution was actually served,
2156
+ none of its tools was called, and the turn went elsewhere. _Why it was not
2157
+ there:_ only the last was checked, while the refusal text asserted all three.
2158
+ _How it improves:_ nothing claims a fact it did not check. (Making it honest
1956
2159
  needed the kernel to carry its own record of what was served — reading it
1957
2160
  from the existing delta machinery was silently empty in the grouped chart
1958
2161
  shape, which would have disabled parking there entirely.)
1959
- - **A map that cannot explain its cursor moves is refused at mount.** *What
1960
- changed:* `.maps()` requires a map that reports why the cursor moved. *Why it
1961
- was not there:* without it, no explanation ever arrives, and a kernel whose
1962
- whole job is weighing evidence had none to weigh — silently. *How it
1963
- improves:* the failure is named at build time instead of at 3am.
2162
+ - **A map that cannot explain its cursor moves is refused at mount.** _What
2163
+ changed:_ `.maps()` requires a map that reports why the cursor moved. _Why it
2164
+ was not there:_ without it, no explanation ever arrives, and a kernel whose
2165
+ whole job is weighing evidence had none to weigh — silently. _How it
2166
+ improves:_ the failure is named at build time instead of at 3am.
1964
2167
 
1965
2168
  ### Added
1966
2169
 
@@ -1970,9 +2173,9 @@ the published bytes and is unchanged.
1970
2173
  shipped and was unreachable: the kernel's data model carried the field and
1971
2174
  no option ever set it. It suppresses the park, not the measurement — the
1972
2175
  record still shows a map riding every call unused.
1973
- - **Two documentation pages**: *Mounted maps* (the kernel's why, the evidence
1974
- ladder, the idle test, the three facts, turns, and the park card) and *The
1975
- cache meter* (how to read a report, and exactly which providers can feed it).
2176
+ - **Two documentation pages**: _Mounted maps_ (the kernel's why, the evidence
2177
+ ladder, the idle test, the three facts, turns, and the park card) and _The
2178
+ cache meter_ (how to read a report, and exactly which providers can feed it).
1976
2179
  - **A measurement, not a fix, for the prompt-cache cost of `read_skill`.** The
1977
2180
  tool's description is rebuilt from the cursor every iteration, and changing a
1978
2181
  tool definition invalidates a provider's entire prompt cache. A shipped test
@@ -1990,7 +2193,6 @@ through `isKnown(...)`, or render them with `describeClaim(...)`. This is a
1990
2193
  signature change in a minor release, and deliberately so: the old bare numbers
1991
2194
  were not a contract worth keeping, because the value was zero on every turn.
1992
2195
 
1993
-
1994
2196
  ## [9.58.0] - 2026-08-20
1995
2197
 
1996
2198
  **A map's cursor says where it stands. Nothing said whether it had earned
@@ -2086,7 +2288,7 @@ to do, and no longer had the evidence to do it.
2086
2288
  What it did next was invent. It took an entity name it remembered plus the
2087
2289
  shape of an id it had used earlier, assembled one that has never existed, and
2088
2290
  was refused — a wasted action out of a small budget. In one archived run the
2089
- final answer to the *person* named a host that appears in no tool result at
2291
+ final answer to the _person_ named a host that appears in no tool result at
2090
2292
  all.
2091
2293
 
2092
2294
  Nothing in the conversation said the evidence had gone. That is the release.
@@ -2109,7 +2311,7 @@ Nothing in the conversation said the evidence had gone. That is the release.
2109
2311
  already inside `keepRecentTurns` costs nothing at all. Nothing at or before
2110
2312
  the current request is pinnable, so a new user turn releases the whole
2111
2313
  previous loop. The floor is `1 request + keepLastToolResults pins +
2112
- keepRecentTurns turns`, whatever your tool count, iteration count or run
2314
+ keepRecentTurns turns`, whatever your tool count, iteration count or run
2113
2315
  length.
2114
2316
 
2115
2317
  And a pin that BLOCKS is worse than a pin that misses: when two consecutive
@@ -2120,9 +2322,9 @@ Nothing in the conversation said the evidence had gone. That is the release.
2120
2322
  `keepLastToolResults: false` (or `0`) reproduces 9.56.0 byte for byte.
2121
2323
 
2122
2324
  - **A drop now says whose results it took.** The authored notice gains one
2123
- sentence: *"Tool results are among them (whats_here, pan_view) — call the
2325
+ sentence: _"Tool results are among them (whats_here, pan_view) — call the
2124
2326
  tool again if you need its output; do not reconstruct ids or values from
2125
- memory."* The drop is now STATED rather than silent — whether that sentence
2327
+ memory."_ The drop is now STATED rather than silent — whether that sentence
2126
2328
  changes what a model does next is **not measured here**: the five archived
2127
2329
  runs have not been re-run with it on, so it ships as an honesty fix and not
2128
2330
  as a performance claim. Tool names are the only caller data that reaches it, and they
@@ -2148,13 +2350,13 @@ Nothing in the conversation said the evidence had gone. That is the release.
2148
2350
  > You are on action 25 of 30; 5 remain. Finish what you have rather than
2149
2351
  > start something new.
2150
2352
 
2151
- Measured, not decorative: given its remaining budget a model wrote *"I have 5
2152
- steps left, enough to finish this properly"* and landed the task, where
2353
+ Measured, not decorative: given its remaining budget a model wrote _"I have 5
2354
+ steps left, enough to finish this properly"_ and landed the task, where
2153
2355
  before it spiralled and produced no answer at all.
2154
2356
 
2155
2357
  The vocabulary is closed rather than a `(ctx) => string` because of
2156
2358
  **absence**. Given a function, an author writes `${ctx.maxIterations}` and
2157
- ships *"23 of undefined"*, or writes `?? 0` and ships a fabricated
2359
+ ships _"23 of undefined"_, or writes `?? 0` and ships a fabricated
2158
2360
  denominator that nothing — and no model — can tell from a real zero. With
2159
2361
  named slots the library owns absence and applies one rule: if any named fact
2160
2362
  is unavailable, the whole instruction is skipped, by name, as
@@ -2178,7 +2380,7 @@ Nothing in the conversation said the evidence had gone. That is the release.
2178
2380
  message that would become the head was already a user turn (the pinned
2179
2381
  request, or an older turn of a restored conversation) no notice was owed at
2180
2382
  all. Its 245–358 characters were nevertheless allowed to veto a legitimate
2181
- drop; and because the removable span is the longest *contiguous* run, the
2383
+ drop; and because the removable span is the longest _contiguous_ run, the
2182
2384
  same verdict came back at every boundary while the window grew without
2183
2385
  bound.
2184
2386
 
@@ -2383,11 +2585,11 @@ they do.
2383
2585
  `unknown`, so a surface that guessed at it would put words in a tool's
2384
2586
  mouth. One rule, and it is the whole rule:
2385
2587
 
2386
- | your payload | the line a person reads |
2387
- | --- | --- |
2388
- | `{ message: 'Hop 3 of 12' }` | `Hop 3 of 12` — your sentence, verbatim |
2389
- | `{ done: 3, total: 12 }` | `` `walk_graph` reported progress (3 so far)… `` |
2390
- | `'a bare string'` | the same generic line |
2588
+ | your payload | the line a person reads |
2589
+ | ---------------------------- | ------------------------------------------------ |
2590
+ | `{ message: 'Hop 3 of 12' }` | `Hop 3 of 12` — your sentence, verbatim |
2591
+ | `{ done: 3, total: 12 }` | `` `walk_graph` reported progress (3 so far)… `` |
2592
+ | `'a bare string'` | the same generic line |
2391
2593
 
2392
2594
  A top-level string field named **`message`** is shown verbatim, trimmed, and
2393
2595
  cut at **120 characters** with the cut stated (`… (+N more)`) — `message` is
@@ -2413,8 +2615,8 @@ they do.
2413
2615
  `tool_end` used to be able to clear a SIBLING call's status, leaving the
2414
2616
  bubble blank while a tool was still working.
2415
2617
 
2416
- - **Commentary narrates the middle**, so recordings replay it: *"The
2417
- `walk_graph` tool reported progress while it was still running."* The
2618
+ - **Commentary narrates the middle**, so recordings replay it: _"The
2619
+ `walk_graph` tool reported progress while it was still running."_ The
2418
2620
  teaching voice states the fact and never the payload — the same split the
2419
2621
  Lens teaching view keeps, in the same words.
2420
2622
 
@@ -2468,8 +2670,10 @@ tool's name on it.
2468
2670
  series: [{ t: '2026-08-19T10:00:00Z', entity: 'fc1/3', metric: 'avg_iops', value: 18450 }],
2469
2671
  grain: { interval: '30m', aggregation: 'avg', is_counter: false },
2470
2672
  provenance: { measured_at: '2026-08-19T10:20:00Z', source: 'InfluxDB SwitchPortStats' },
2471
- coverage: { checked: ['fabric A: all 48 ports'],
2472
- notChecked: [{ what: 'the peer fabric', why: 'collector scoped to one fabric' }] },
2673
+ coverage: {
2674
+ checked: ['fabric A: all 48 ports'],
2675
+ notChecked: [{ what: 'the peer fabric', why: 'collector scoped to one fabric' }],
2676
+ },
2473
2677
  render: { default: 'table', columns: ['entity', 'value'], sort: 'value desc' },
2474
2678
  });
2475
2679
  ```
@@ -2521,7 +2725,7 @@ tool's name on it.
2521
2725
  - Zero-cost when unused: recognition is strict (`af_semantics: true` AND the
2522
2726
  whole rule set), so every value any tool has ever returned keeps its bytes.
2523
2727
  Runnable example: `examples/features/66-semantic-envelope.ts`. Guide:
2524
- docs *Build → Semantic tool results*.
2728
+ docs _Build → Semantic tool results_.
2525
2729
 
2526
2730
  ## [9.52.0] - 2026-08-19
2527
2731
 
@@ -2601,7 +2805,7 @@ opaque function on a route edge.
2601
2805
 
2602
2806
  - **`guard:` on route edges — guards as data (the `when` predicate's declared
2603
2807
  twin).** `.route(a, b, { guard: { riskLevel: { in: ['high','critical'] },
2604
- score: { gte: 0.7 } } })` — conditions over the hop (`toolName`, `result`,
2808
+ score: { gte: 0.7 } } })` — conditions over the hop (`toolName`, `result`,
2605
2809
  `status`, `iteration`, `userMessage`, `currentSkillId`) and over the tool
2606
2810
  result's own top-level JSON fields, operators `eq/ne/gt/gte/lt/lte/in/notIn`
2607
2811
  (deliberately footprintjs's `WhereFilter` grammar, mirrored door-locally —
@@ -2611,6 +2815,7 @@ opaque function on a route edge.
2611
2815
  outcome, AND these conditions"). ONE compilation produces the predicate
2612
2816
  that routes, the serializable `SkillGuardData`, and the evidence evaluator
2613
2817
  — so the three can never describe different guards. What being data buys:
2818
+
2614
2819
  - **the check-up proves contradictions** — new ERROR
2615
2820
  `guard-unsatisfiable`: crossed bounds (`gt: 5, lt: 3`), `eq` a same-key
2616
2821
  `ne`/`in`/`notIn` excludes, a `status` outside the closed result-status
@@ -2631,7 +2836,7 @@ opaque function on a route edge.
2631
2836
  evaluation: `guard` on the taken hop (verdict `true`) and `guardsClosed`
2632
2837
  for refusals (verdict `false`, at most one per edge per iteration, on
2633
2838
  whatever move resulted — a stay says `score gte 0.7 — saw "0.2" →
2634
- failed`). Agents without guards keep byte-identical events.
2839
+ failed`). Agents without guards keep byte-identical events.
2635
2840
 
2636
2841
  - **SkillMap & SkillWalker are now the official names.** You declare the
2637
2842
  **SkillMap**; the agent is the **SkillWalker**; the recording carries both.
@@ -2714,7 +2919,7 @@ is the whole release.
2714
2919
  ### Notes for recording consumers (the lens, triage platforms)
2715
2920
 
2716
2921
  - Draw the declared topology from `skill.graph_declared` (`declaredSource:
2717
- 'recording'` can now mean COMPLETE); filter `from !== null` for
2922
+ 'recording'` can now mean COMPLETE); filter `from !== null` for
2718
2923
  node-to-node edges, exactly as with a built graph's `edges`.
2719
2924
  - Fill per-beat reachability from `cursorMove.reachable` first; the refusal's
2720
2925
  `allowed` and declared-edge folds remain as fallbacks for older eras.
@@ -2773,7 +2978,7 @@ is the whole release.
2773
2978
  `recording.json`, the manifest carries a note naming the fact and the line
2774
2979
  that supplies it, and `BugReportUnit.enveloped` says per conversation which
2775
2980
  shape it got. Nothing is stamped that was not known.
2776
- - **The evidence is never packed twice** — an envelope *or* a bare recording,
2981
+ - **The evidence is never packed twice** — an envelope _or_ a bare recording,
2777
2982
  never both. The zip is store-only, so a duplicated recording is duplicated
2778
2983
  bytes against the size ceiling the trim hints exist to keep a reporter under.
2779
2984
  - The GitHub issue body names the file that is really in the bundle, including
@@ -3324,10 +3529,9 @@ answer for. These are ours, built from that evidence. The credit is the field's.
3324
3529
  ### Fixed
3325
3530
 
3326
3531
  - Two unanchored `coverage` rules in `.gitignore` matched `src/core/agent/
3327
- coverage/`, so a new source directory would have been absent from every clone.
3532
+ coverage/`, so a new source directory would have been absent from every clone.
3328
3533
  Anchored to the vitest report directory, with a comment naming why.
3329
3534
 
3330
-
3331
3535
  ## [9.42.0] - 2026-08-15
3332
3536
 
3333
3537
  **Comparing strategies with statistics, and retention on the session port.**
@@ -3413,7 +3617,6 @@ answer for. These are ours, built from that evidence. The credit is the field's.
3413
3617
  from a counter, so they differ in their prefix and the fold never shows.
3414
3618
  Fixing it re-keys stored sessions. Next release.
3415
3619
 
3416
-
3417
3620
  ## [9.41.0] - 2026-08-15
3418
3621
 
3419
3622
  **Comparing strategies, and proving a store.** An audit asked whether this
@@ -3483,7 +3686,6 @@ key, and the proof obligations for the second port.
3483
3686
  - `MemoryDefinition` gains optional declared `strategy`, `retrieval` and
3484
3687
  `embedderId` — additive, and what makes a memory row worth grouping on.
3485
3688
 
3486
-
3487
3689
  ## [9.40.0] - 2026-08-15
3488
3690
 
3489
3691
  **Two identity bugs, one of which had been paying out zeros since v2.8.**
@@ -3534,7 +3736,6 @@ key, and the proof obligations for the second port.
3534
3736
  `provider` now flows through to `CostTick` as well (optional; absent stays
3535
3737
  absent rather than becoming `'unknown'`).
3536
3738
 
3537
-
3538
3739
  ## [9.39.0] - 2026-08-14
3539
3740
 
3540
3741
  **Three false doors closed, and two promises the recording did not keep.** An
@@ -3573,7 +3774,7 @@ forking. Mostly yes — but the first thing such an author reads was wrong.
3573
3774
  was reserved. Not re-exported from any barrel, not an `exports` subpath, not
3574
3775
  in the shipped-surface baseline — so nothing public could reach it and its
3575
3776
  removal is not breaking. The by-instance door (`agent.enable.observability({
3576
- strategy })`) is real, first-class, and now the only one. The cache twin
3777
+ strategy })`) is real, first-class, and now the only one. The cache twin
3577
3778
  legitimately has two doors; this one had one door and a sign pointing at a
3578
3779
  wall.
3579
3780
 
@@ -3597,7 +3798,6 @@ forking. Mostly yes — but the first thing such an author reads was wrong.
3597
3798
  `enable.cost()` receives zeros. This release makes `model` resolve there; the
3598
3799
  numbers are still zero. Its own ticket.
3599
3800
 
3600
-
3601
3801
  ## [9.38.0] - 2026-08-14
3602
3802
 
3603
3803
  **Two behaviour changes that are bug fixes — a call that used to run now
@@ -3965,12 +4165,13 @@ labels." Ours had no rule at all. Now it does, and it is enforced, not
3965
4165
  promised.
3966
4166
 
3967
4167
  ### What it is, and — because the name invites the wrong reading — what it
4168
+
3968
4169
  ### is provably not
3969
4170
 
3970
4171
  It is a **fabrication detector, not a correctness judge.** If a value in
3971
4172
  the answer never appeared in anything a tool returned this turn, the model
3972
4173
  typed it rather than read it, and that is all this checks. It cannot catch
3973
- a false claim built entirely from real values — *"fc1/3 is healthy"* when
4174
+ a false claim built entirely from real values — _"fc1/3 is healthy"_ when
3974
4175
  the data says the port is down uses two grounded tokens and sails through.
3975
4176
  It cannot catch a real value attached to the wrong thing, a fabricated
3976
4177
  quantity under the digit threshold, or a fabricated name spelled in
@@ -4025,7 +4226,7 @@ is not judged at all, and a turn the iteration or cost limit just cut
4025
4226
  short does not get to spend a revision it can't afford. It composes with,
4026
4227
  rather than collides with, `.reliability()`: reliability governs whether a
4027
4228
  call is retried before anything is committed; this governs an answer
4028
- *after* it has already been committed to the transcript.
4229
+ _after_ it has already been committed to the transcript.
4029
4230
 
4030
4231
  ### Measured, not assumed
4031
4232
 
@@ -4104,7 +4305,7 @@ never touched `footprintjs` itself but imports the file that now did.
4104
4305
  - `isDevMode` (a `footprintjs` import) → a bound `devWarn()` / `devMode()`
4105
4306
  reader that the host supplies; every existing warning reads verbatim, and
4106
4307
  the existing `enableDevMode()` tests pass unchanged — the proof that
4107
- nothing about *what gets warned* moved, only *how it's asked*.
4308
+ nothing about _what gets warned_ moved, only _how it's asked_.
4108
4309
  - `ToolResultStatus` → pulled out to a zero-import leaf and re-exported from
4109
4310
  its old home, so the envelope grammar is unchanged for every existing
4110
4311
  caller. The fence caught four more inline crossings of this type that
@@ -4267,7 +4468,7 @@ was in the bundle — an empty bundle read as "nobody was turned away" when
4267
4468
  it only meant "nobody ran." `standingAgent({ onIngressDecision })` now
4268
4469
  hands your sink one `IngressRecord` per request, filed at the terminal the
4269
4470
  reply actually reached. The honest contract is stated in the type itself:
4270
- `'served'` means **delivered**, not *admitted* — a request the door let
4471
+ `'served'` means **delivered**, not _admitted_ — a request the door let
4271
4472
  through whose run, store or provider then broke files as `'failed'`, and
4272
4473
  the record carries the admission verdict (`allow` / `queue` / `refuse`)
4273
4474
  either way. It is a stream you chain into your own sink, not a join onto
@@ -4399,7 +4600,7 @@ read/write." Both, plus `decayPolicy`, are now carried under prefixed
4399
4600
  metadata keys and restored verbatim on read. A caller's own value under one
4400
4601
  of the three keys this adapter generates (`source`, `resourceName`,
4401
4602
  `distance`) is refused by name rather than silently shadowed — recognized by
4402
- *identity*, not shape, so a caller's own `distance: 12` cannot be mistaken
4603
+ _identity_, not shape, so a caller's own `distance: 12` cannot be mistaken
4403
4604
  for this adapter's. An oversized carried field is refused rather than
4404
4605
  truncated: provenance that came back shortened would be provenance nobody
4405
4606
  could tell was shortened.
@@ -4589,7 +4790,7 @@ effects. Every line follows the same rules as the rest of the layer:
4589
4790
  retry happened — no event attests one, so the words don't either.
4590
4791
  - **Sizes humanized, and the two units told apart.** `humanizeBytes` /
4591
4792
  `humanizeChars` — `41.0 KB` where the ceiling counts bytes, `240,000
4592
- characters` where it counts characters (`tools.result_refused` counts
4793
+ characters` where it counts characters (`tools.result_refused` counts
4593
4794
  characters, because that's what the limit does).
4594
4795
  - **Refs and digests stay out of prose.** They identify a row for the details
4595
4796
  panel; a reader doesn't read them. The repeated-call nudge's fingerprints
@@ -4643,8 +4844,8 @@ import { googleIdentity } from 'agentfootprint/security';
4643
4844
  now declares the same `ranksBy: 'server-text'` for the identical reason, so
4644
4845
  the two server-ranked stores no longer disagree on how they say what they
4645
4846
  are.)
4646
- - **`googleIdentity`** — a narrow `CredentialProvider`: it vends *Google*
4647
- access tokens for *Google* APIs from whatever credential the environment
4847
+ - **`googleIdentity`** — a narrow `CredentialProvider`: it vends _Google_
4848
+ access tokens for _Google_ APIs from whatever credential the environment
4648
4849
  already has (ADC, workload identity, an impersonated service account).
4649
4850
  `mode: 'user'` is refused by name rather than quietly served a machine
4650
4851
  token, since Google's user-token equivalent has no Node surface yet.
@@ -4719,7 +4920,9 @@ provider scopes a vault on:
4719
4920
 
4720
4921
  ```ts
4721
4922
  await standingAgent({
4722
- agent, sessions, host: nodeHost({ port: 8080 }),
4923
+ agent,
4924
+ sessions,
4925
+ host: nodeHost({ port: 8080 }),
4723
4926
  identity: {
4724
4927
  verify: jwksIdentity({
4725
4928
  jwksUrl: 'https://idp.example.com/.well-known/jwks.json',
@@ -4732,7 +4935,7 @@ await standingAgent({
4732
4935
 
4733
4936
  **Configured is closed-by-default.** A request with no `Authorization`
4734
4937
  header is refused (401) unless `allowAnonymous: true` is set, and a request
4735
- that *names* a `userId` without proving it is refused either way — a door
4938
+ that _names_ a `userId` without proving it is refused either way — a door
4736
4939
  that verifies a token when offered and waves the request through when it is
4737
4940
  not is a door anybody opens by sending less. `jwksIdentity` is the one
4738
4941
  adapter this release ships (`jose`, loaded lazily, pinned against a real
@@ -4751,7 +4954,9 @@ Nothing else reads them; they do not enter the run's own identity tuple.
4751
4954
 
4752
4955
  ```ts
4753
4956
  await standingAgent({
4754
- agent, sessions, host,
4957
+ agent,
4958
+ sessions,
4959
+ host,
4755
4960
  identity: { verify },
4756
4961
  admission: turnsPerHour({ limit: 60 }),
4757
4962
  });
@@ -4936,8 +5141,12 @@ wire, scoped to the session that asks.**
4936
5141
  ### Added — two wire operations on the existing invoke path
4937
5142
 
4938
5143
  ```ts
4939
- { op: 'artifact-head', ref } // → meta
4940
- { op: 'artifact-get', ref } // → meta + data
5144
+ {
5145
+ op: 'artifact-head', ref;
5146
+ } // → meta
5147
+ {
5148
+ op: 'artifact-get', ref;
5149
+ } // → meta + data
4941
5150
  ```
4942
5151
 
4943
5152
  Resolved under the requesting session's identity-composed scope — exactly
@@ -4988,7 +5197,9 @@ lines.**
4988
5197
  defineTool({
4989
5198
  name: 'summarize',
4990
5199
  wants: { dataset: 'dataset/rows' },
4991
- execute: async (args, ctx) => { /* args.dataset is the RESOLVED DATA */ },
5200
+ execute: async (args, ctx) => {
5201
+ /* args.dataset is the RESOLVED DATA */
5202
+ },
4992
5203
  });
4993
5204
  ```
4994
5205
 
@@ -5176,7 +5387,7 @@ on `llm_start`.
5176
5387
  Precedence is stated and enforced, most specific wins: **escalation
5177
5388
  brain > per-skill brain > `.configure()`'s run model > the build
5178
5389
  default.** A brain naming only a model inherits the agent's own provider;
5179
- a brain naming a *foreign* provider without a model is refused at
5390
+ a brain naming a _foreign_ provider without a model is refused at
5180
5391
  `Agent.build()` — the run's configured model belongs to another vendor's
5181
5392
  namespace and would fail mid-turn, on exactly the iteration the cursor
5182
5393
  enters the skill.
@@ -5213,12 +5424,12 @@ returns today — stay byte-identical; the envelope is recognized only by
5213
5424
  its own strict shape.
5214
5425
 
5215
5426
  - **`propose-transition`** — `{ kind: 'propose-transition', targetSkillId,
5216
- reason }`. The typed replacement for a string routing marker: the
5217
- *graph* decides. A same-batch declared edge still wins; an unreachable
5427
+ reason }`. The typed replacement for a string routing marker: the
5428
+ _graph_ decides. A same-batch declared edge still wins; an unreachable
5218
5429
  target is refused out loud, not silently dropped.
5219
5430
  - **`require-instruction`** — `{ kind: 'require-instruction',
5220
- instructionId, deliveryLease: 'next-call' | 'until-skill-exit' }`.
5221
- Pushes a *registered* instruction into the coming call(s) —
5431
+ instructionId, deliveryLease: 'next-call' | 'until-skill-exit' }`.
5432
+ Pushes a _registered_ instruction into the coming call(s) —
5222
5433
  `read_skill` stays the pull door for optional knowledge; this is the
5223
5434
  push door for mandatory procedure, and it only pushes what was
5224
5435
  registered at build. An unknown id is refused, never improvised.
@@ -5402,7 +5613,7 @@ unmatched, never as an uncontested winner.
5402
5613
  ### Zero-cost when unused
5403
5614
 
5404
5615
  Graphs that use none of the new options are byte-identical in behavior
5405
- *and* events to 9.16.0 — pinned by regression tests. 78+ new tests cover
5616
+ _and_ events to 9.16.0 — pinned by regression tests. 78+ new tests cover
5406
5617
  the cascade, the scorers, the strictness postures, and continuity.
5407
5618
 
5408
5619
  ### Deferred
@@ -5437,6 +5648,7 @@ ordering bug with no error, no log line, just a silently different cursor.
5437
5648
 
5438
5649
  On the record, so a trace answers "why didn't the second call route?"
5439
5650
  instead of leaving the reader to guess.
5651
+
5440
5652
  - Single-tool iterations are byte-identical to 9.15.0 — this only changes
5441
5653
  behavior when a batch actually contains more than one result.
5442
5654
  - New `AgentState.toolResults` / `InjectionContext.toolResults`: the full
@@ -5451,7 +5663,7 @@ ordering bug with no error, no log line, just a silently different cursor.
5451
5663
  Wiring a `.skillGraph(...)` onto a classic agent meant the graph would still
5452
5664
  route and the trace would still show an activation — but the model never saw
5453
5665
  the newly-active skill's prompt or tools, because the slot it would have
5454
- changed was already frozen. The configuration *looked* like it worked and
5666
+ changed was already frozen. The configuration _looked_ like it worked and
5455
5667
  didn't.
5456
5668
 
5457
5669
  `Agent.build()` now refuses this combination outright, naming both the
@@ -5505,7 +5717,7 @@ const graph = skillGraph({
5505
5717
 
5506
5718
  What previously required `autoActivate: 'currentSkill'` typed on every single
5507
5719
  skill is now one graph-level line. A skill's own explicit `autoActivate` always
5508
- wins — the graph sets a default, never an override. Only *wired* skills (named
5720
+ wins — the graph sets a default, never an override. Only _wired_ skills (named
5509
5721
  by an entry or a route) are stamped: an unwired skill's tools would otherwise
5510
5722
  never appear at all. With the dial absent or `false`, compiled skills are
5511
5723
  byte-identical to 9.14.0 (pinned by test). On a `tree()` graph the flat-arm
@@ -5586,10 +5798,10 @@ const memory = defineMemory({
5586
5798
  type: MEMORY_TYPES.EPISODIC,
5587
5799
  strategy: {
5588
5800
  kind: MEMORY_STRATEGIES.SUMMARIZE,
5589
- recent: 6, // the 6 newest entries stay verbatim
5590
- size: 20, // how much history to load per turn
5591
- llm: anthropic(), // its OWN instance, not the agent's
5592
- model: 'claude-haiku-4-5', // named explicitly — no fallback
5801
+ recent: 6, // the 6 newest entries stay verbatim
5802
+ size: 20, // how much history to load per turn
5803
+ llm: anthropic(), // its OWN instance, not the agent's
5804
+ model: 'claude-haiku-4-5', // named explicitly — no fallback
5593
5805
  },
5594
5806
  store,
5595
5807
  });
@@ -5619,8 +5831,8 @@ quietly bills your MAIN model for compression, and a different vendor is sent a
5619
5831
  model id it has never heard of, mid-conversation, on a paid run.
5620
5832
 
5621
5833
  `Agent.memory()` now also refuses a summarizer that is the agent's own provider
5622
- **instance** at the agent's own model (the narrow 8.14.0 rule; a *second
5623
- instance* of the same vendor at the same model is allowed and sometimes right).
5834
+ **instance** at the agent's own model (the narrow 8.14.0 rule; a _second
5835
+ instance_ of the same vendor at the same model is allowed and sometimes right).
5624
5836
  `defineMemory` cannot make that check — it has never heard of an agent — so a
5625
5837
  `MemoryDefinition` now declares `billing: { provider, model }` and the builder
5626
5838
  reads it, the same field and shape `WindowStrategy.billing` already used. One
@@ -5733,7 +5945,7 @@ why, with a date.
5733
5945
  import { gemini } from 'agentfootprint/providers';
5734
5946
 
5735
5947
  const vertex = gemini({ project: 'my-project', location: 'us-central1' }); // ADC
5736
- const studio = gemini({ apiKey: process.env.GEMINI_API_KEY! }); // one key
5948
+ const studio = gemini({ apiKey: process.env.GEMINI_API_KEY! }); // one key
5737
5949
  ```
5738
5950
 
5739
5951
  Two doors, one adapter, and neither is guessed: a project selects Vertex, a key
@@ -5786,8 +5998,8 @@ Three decisions worth reading before you rely on them:
5786
5998
  that arrives anyway is kept out of the visible answer on both paths.
5787
5999
  - **A stream that reports no usage reports ZERO, never an estimate.**
5788
6000
  `models.countTokens` is on the namespace, is not called, and is named in the pin
5789
- as not called: it answers what a request *tokenises to*, not what the call was
5790
- *billed for*. Same law as `openai()` and `ollama()`. (Usage is read off the
6001
+ as not called: it answers what a request _tokenises to_, not what the call was
6002
+ _billed for_. Same law as `openai()` and `ollama()`. (Usage is read off the
5791
6003
  closing chunk BEFORE any content guard — the bug that made streamed turns bill
5792
6004
  as zero on two earlier adapters.)
5793
6005
 
@@ -5859,8 +6071,8 @@ new and load-bearing.
5859
6071
 
5860
6072
  ### Added — Gemini's over-long-request sentence joins the typed error
5861
6073
 
5862
- `ContextWindowExceededError` (9.6.0) now translates *"The input token count
5863
- (1200293) exceeds the maximum number of tokens allowed (1048576)."* — a word order
6074
+ `ContextWindowExceededError` (9.6.0) now translates _"The input token count
6075
+ (1200293) exceeds the maximum number of tokens allowed (1048576)."_ — a word order
5864
6076
  the existing patterns did not match — and reads both numbers out of it, including
5865
6077
  the case where Google ships the first parenthesis empty. Detection stays
5866
6078
  conservative: "INPUT token count" is what keeps it off a `max_tokens` validation
@@ -5909,8 +6121,8 @@ concurrency-and-sessions section, and a status row per boundary.
5909
6121
  - A `gemini()` error never prints the API key it was constructed with. The
5910
6122
  redaction is narrow by design — the exact string you passed, removed from the
5911
6123
  message, the stack and the wrapped cause — and is not a heuristic scrubber:
5912
- a thrown provider error reaches the model as a tool result *and* the commit log
5913
- *and* every observability sink, so one interpolation would leak to all of them.
6124
+ a thrown provider error reaches the model as a tool result _and_ the commit log
6125
+ _and_ every observability sink, so one interpolation would leak to all of them.
5914
6126
 
5915
6127
  ## [9.12.0] - 2026-08-12
5916
6128
 
@@ -5931,7 +6143,7 @@ provider signed:
5931
6143
  const credentials = agentCoreIdentity({
5932
6144
  region: 'us-west-2',
5933
6145
  workloadName: 'workflow_assistant_agent',
5934
- requireUserToken: true, // optional — refuse a delegated call with no proof
6146
+ requireUserToken: true, // optional — refuse a delegated call with no proof
5935
6147
  });
5936
6148
 
5937
6149
  // inside a tool
@@ -5946,7 +6158,7 @@ await ctx.credentials.getCredential({ service: 'google', mode: 'user', userToken
5946
6158
  - **Nothing downstream changed.** `GetWorkloadAccessTokenForJWT` answers with the
5947
6159
  same `workloadAccessToken` the by-userId exchange does, so it feeds the same
5948
6160
  `GetResourceOauth2Token` call, the same `Credential`, the same `toHeaders()`.
5949
- The vault entry at the end belongs to the *person* rather than to the agent,
6161
+ The vault entry at the end belongs to the _person_ rather than to the agent,
5950
6162
  which is what makes revoking their access actually revoke it.
5951
6163
  - **Verified against the real SDK before it shipped**, names and shapes both:
5952
6164
  `{ workloadName, userToken }` in, `{ workloadAccessToken }` out. It joins the
@@ -6030,7 +6242,7 @@ refuses a call it used to allow, or emits a field it did not before.
6030
6242
  ### Added — `maxToolResultChars`: a ceiling on ONE tool result
6031
6243
 
6032
6244
  ```ts
6033
- Agent.create({ provider, model, maxToolResultChars: 20_000 })
6245
+ Agent.create({ provider, model, maxToolResultChars: 20_000 });
6034
6246
  ```
6035
6247
 
6036
6248
  Over the cap, the result is **replaced** by a marker that tells the model what
@@ -6070,8 +6282,8 @@ happened and what to do about it:
6070
6282
  ### Added — WHO the run was for, on every event
6071
6283
 
6072
6284
  `EventMeta.principal` and `EventMeta.tenant` (9.11.0) join `sessionId` (9.4.0) on
6073
- every event's meta. The stream has always said *what* happened and *when*; this
6074
- is the *who*, and the three together are an audit record rather than a debug log.
6285
+ every event's meta. The stream has always said _what_ happened and _when_; this
6286
+ is the _who_, and the three together are an audit record rather than a debug log.
6075
6287
 
6076
6288
  ```ts
6077
6289
  await agent.run(message, {
@@ -6092,7 +6304,7 @@ await agent.run(message, {
6092
6304
  and `sessionId` beside it is the fact the transport delivered.
6093
6305
  - **Which sinks carry it, checked rather than assumed.** `fileObservability`,
6094
6306
  `cloudwatchObservability`, `agentcoreObservability` and `auditExport` serialize
6095
- the whole envelope and inherit it for free — in `auditExport`'s case *inside*
6307
+ the whole envelope and inherit it for free — in `auditExport`'s case _inside_
6096
6308
  the hash chain, so editing who breaks the same verification as editing what.
6097
6309
  `otelObservability` maps signals onto spans rather than serializing, so the
6098
6310
  actor is PLACED there: `agentfootprint.principal.id` /
@@ -6118,7 +6330,7 @@ const policy = PermissionPolicy.fromRoles(roles, 'support', {
6118
6330
  ```
6119
6331
 
6120
6332
  - **`Tool.capabilities`** (`'memory_read' | 'memory_write' | 'external_net' |
6121
- 'user_data'`) is a declaration, never an inference. A tool's reach is not
6333
+ 'user_data'`) is a declaration, never an inference. A tool's reach is not
6122
6334
  knowable from its name, schema or description, and guessing would rest a policy
6123
6335
  decision on a heuristic.
6124
6336
  - **`PermissionChecker.governs`** is an optional, feature-detected member —
@@ -6153,8 +6365,8 @@ PermissionPolicy.fromRoles(roles, 'support', {
6153
6365
  - The refusal lands **before `execute`**, so a `surfaceMode: 'tool-only'` skill's
6154
6366
  body is never even computed.
6155
6367
  - **Hidden means unnamed.** The graph offer lists unreachable skills as "not
6156
- reachable from here" because a cursor can move; a hidden skill is about *who is
6157
- asking*, and naming it would tell one role about another role's capabilities.
6368
+ reachable from here" because a cursor can move; a hidden skill is about _who is
6369
+ asking_, and naming it would tell one role about another role's capabilities.
6158
6370
  - **The enum stays the full catalog.** `toolArgValidation` runs before the gate,
6159
6371
  so narrowing it would turn a policy refusal into a generic schema error and the
6160
6372
  model would never read the policy's own message — the reasoning 8.5.0 recorded
@@ -6204,7 +6416,7 @@ await standingAgent({
6204
6416
  agentFactory: () => Agent.create({ provider, model }).system('…').build(),
6205
6417
  sessions: sqliteSessions({ file: './sessions.db' }),
6206
6418
  host: nodeHost({ port: 8080 }),
6207
- maxActiveSessions: 200, // default 100
6419
+ maxActiveSessions: 200, // default 100
6208
6420
  });
6209
6421
  ```
6210
6422
 
@@ -6302,7 +6514,6 @@ nodeHost({ sessionCookie: 'af_session' }); // …or no client code at all
6302
6514
  - New example: `examples/deploy/multi-user.ts` — two people served at once,
6303
6515
  proving the overlap in wall clock and that neither saw the other's memory.
6304
6516
 
6305
-
6306
6517
  ## [9.9.0] - 2026-08-12
6307
6518
 
6308
6519
  **A bug report IS the evidence.**
@@ -6320,7 +6531,7 @@ with the prose attached.
6320
6531
  ```ts
6321
6532
  import { describeBugReport, exportBugReport } from 'agentfootprint/observe';
6322
6533
 
6323
- const offer = describeBugReport(recording); // measure — nothing has left yet
6534
+ const offer = describeBugReport(recording); // measure — nothing has left yet
6324
6535
  // …show offer.units to the human; they tick some…
6325
6536
  const report = exportBugReport(recording, {
6326
6537
  include: ['conv-1', 'file-conversation', 'file-environment'],
@@ -6329,7 +6540,7 @@ const report = exportBugReport(recording, {
6329
6540
  expected: 'the updated price',
6330
6541
  actual: 'the price from before the update',
6331
6542
  });
6332
- fs.writeFileSync(report.filename, report.zip); // a real .zip
6543
+ fs.writeFileSync(report.filename, report.zip); // a real .zip
6333
6544
  ```
6334
6545
 
6335
6546
  - **The manifest is SELECTABLE UNITS, not a blob.** Each conversation is a unit
@@ -6347,7 +6558,7 @@ fs.writeFileSync(report.filename, report.zip); // a real .zip
6347
6558
  - **What was left out is STATED.** `manifest.excluded` counts the conversations,
6348
6559
  files, events and turns that were withheld, names their unit ids, and the
6349
6560
  issue body repeats it. A maintainer reading turn 4 must be able to tell that
6350
- turns 1–3 were *withheld*, not *lost*.
6561
+ turns 1–3 were _withheld_, not _lost_.
6351
6562
  - **Redacted keys, BY NAME.** The recording arrives already redacted (footprintjs
6352
6563
  scrubs at commit time), so nothing here scrubs anything — it would be too late
6353
6564
  to matter and a second policy could only disagree with the first. Instead the
@@ -6383,9 +6594,9 @@ plain `fetch` with no SDK:
6383
6594
 
6384
6595
  ```ts
6385
6596
  const reporter = githubBugReporter({
6386
- issueRepo: 'footprintjs/agentfootprint', // public — the conversation
6387
- evidenceRepo: 'acme/af-bug-evidence', // private — the run
6388
- }); // token: GITHUB_TOKEN, or `token`
6597
+ issueRepo: 'footprintjs/agentfootprint', // public — the conversation
6598
+ evidenceRepo: 'acme/af-bug-evidence', // private — the run
6599
+ }); // token: GITHUB_TOKEN, or `token`
6389
6600
  const { issueUrl, zipUrl } = await reporter.file(report);
6390
6601
  ```
6391
6602
 
@@ -6426,13 +6637,13 @@ const { issueUrl, zipUrl } = await reporter.file(report);
6426
6637
  calls — browser-safe and server-safe, no client secret, no dependency:
6427
6638
 
6428
6639
  ```ts
6429
- const signIn = await githubDeviceSignIn({ clientId }); // returns at once
6640
+ const signIn = await githubDeviceSignIn({ clientId }); // returns at once
6430
6641
  show(`Open ${signIn.verificationUri} and enter ${signIn.userCode}`);
6431
- const { token, login } = await signIn.completed; // resolves on approve
6642
+ const { token, login } = await signIn.completed; // resolves on approve
6432
6643
  ```
6433
6644
 
6434
- A server PAT files every report as the *application*; this files it as the
6435
- *reporter*, which is what a field tester filing upstream needs. It honours
6645
+ A server PAT files every report as the _application_; this files it as the
6646
+ _reporter_, which is what a field tester filing upstream needs. It honours
6436
6647
  `slow_down`, respects the code's expiry, takes an `AbortSignal`, and fetches
6437
6648
  `/user` for attribution (a `/user` that refuses is not fatal — the token still
6438
6649
  works, the login is simply absent). The token it returns is handed to
@@ -6502,7 +6713,9 @@ agent.enable.observability({
6502
6713
  maxBytes: 64 * 1024 * 1024,
6503
6714
  }),
6504
6715
  });
6505
- process.on('SIGTERM', async () => { await agent.shutdown(); }); // flushes
6716
+ process.on('SIGTERM', async () => {
6717
+ await agent.shutdown();
6718
+ }); // flushes
6506
6719
  ```
6507
6720
 
6508
6721
  Four things it is deliberate about:
@@ -6546,8 +6759,8 @@ import { vaultCredentials } from 'agentfootprint/security';
6546
6759
 
6547
6760
  const credentials = vaultCredentials({
6548
6761
  address: 'https://vault.internal:8200',
6549
- paths: { github: 'ci/github' }, // …or resolve(service), or neither
6550
- }); // token: `token`, else VAULT_TOKEN
6762
+ paths: { github: 'ci/github' }, // …or resolve(service), or neither
6763
+ }); // token: `token`, else VAULT_TOKEN
6551
6764
  ```
6552
6765
 
6553
6766
  The tool code does not change from the `staticTokens` version — same port, same
@@ -6581,7 +6794,7 @@ service)` as the seam for a shop whose field names are its own.
6581
6794
  the service, the mount path and the HTTP status — and nothing from the
6582
6795
  response body, nothing from the token, not even the field names the secret
6583
6796
  carries. This is the 8.6.0 law applied one adapter down: a thrown message
6584
- reaches the model as a tool result *and* rides
6797
+ reaches the model as a tool result _and_ rides
6585
6798
  `agentfootprint.credential.failed` to every observer. It is pinned by a
6586
6799
  grep-shaped test that walks every failure path — unknown service, 401, 403,
6587
6800
  404, 503, a non-JSON reply, a KV v1 response, an unrecognised field set, and a
@@ -6594,21 +6807,21 @@ service)` as the seam for a shop whose field names are its own.
6594
6807
  - **`infrastructure/on-premises.mdx`** — the provider page beside AWS. The
6595
6808
  local-first ladder (mock → local model → your gateway → a paid API) as the
6596
6809
  opening frame, then a service-by-service map: LLM (`ollama`, `openai({
6597
- baseURL })` for vLLM / llama.cpp / a corporate gateway, or the two-method
6810
+ baseURL })` for vLLM / llama.cpp / a corporate gateway, or the two-method
6598
6811
  port), stores (`sqliteVectorStore`, `pgVectorStore`, `staticVectorStore`,
6599
6812
  `RedisStore`), embedders (`localEmbedder`, `staticEmbedder`), hosting
6600
6813
  (`httpHost` / `nodeHost` + `sqliteSessions` / `memorySessions`), code
6601
- execution (`localCodeRunner` — *isolation, not a sandbox*), telemetry
6814
+ execution (`localCodeRunner` — _isolation, not a sandbox_), telemetry
6602
6815
  (`otelObservability` to any OTLP collector, `fileObservability` when there is
6603
6816
  none, `auditExport` for evidence), credentials (`staticTokens`,
6604
6817
  `vaultCredentials`, and the port for everything else) and tools (`mcpClient`
6605
6818
  over stdio or Streamable HTTP). It ends with **what is NOT here** — no
6606
6819
  Kubernetes-native anything, no second secret-manager adapter, no metrics
6607
6820
  exporter, no retention policy, no air-gapped model distribution.
6608
- - **The status vocabulary gained one honest rung.** *Contract-shaped and tested;
6609
- awaiting field use* is what `fileObservability` and `vaultCredentials` carry:
6821
+ - **The status vocabulary gained one honest rung.** _Contract-shaped and tested;
6822
+ awaiting field use_ is what `fileObservability` and `vaultCredentials` carry:
6610
6823
  their ports and refusals are pinned by tests, and neither has met a real
6611
- production disk or vault. *Verified in a production field deployment* now
6824
+ production disk or vault. _Verified in a production field deployment_ now
6612
6825
  appears in exactly one place, describing a deployment **shape** — a standing
6613
6826
  agent over `httpHost` + `sqliteSessions` against an OpenAI-compatible gateway,
6614
6827
  the shape several past releases exist because of — and never an adapter. The
@@ -6639,14 +6852,14 @@ handed that said "this is over".
6639
6852
 
6640
6853
  Three optional fields, sourced from what the engine already stamps:
6641
6854
 
6642
- | field | source | absent when |
6643
- |---|---|---|
6644
- | `ctx.runId` | the run in flight | there is no run — a call served over `mcpServe` is one call, not a turn |
6645
- | `ctx.sessionId` | `run({ sessionId })` ← `HostRequest.sessionId` | the run is not session-bound |
6646
- | `ctx.identity` | the identity the CALLER passed | the caller passed none |
6855
+ | field | source | absent when |
6856
+ | --------------- | ---------------------------------------------- | ----------------------------------------------------------------------- |
6857
+ | `ctx.runId` | the run in flight | there is no run — a call served over `mcpServe` is one call, not a turn |
6858
+ | `ctx.sessionId` | `run({ sessionId })` ← `HostRequest.sessionId` | the run is not session-bound |
6859
+ | `ctx.identity` | the identity the CALLER passed | the caller passed none |
6647
6860
 
6648
6861
  Every one is **absent rather than invented**, which is the 9.4.0 rule applied one
6649
- layer down. `ctx.identity` is deliberately *not* the run's internal
6862
+ layer down. `ctx.identity` is deliberately _not_ the run's internal
6650
6863
  `runIdentity`: that is always populated, defaulting to
6651
6864
  `{ conversationId: '<runId>' }`, and handing a tool a synthesized conversation as
6652
6865
  "the identity" would let it isolate a live sandbox on a fiction.
@@ -6676,12 +6889,12 @@ tool that wants a run-scoped session needs to know it is at a door with no runs
6676
6889
  BEFORE it opens one. Asking for a scope a door cannot honour throws, naming the
6677
6890
  door.
6678
6891
 
6679
- | scope | fires |
6680
- |---|---|
6681
- | `'call'` | when `tool.execute` settles — resolve **or** throw. Every door, including `mcpServe`. |
6682
- | `'run'` | at a run terminal that is **not a pause**. |
6683
- | `'session'` | `agent.closeToolSessions({ sessionId })`. |
6684
- | `'shutdown'` | `agent.shutdown()`. |
6892
+ | scope | fires |
6893
+ | ------------ | ------------------------------------------------------------------------------------- |
6894
+ | `'call'` | when `tool.execute` settles — resolve **or** throw. Every door, including `mcpServe`. |
6895
+ | `'run'` | at a run terminal that is **not a pause**. |
6896
+ | `'session'` | `agent.closeToolSessions({ sessionId })`. |
6897
+ | `'shutdown'` | `agent.shutdown()`. |
6685
6898
 
6686
6899
  Seven laws, each pinned: at most once ever · idempotent by `(tool, scope, key)`
6687
6900
  with the FIRST registration winning (it holds the live handle) · reverse
@@ -6692,7 +6905,7 @@ tolerates "already gone" · nothing live is ever persisted into a checkpoint.
6692
6905
  **A pause is not a terminal.** `'run'` teardown deliberately does not hang off
6693
6906
  `finally`, which also runs on both pause shapes. A `checkIn` on a code
6694
6907
  interpreter stops the run so a person can approve the code; tearing the sandbox
6695
- down there destroys the exact state the resume needs, and it fails *quietly* — as
6908
+ down there destroys the exact state the resume needs, and it fails _quietly_ — as
6696
6909
  a resumed run that "just re-ran everything". An error IS a terminal.
6697
6910
 
6698
6911
  ### Added — `agent.closeToolSessions({ sessionId })`
@@ -6781,7 +6994,7 @@ the rows, and what comes back is the finding.
6781
6994
  `client.send(new Command(...))`, pinned in `test/adapters/aws/awsCommandPin.ts`
6782
6995
  and **verified against a real install of the SDK before shipping** — including
6783
6996
  two shapes a design could only have guessed at: `Invoke` answers with an EVENT
6784
- STREAM, and seven of its nine union members are modelled *exceptions* (folded
6997
+ STREAM, and seven of its nine union members are modelled _exceptions_ (folded
6785
6998
  in as empty output, an `AccessDenied` would have reported a clean run that
6786
6999
  "printed nothing"), and `Stop` takes the session id, not a URI.
6787
7000
  - **`codeRunnerTool({ runner, scope })`** (main barrel) — holds one session per
@@ -6846,8 +7059,8 @@ and now shares the paging with the general one.
6846
7059
  **Behaviour change, named loudly: multi-turn memory starts actually
6847
7060
  retaining.** A six-turn conversation stores twelve message entries where it
6848
7061
  stored two, and the window injects up to `size` of them instead of the last
6849
- exchange — so prompts get longer and stores get bigger *because the agent is
6850
- now remembering what it was asked to remember*. Turn it down deliberately
7062
+ exchange — so prompts get longer and stores get bigger _because the agent is
7063
+ now remembering what it was asked to remember_. Turn it down deliberately
6851
7064
  (`size`, `DECAY`, `.compaction()`) rather than by accident.
6852
7065
 
6853
7066
  Cost: one paged `list()` per store per run, and only when a memory actually
@@ -7096,7 +7309,7 @@ compaction that runs.
7096
7309
  **AWS adapters tell the truth.**
7097
7310
 
7098
7311
  A production field report tested 9.3.0 against a real account and found two
7099
- adapters *dispatching calls that were never made against AWS* — one sending a
7312
+ adapters _dispatching calls that were never made against AWS_ — one sending a
7100
7313
  command that does not exist, one calling a method that a command-based client
7101
7314
  does not have. Both compiled. Both had green tests. Every one of those tests
7102
7315
  injected a double past the SDK, which is exactly why the bug class survives: the
@@ -7193,9 +7406,9 @@ are untouched.
7193
7406
 
7194
7407
  ### Changed — a fail-closed refusal now READS final
7195
7408
 
7196
- When a `PermissionChecker` throws, the call is denied. What the model was *told*
7409
+ When a `PermissionChecker` throws, the call is denied. What the model was _told_
7197
7410
  was the checker's own thrown message — and those are written for operators:
7198
- *"not available right now"*, `ECONNREFUSED`, *"timed out"*. **Measured in
7411
+ _"not available right now"_, `ECONNREFUSED`, _"timed out"_. **Measured in
7199
7412
  production: a real model read that as weather and retried the same tool to
7200
7413
  `maxIterations`, then returned the empty string.** Against the local policy's
7201
7414
  long-standing bracketed form the same model adapted cleanly on the first
@@ -7236,8 +7449,8 @@ calls did so in a silence that read like health.
7236
7449
  ### Added — `EventMeta.sessionId`: which CONVERSATION an event belongs to
7237
7450
 
7238
7451
  `meta.runId` is per `run()` / `resume()`; a session spans many. A shipped
7239
- telemetry stream could answer *"what happened in this run?"* and not *"what
7240
- happened in this conversation?"* — the question a session-oriented host is built
7452
+ telemetry stream could answer _"what happened in this run?"_ and not _"what
7453
+ happened in this conversation?"_ — the question a session-oriented host is built
7241
7454
  around, and one the events alone cannot be joined back into afterwards.
7242
7455
 
7243
7456
  `standingAgent` now threads the caller's own session id onto every event the run
@@ -7257,13 +7470,13 @@ three preconditions it had no evidence for, all three of which were wrong at
7257
7470
  once:
7258
7471
 
7259
7472
  - **A euclidean index was accepted.** The construction refusal read
7260
- `options.distanceMetric`, a *claim by the caller* about an index the store did
7473
+ `options.distanceMetric`, a _claim by the caller_ about an index the store did
7261
7474
  not create, so the default (undeclared) sailed through. Measured live: a
7262
7475
  vector queried against itself returned **0.9991630113800056**, where a true
7263
7476
  cosine self-similarity is exactly `1.0` — the very "number that READS like a
7264
7477
  cosine and is not one" this adapter's own header warns about.
7265
7478
  - **A missing `nonFilterableMetadataKeys: ['af']`** surfaced as a raw AWS
7266
- `ValidationException` — *"Filterable metadata must have at most 2048 bytes"* —
7479
+ `ValidationException` — _"Filterable metadata must have at most 2048 bytes"_ —
7267
7480
  **mid-import**, with documents already written and success already reported
7268
7481
  for them.
7269
7482
  - **The index dimension was never compared with the embedder.**
@@ -7287,11 +7500,11 @@ index whose metric, layout and dimension it would be guessing at.
7287
7500
 
7288
7501
  **Three promises the code had already made, kept.**
7289
7502
 
7290
- `MemoryStore`'s own docstring has named its backends since 2.x — *"Every storage
7503
+ `MemoryStore`'s own docstring has named its backends since 2.x — _"Every storage
7291
7504
  backend (InMemory, Redis, DynamoDB, **Postgres**, Bedrock AgentCore) implements
7292
- this interface"* — and named the queries too, in the two places an implementer
7293
- would look: *"**Postgres**: multi-row INSERT … ON CONFLICT DO UPDATE"* for
7294
- `putMany`, and *"**pgvector**: `ORDER BY embedding <=> query LIMIT k`"* for
7505
+ this interface"_ — and named the queries too, in the two places an implementer
7506
+ would look: _"**Postgres**: multi-row INSERT … ON CONFLICT DO UPDATE"_ for
7507
+ `putMany`, and _"**pgvector**: `ORDER BY embedding <=> query LIMIT k`"_ for
7295
7508
  `search`. Every one of those sentences was true about the design and false about
7296
7509
  the shipped package. Two of the three items below are the same shape: a
7297
7510
  documented promise with nothing behind it.
@@ -7455,8 +7668,8 @@ are now pinned; the rest are below.
7455
7668
  ### The conversation has a door with its own name
7456
7669
 
7457
7670
  ```ts
7458
- await agent.run({ message: 'Book me a table for two.' }); // one turn
7459
- await agent.followUp('Make it three.'); // the next one
7671
+ await agent.run({ message: 'Book me a table for two.' }); // one turn
7672
+ await agent.followUp('Make it three.'); // the next one
7460
7673
 
7461
7674
  // …or hand the conversation around — plain JSON, any store, any machine:
7462
7675
  const conversation = agent.checkpoint();
@@ -7514,7 +7727,7 @@ already broken in effect, so the throw is a fix, not a new restriction.
7514
7727
  is what makes `checkpoint()`, `getLastSnapshot()` and `followUp()` possible.
7515
7728
  Two overlapping runs both finished, both returning plausible answers, and the
7516
7729
  state afterwards belonged to whichever finished last — so `checkpoint()` could
7517
- hand back the *other* run's conversation, with nothing in either recording
7730
+ hand back the _other_ run's conversation, with nothing in either recording
7518
7731
  saying so. That is corruption, not concurrency. `standingAgent` has serialized
7519
7732
  runs since it existed and calls it "a correctness requirement rather than a
7520
7733
  tuning choice"; the guarantee now lives in the primitive. Two turns at once:
@@ -7528,16 +7741,16 @@ a consent gate any later message could walk around. Answer it with
7528
7741
  `resume(checkpoint, decision)`, or say plainly that it is being dropped:
7529
7742
 
7530
7743
  ```ts
7531
- const dropped = agent.abandonPause(); // { toolName, toolCallId, question }
7744
+ const dropped = agent.abandonPause(); // { toolName, toolCallId, question }
7532
7745
  await agent.run({ message: 'never mind, different question' });
7533
7746
  ```
7534
7747
 
7535
- `abandonPause()` *returns* what it dropped, so the abandonment can be logged
7748
+ `abandonPause()` _returns_ what it dropped, so the abandonment can be logged
7536
7749
  rather than performed blind.
7537
7750
 
7538
7751
  **A pause belongs to a session, not to the instance.** `standingAgent` shares
7539
7752
  one `Agent` across every session, so the instance guard alone would have let
7540
- session A's unanswered question refuse session B's *first* message — a
7753
+ session A's unanswered question refuse session B's _first_ message — a
7541
7754
  different conversation, a different person, an answer they were never asked
7542
7755
  for. The composer now releases the instance at the moment ownership moves: once
7543
7756
  the pause is in the store, the store owns it, and a later request carrying a
@@ -7558,7 +7771,7 @@ No shipped example, test or doc called it twice.
7558
7771
  ### `agent.canExplain()`
7559
7772
 
7560
7773
  `.selfExplain()` was already honest to the **model** with no record bound — the
7561
- trace tools answer *"No completed run is available yet"* and the skill body says
7774
+ trace tools answer _"No completed run is available yet"_ and the skill body says
7562
7775
  to say so plainly. It had no answer for the **program**. `canExplain()` returns
7563
7776
  `false` for two honest reasons — not built with `.selfExplain()`, or built with
7564
7777
  it and no turn completed — so a caller can route a why-question before spending
@@ -7586,7 +7799,7 @@ of this class quietly. Three kinds of pin:
7586
7799
  bare `throw`.
7587
7800
  2. **Adapted** — pinned by what reaches the model / the store / the caller,
7588
7801
  observed on the wire, never by mocking internals.
7589
- 3. **Stated** — pinned **twice**: the behavior, *and* the sentence in the source
7802
+ 3. **Stated** — pinned **twice**: the behavior, _and_ the sentence in the source
7590
7803
  that states it. A stated behavior whose statement was deleted is back to
7591
7804
  being a silent success, and only the second assertion catches that.
7592
7805
 
@@ -7605,7 +7818,7 @@ decision somebody makes in a diff.
7605
7818
  prints the messages the provider actually received for each. The model's own
7606
7819
  "this is your first message" reply is the evidence.
7607
7820
  - `examples/features/49-self-explain-live.ts` — turn 2 now goes through
7608
- `followUp()`. It was a second `run()`, and the scripted mock read the *first*
7821
+ `followUp()`. It was a second `run()`, and the scripted mock read the _first_
7609
7822
  user message, so the demo's own scripting was masking the restart. Live, the
7610
7823
  why-question used to arrive with no subject.
7611
7824
  - `docs-next/content/docs/build/conversations.mdx` — the conversation, the two
@@ -7617,13 +7830,13 @@ decision somebody makes in a diff.
7617
7830
  Additive except for the three refusals, and all three fire on code that was
7618
7831
  already wrong:
7619
7832
 
7620
- | if you… | you now get | do this |
7621
- | --- | --- | --- |
7622
- | call `run()` twice expecting continuity | the same behavior as before (a new conversation) | `followUp(message)` or `run({ message, continueFrom })` |
7623
- | overlap two `run()` calls on one agent | `RunInFlightError` | await the first, or build a second agent |
7624
- | send a message while a pause is open | `PendingQuestionError` | `resume(checkpoint, decision)`, or `abandonPause()` first |
7625
- | call `.system()` twice | build-time throw | join the strings, or `.steering()` / `.configure()` |
7626
- | continue a stored conversation on a differently-**named** agent | `ConversationMismatchError` | continue it on the agent whose id recorded it |
7833
+ | if you… | you now get | do this |
7834
+ | --------------------------------------------------------------- | ------------------------------------------------ | --------------------------------------------------------- |
7835
+ | call `run()` twice expecting continuity | the same behavior as before (a new conversation) | `followUp(message)` or `run({ message, continueFrom })` |
7836
+ | overlap two `run()` calls on one agent | `RunInFlightError` | await the first, or build a second agent |
7837
+ | send a message while a pause is open | `PendingQuestionError` | `resume(checkpoint, decision)`, or `abandonPause()` first |
7838
+ | call `.system()` twice | build-time throw | join the strings, or `.steering()` / `.configure()` |
7839
+ | continue a stored conversation on a differently-**named** agent | `ConversationMismatchError` | continue it on the agent whose id recorded it |
7627
7840
 
7628
7841
  `checkpoint()` payloads written by 9.1.0 and earlier continue to work; they
7629
7842
  simply carry no `identity` or `agent`, which is the honest answer for a
@@ -7654,13 +7867,13 @@ embedder might have — which then cuts every larger embedder short.
7654
7867
  characters, that this embedder represents faithfully. Every shipped embedder
7655
7868
  fills it in:
7656
7869
 
7657
- | embedder | `maxInputChars` | where the number comes from |
7658
- |---|---|---|
7659
- | `localEmbedder()` | `2000` | measured — the default model's 512-wordpiece-token cliff |
7660
- | `openaiEmbedder()` | `32000` | the documented 8,191-token window, at 4 characters a token |
7661
- | `bedrockEmbedder()` | `32000` | Titan's documented 8,192-token window, same conversion |
7662
- | `staticEmbedder()` | `1000000` | no transformer, so no context window — nothing is ever clipped |
7663
- | `mockEmbedder()` | `1000000` | reads every character in a loop |
7870
+ | embedder | `maxInputChars` | where the number comes from |
7871
+ | ------------------- | --------------- | -------------------------------------------------------------- |
7872
+ | `localEmbedder()` | `2000` | measured — the default model's 512-wordpiece-token cliff |
7873
+ | `openaiEmbedder()` | `32000` | the documented 8,191-token window, at 4 characters a token |
7874
+ | `bedrockEmbedder()` | `32000` | Titan's documented 8,192-token window, same conversion |
7875
+ | `staticEmbedder()` | `1000000` | no transformer, so no context window — nothing is ever clipped |
7876
+ | `mockEmbedder()` | `1000000` | reads every character in a loop |
7664
7877
 
7665
7878
  `indexCorpus`, `indexFolder` and `indexDocuments` read the embedder's declared
7666
7879
  ceiling **in preference to** their own 2,000-character default. An explicit
@@ -7678,7 +7891,7 @@ is accepted because the cliff belongs to the **model**, not to the factory.
7678
7891
  ### Truncation became visible
7679
7892
 
7680
7893
  A run that clipped anything now says so — **once**, on `console.warn`, naming
7681
- the count, the ceiling in effect *and where that ceiling came from*, and the
7894
+ the count, the ceiling in effect _and where that ceiling came from_, and the
7682
7895
  two fixes (re-split smaller, or raise `maxChunkChars`). `IndexReport` gains
7683
7896
  **`truncatedCount`** beside the existing `truncated` list: the list is what you
7684
7897
  debug with, the count is what you assert on and what a dashboard row can hold.
@@ -7726,27 +7939,27 @@ deprecation warnings on 8.20.0, it compiles and runs unchanged on 9.0.0.**
7726
7939
 
7727
7940
  ### Removed — the sixteen door aliases
7728
7941
 
7729
- Each removed path re-exported the *same symbols* the door carries, never copies,
7942
+ Each removed path re-exported the _same symbols_ the door carries, never copies,
7730
7943
  so this is a find-and-replace on import lines. No name moved; no name was lost.
7731
7944
 
7732
- | you were importing from | import from |
7733
- |---|---|
7734
- | `agentfootprint/llm-providers` | `agentfootprint/providers` |
7735
- | `agentfootprint/embedders` | `agentfootprint/providers` |
7736
- | `agentfootprint/tool-providers` | `agentfootprint/providers` |
7737
- | `agentfootprint/thinking` | `agentfootprint/providers` |
7738
- | `agentfootprint/memory-providers` | `agentfootprint/memory` |
7739
- | `agentfootprint/observability-providers` | `agentfootprint/observe` |
7740
- | `agentfootprint/strategies` | `agentfootprint/observe` |
7741
- | `agentfootprint/stream` | `agentfootprint/observe` |
7742
- | `agentfootprint/status` | `agentfootprint/observe` |
7743
- | `agentfootprint/locales` | `agentfootprint/observe` |
7744
- | `agentfootprint/debug` | `agentfootprint/observe` |
7745
- | `agentfootprint/debug/finders` | `agentfootprint/observe` |
7746
- | `agentfootprint/observability/contextError/finders` | `agentfootprint/observe` |
7747
- | `agentfootprint/hosting-providers` | `agentfootprint/hosting` |
7748
- | `agentfootprint/injection-engine` | `agentfootprint/context` |
7749
- | `agentfootprint/identity` | `agentfootprint/security` |
7945
+ | you were importing from | import from |
7946
+ | --------------------------------------------------- | -------------------------- |
7947
+ | `agentfootprint/llm-providers` | `agentfootprint/providers` |
7948
+ | `agentfootprint/embedders` | `agentfootprint/providers` |
7949
+ | `agentfootprint/tool-providers` | `agentfootprint/providers` |
7950
+ | `agentfootprint/thinking` | `agentfootprint/providers` |
7951
+ | `agentfootprint/memory-providers` | `agentfootprint/memory` |
7952
+ | `agentfootprint/observability-providers` | `agentfootprint/observe` |
7953
+ | `agentfootprint/strategies` | `agentfootprint/observe` |
7954
+ | `agentfootprint/stream` | `agentfootprint/observe` |
7955
+ | `agentfootprint/status` | `agentfootprint/observe` |
7956
+ | `agentfootprint/locales` | `agentfootprint/observe` |
7957
+ | `agentfootprint/debug` | `agentfootprint/observe` |
7958
+ | `agentfootprint/debug/finders` | `agentfootprint/observe` |
7959
+ | `agentfootprint/observability/contextError/finders` | `agentfootprint/observe` |
7960
+ | `agentfootprint/hosting-providers` | `agentfootprint/hosting` |
7961
+ | `agentfootprint/injection-engine` | `agentfootprint/context` |
7962
+ | `agentfootprint/identity` | `agentfootprint/security` |
7750
7963
 
7751
7964
  What ships now is exactly: the root barrel, the ten doors (`/providers`,
7752
7965
  `/memory`, `/rag`, `/cache`, `/observe`, `/events`, `/context`, `/resilience`,
@@ -7762,23 +7975,23 @@ implementation barrel is served by its door as the same object.
7762
7975
 
7763
7976
  ### Removed — options, strings, methods, fields
7764
7977
 
7765
- | removed | replacement | since |
7766
- |---|---|---|
7767
- | `AgentBuilder.recorder(rec)` | `AgentBuilder.watch(rec)` — same list, same order, same attachment, and variadic | deprecated 8.0.0 |
7768
- | `defineSkill({ viaToolName })` | drop it — `'read_skill'` is the only activation tool the library builds; gate on a `rule` trigger or a `skillGraph()` edge | deprecated 8.7.0 |
7769
- | `skillsFromDir(dir, { viaToolName })` | drop it — same reason | deprecated 8.7.0 |
7770
- | `WindowRefusalReason` member `'summary-not-smaller'` | `'replacement-not-smaller'` | renamed 8.14.0 |
7771
- | type `FoldRefusal` | `WindowRefusal` | renamed 7.17 |
7772
- | type `FoldRefusalReason` | `WindowRefusalReason` | renamed 7.17 |
7773
- | `WindowStrategy` exported from `agentfootprint/memory` | `MemoryWindowStrategy` | renamed 7.27.1 |
7774
- | `CompactionRecord.foldedStageIds` | `WindowRecord.removedStageIds` | family name published 7.17 |
7775
- | `CompactionRecord.foldedMessageCount` | `WindowRecord.removedMessageCount` | family name published 7.17 |
7776
- | `ContextBudgetPressurePayload.capTokens` | `cap`, read with `unit` | renamed 8.14.0 |
7777
- | `ContextBudgetPressurePayload.projectedTokens` | `projected`, read with `unit` | renamed 8.14.0 |
7778
- | `BudgetPressureRecord.capTokens` | `cap`, read with `unit` | renamed 8.14.0 |
7779
- | `BudgetPressureRecord.projectedTokens` | `projected`, read with `unit` | renamed 8.14.0 |
7780
-
7781
- Three of those are worth a sentence each, because the *reason* is the migration:
7978
+ | removed | replacement | since |
7979
+ | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- | -------------------------- |
7980
+ | `AgentBuilder.recorder(rec)` | `AgentBuilder.watch(rec)` — same list, same order, same attachment, and variadic | deprecated 8.0.0 |
7981
+ | `defineSkill({ viaToolName })` | drop it — `'read_skill'` is the only activation tool the library builds; gate on a `rule` trigger or a `skillGraph()` edge | deprecated 8.7.0 |
7982
+ | `skillsFromDir(dir, { viaToolName })` | drop it — same reason | deprecated 8.7.0 |
7983
+ | `WindowRefusalReason` member `'summary-not-smaller'` | `'replacement-not-smaller'` | renamed 8.14.0 |
7984
+ | type `FoldRefusal` | `WindowRefusal` | renamed 7.17 |
7985
+ | type `FoldRefusalReason` | `WindowRefusalReason` | renamed 7.17 |
7986
+ | `WindowStrategy` exported from `agentfootprint/memory` | `MemoryWindowStrategy` | renamed 7.27.1 |
7987
+ | `CompactionRecord.foldedStageIds` | `WindowRecord.removedStageIds` | family name published 7.17 |
7988
+ | `CompactionRecord.foldedMessageCount` | `WindowRecord.removedMessageCount` | family name published 7.17 |
7989
+ | `ContextBudgetPressurePayload.capTokens` | `cap`, read with `unit` | renamed 8.14.0 |
7990
+ | `ContextBudgetPressurePayload.projectedTokens` | `projected`, read with `unit` | renamed 8.14.0 |
7991
+ | `BudgetPressureRecord.capTokens` | `cap`, read with `unit` | renamed 8.14.0 |
7992
+ | `BudgetPressureRecord.projectedTokens` | `projected`, read with `unit` | renamed 8.14.0 |
7993
+
7994
+ Three of those are worth a sentence each, because the _reason_ is the migration:
7782
7995
 
7783
7996
  - **`viaToolName` named a door that was never built.** The evaluator activates
7784
7997
  an `llm-activated` skill by matching `ctx.activatedInjectionIds`, only
@@ -7817,7 +8030,7 @@ replacement and says when the signpost comes down.
7817
8030
  Deleting the type member alone would have been a silent DOWNGRADE for the two
7818
8031
  `viaToolName` cases: an object literal gets an excess-property error, but an
7819
8032
  options bag arriving through a variable does not, and the value would then be
7820
- *ignored* where 8.7.0 refused it. So the field is read at run time exactly once
8033
+ _ignored_ where 8.7.0 refused it. So the field is read at run time exactly once
7821
8034
  more, to say it is gone.
7822
8035
 
7823
8036
  ### Two things deliberately kept
@@ -7832,7 +8045,7 @@ more, to say it is gone.
7832
8045
  `CircuitState`, which is declared in both breaker files as
7833
8046
  `'closed' | 'open' | 'half-open'` — two declarations, one type, pinned as such.
7834
8047
  - **`buildRunSteps(events)` survives, still `@deprecated`.** Its deprecation is
7835
- a *preference*, not a migration: live consumers should attach
8048
+ a _preference_, not a migration: live consumers should attach
7836
8049
  `runStepRecorder()` and read `getSteps()` (O(N), the house pattern) instead of
7837
8050
  re-walking an event log (O(N²) across repeated calls). But the shim is the only
7838
8051
  way to build steps from a saved event list — replay, post-hoc analysis, tests —
@@ -7909,7 +8122,7 @@ The fix is one coherent rule across the structural splitters:
7909
8122
  alone. The heading is now glued to the first body paragraph; it can never
7910
8123
  again be a chunk by itself.
7911
8124
  - **The family was inspected, and two members are exempt by design.**
7912
- `fixedWithOverlap` cuts uniformly sized chunks *by request* — imposing a
8125
+ `fixedWithOverlap` cuts uniformly sized chunks _by request_ — imposing a
7913
8126
  250-char floor on `fixedWithOverlap({ chars: 120 })` would repeal the
7914
8127
  caller's own choice, and its only runt (the file tail) has always folded
7915
8128
  backward. `wholeDocument` is one chunk per document by definition.
@@ -7963,7 +8176,7 @@ any immutable or serverless runtime has this problem):
7963
8176
  - **`exportCorpus(store, identity?)`** (`agentfootprint/rag`) — every entry
7964
8177
  of a corpus namespace as one plain-JSON `CorpusBundle`:
7965
8178
  `{ entries: [{ id, text, vector, metadata }], embedder: { id, dimensions },
7966
- namespace }`. Plain JSON on purpose — the runtime that needs this is
8179
+ namespace }`. Plain JSON on purpose — the runtime that needs this is
7967
8180
  exactly the runtime that cannot open a database file. It refuses an empty
7968
8181
  namespace (naming the identity-mismatch cause), entries with no vector or
7969
8182
  no passage (a bundle never ships an unservable or uncitable entry), and a
@@ -8125,7 +8338,7 @@ it, before a byte is embedded. `InMemoryStore` and `sqliteVectorStore` declare
8125
8338
  **Behaviour change:** building a corpus into `AgentCoreStore` or `RedisStore` is
8126
8339
  refused where it used to run. Neither could ever serve those vectors back —
8127
8340
  `RedisStore` has no `search()` at all, so the same mistake already failed one
8128
- layer later, when `defineRAG` refused the store *after* the whole index had been
8341
+ layer later, when `defineRAG` refused the store _after_ the whole index had been
8129
8342
  embedded and billed. The refusal moved to the call that starts the spending.
8130
8343
 
8131
8344
  **Absence is not a `false`.** A store that declares nothing behaves exactly as it
@@ -8145,7 +8358,7 @@ it wrong.
8145
8358
  ```ts
8146
8359
  import { bedrockEmbedder } from 'agentfootprint/providers';
8147
8360
 
8148
- const embedder = bedrockEmbedder({ region: 'us-east-1' }); // 1024-d
8361
+ const embedder = bedrockEmbedder({ region: 'us-east-1' }); // 1024-d
8149
8362
  const small = bedrockEmbedder({ region: 'us-east-1', dimensions: 512 });
8150
8363
  ```
8151
8364
 
@@ -8318,7 +8531,7 @@ the run stops re-asking and names the rule — in the `outputAttempts` row
8318
8531
  (`brokenBy`), in the event, in `outputContractUnmet()`, and in the warning.
8319
8532
  Re-asking cannot fix a rule: a deterministic one breaks the next answer
8320
8533
  identically, so the retries buy a repeat of the same ending. An answer that was
8321
- *already* bad still spends its retries — the stop applies only when the
8534
+ _already_ bad still spends its retries — the stop applies only when the
8322
8535
  middleware is the cause.
8323
8536
 
8324
8537
  ### `.outputFallback()` says which door reaches its tiers
@@ -8405,8 +8618,8 @@ flowchartAsTool({
8405
8618
  name: 'weather_advice',
8406
8619
  description: 'Decide whether to bike tomorrow.',
8407
8620
  flowchart: adviceChart,
8408
- keepRecord: true, // ← off by default
8409
- keepRecordLimit: 20, // ← bounded LRU window (this is the default)
8621
+ keepRecord: true, // ← off by default
8622
+ keepRecordLimit: 20, // ← bounded LRU window (this is the default)
8410
8623
  redact: { keys: ['apiKey'] },
8411
8624
  });
8412
8625
  ```
@@ -8497,8 +8710,8 @@ artifacts by hand: `innerRunStore`, `innerRunsOf`, `mergeInnerRuns`,
8497
8710
  ### The demo
8498
8711
 
8499
8712
  `examples/features/50-through-the-tool-boundary.ts` — a weather-advice agent
8500
- whose ONE tool is a 4-stage footprintjs chart. Turn 1: *"Should I bike to work
8501
- in Chicago tomorrow?"* Turn 2: *"Why did you say it'll rain?"* — answered
8713
+ whose ONE tool is a 4-stage footprintjs chart. Turn 1: _"Should I bike to work
8714
+ in Chicago tomorrow?"_ Turn 2: _"Why did you say it'll rain?"_ — answered
8502
8715
  through visible `find_in_trace` → `inspect_tool_call` → `inspect_tool_run`
8503
8716
  calls that cite the inner stage (`validate-forecast#1`), the exact field
8504
8717
  (`rainChancePct = 82`) and the rule that consumed it. The chart's own stage
@@ -8519,8 +8732,8 @@ evidence existed, and the model had no way to reach it.
8519
8732
  ### `find_in_trace(query)` — free text in, step ids out
8520
8733
 
8521
8734
  Every other trace tool needs a name you already have: a step id, a state key, a
8522
- variable. But a follow-up question arrives in the user's words — *"why did you say
8523
- order 7712 was out of warranty?"* — and the model's only options were to guess a
8735
+ variable. But a follow-up question arrives in the user's words — _"why did you say
8736
+ order 7712 was out of warranty?"_ — and the model's only options were to guess a
8524
8737
  state key or read the whole narrative.
8525
8738
 
8526
8739
  This searches stage names and descriptions, state keys, every committed value and
@@ -8657,7 +8870,7 @@ await callTraceTool(tools, 'find_in_trace', { query: 'order 7712' });
8657
8870
 
8658
8871
  Pure: no engine, no agent, no I/O. Exported from `agentfootprint/observe` (and
8659
8872
  `agentfootprint/debug`). Honest about the two things a serialized run cannot carry
8660
- back — `controlDeps` is a lookup *function* and does not serialize (slices say
8873
+ back — `controlDeps` is a lookup _function_ and does not serialize (slices say
8661
8874
  `⚠ control edges unavailable`, the marker that already existed), and the narrative
8662
8875
  survives only if a narrative recorder was attached, since `recordRun` deliberately
8663
8876
  attaches none. Two teaching refusals name `recordRun` as the producer: a bundle with
@@ -8710,11 +8923,11 @@ cursor half and left the rule half standing, and the rule half is the bug: an en
8710
8923
  routed to `T`, `S`'s rule still matched and `S` and `T` were both active. Measured on
8711
8924
  a two-skill support graph:
8712
8925
 
8713
- | iteration | active | tool menu | skill bodies |
8714
- |---|---|---|---|
8715
- | 1 | `triage` | `read_skill`, `lookup_order` | triage |
8926
+ | iteration | active | tool menu | skill bodies |
8927
+ | --------------- | ------------------ | -------------------------------------------- | --------------- |
8928
+ | 1 | `triage` | `read_skill`, `lookup_order` | triage |
8716
8929
  | 2 — the handoff | `triage`, `refund` | `read_skill`, `lookup_order`, `issue_refund` | triage + refund |
8717
- | 3 | `triage`, `refund` | same | triage + refund |
8930
+ | 3 | `triage`, `refund` | same | triage + refund |
8718
8931
 
8719
8932
  Note iteration 3. This was never a one-iteration blip: with the cursor parked on
8720
8933
  `refund`, `triage`'s rule kept matching, so it came back and stayed. The overlap was
@@ -8722,8 +8935,8 @@ the steady state.
8722
8935
 
8723
8936
  **A conditional entry is now active exactly while the cursor is on it** — the same
8724
8937
  compiled expression a route target and an exclusive entry already used. One law for a
8725
- flat graph: *a skill is active iff the cursor is on it, or it declared itself
8726
- unconditional.* `when` chooses where a turn STARTS.
8938
+ flat graph: _a skill is active iff the cursor is on it, or it declared itself
8939
+ unconditional._ `when` chooses where a turn STARTS.
8727
8940
 
8728
8941
  This finishes 8.3.0 rather than reverting it. Both failures 8.3.0 named — a declared
8729
8942
  step INTO an entry skill, and a `read_skill` pick onto one — are carried by the cursor
@@ -8761,7 +8974,7 @@ superseded pick.
8761
8974
 
8762
8975
  `multi-entry-fanout` fired whenever a graph declared two or more entries, including
8763
8976
  when every one of them carried a `when` — a deterministic rule-router, which is a
8764
- taught shape. Worse, the advice it gave was *"give the extras a `when`"*, to entries
8977
+ taught shape. Worse, the advice it gave was _"give the extras a `when`"_, to entries
8765
8978
  that already had one. It computed which entries were unconditional and then used that
8766
8979
  only to soften the middle of the sentence.
8767
8980
 
@@ -8780,7 +8993,7 @@ why the check over-fired. Rewritten.
8780
8993
  A suppression the run cannot name is a silent drop. When a conditional entry's rule
8781
8994
  matched and the cursor law kept it off the wire, the entry's id is now reported on the
8782
8995
  per-iteration evaluation event, beside the `cursorMove` that says where the graph went
8783
- instead. Together they answer *"why isn't my entry loading?"* without anyone
8996
+ instead. Together they answer _"why isn't my entry loading?"_ without anyone
8784
8997
  re-running a predicate to guess.
8785
8998
 
8786
8999
  Omitted when nothing was suppressed, and for every non-skill-graph run — so an
@@ -8826,8 +9039,8 @@ build, and one changes a string on the wire. All eight are below.
8826
9039
  correct branch:
8827
9040
 
8828
9041
  - **same provider family** — it billed your MAIN model for every fold. The
8829
- refusal three lines above it in the same file promised *"the library will not
8830
- quietly bill your main model for compaction"*, and then did.
9042
+ refusal three lines above it in the same file promised _"the library will not
9043
+ quietly bill your main model for compaction"_, and then did.
8831
9044
  - **different provider** — it sent your agent's model id to a vendor that has
8832
9045
  never heard of it, so the fold died mid-run, on a paid run, in a file whose
8833
9046
  own header promises "everything fails at `.build()`, never mid-run".
@@ -8855,7 +9068,7 @@ Readings are now stamped with the iteration whose call produced them and expire
8855
9068
  one boundary later. An expired reading is `undefined`, which every strategy
8856
9069
  already treats as "do not act" — so a window strategy **stands down** instead of
8857
9070
  deciding on a number nobody took, and says so once on the console rather than
8858
- going quiet. "Counted, never guessed" has to mean counted *recently*.
9071
+ going quiet. "Counted, never guessed" has to mean counted _recently_.
8859
9072
 
8860
9073
  An agent whose provider reports usage reliably is unaffected.
8861
9074
 
@@ -8900,10 +9113,10 @@ comparison of two string lengths will not.
8900
9113
  Two emitters share this event name, this `slot: 'messages'` value, and — until
8901
9114
  now — one indistinguishable payload:
8902
9115
 
8903
- | emitter | counts | `unit` |
8904
- |---|---|---|
8905
- | the three context slots (`contextBudget`, **on by default**) | `String.length` | `'chars'` |
8906
- | a window strategy (`.window()` / `.compaction()`) | provider-reported input tokens | `'tokens'` |
9116
+ | emitter | counts | `unit` |
9117
+ | ------------------------------------------------------------ | ------------------------------ | ---------- |
9118
+ | the three context slots (`contextBudget`, **on by default**) | `String.length` | `'chars'` |
9119
+ | a window strategy (`.window()` / `.compaction()`) | provider-reported input tokens | `'tokens'` |
8907
9120
 
8908
9121
  So one subscriber routinely received both, and `cap 200, projected 258` could
8909
9122
  mean 258 characters or 258 tokens — a roughly 4× difference in the same field,
@@ -8927,8 +9140,8 @@ oversight — please do not "fix" them.
8927
9140
  ### `costBudget` can now stop the run
8928
9141
 
8929
9142
  `costBudget` was warn-only, while `commentaryTemplates.ts` narrated
8930
- *"{{appName}} hit a cost limit and stopped."* and `docs/monitor/deployment.mdx`
8931
- claimed the agent *"halts when the per-run USD budget is hit"*. It did neither.
9143
+ _"{{appName}} hit a cost limit and stopped."_ and `docs/monitor/deployment.mdx`
9144
+ claimed the agent _"halts when the per-run USD budget is hit"_. It did neither.
8932
9145
  `docs/monitor/observability.mdx`, on the same site, correctly said the library
8933
9146
  never auto-aborts.
8934
9147
 
@@ -9037,7 +9250,7 @@ version would make an older deployment refuse a session it can serve.
9037
9250
  **Governance never silently drops — and never silently invents.** Eight ways a
9038
9251
  rule you configured could decide nothing, and you could only find out by reading
9039
9252
  a quiet run. Seven are now refused at build time with a message that names the
9040
- fix; one was a rule that ran everywhere except the one path where a *person* had
9253
+ fix; one was a rule that ran everywhere except the one path where a _person_ had
9041
9254
  just typed the value.
9042
9255
 
9043
9256
  Two of these change what a run DOES. Both are called out below.
@@ -9109,7 +9322,7 @@ It now raises `DecisionRequiredError` (`code: 'ERR_DECISION_REQUIRED'`) at the
9109
9322
  API boundary. **Nothing executes and the checkpoint is unchanged**, so the same
9110
9323
  one can be answered properly and resumed again. The error names the gate
9111
9324
  (`gate: 'checkIn' | 'ask'`), the tool, the middleware that asked, and `received`
9112
- — the *shape* that arrived, never its contents, because a resume payload is
9325
+ — the _shape_ that arrived, never its contents, because a resume payload is
9113
9326
  caller data and an error message ends up in logs.
9114
9327
 
9115
9328
  Discriminated by the **pause**, never by the input, via the new
@@ -9191,9 +9404,9 @@ never stops your strategy. It now also carries two methods:
9191
9404
  ```ts
9192
9405
  const telemetry = agent.enable.observability({ strategy: cloudwatch });
9193
9406
 
9194
- await telemetry.flush(); // drain: driver queue first, then the buffer
9195
- telemetry(); // detach (unchanged)
9196
- telemetry.stop(); // release — timers, clients, buffers
9407
+ await telemetry.flush(); // drain: driver queue first, then the buffer
9408
+ telemetry(); // detach (unchanged)
9409
+ telemetry.stop(); // release — timers, clients, buffers
9197
9410
  ```
9198
9411
 
9199
9412
  `flush()` enforces the ORDER, which is the part no consumer could write from
@@ -9316,7 +9529,7 @@ of trying again. A drain that cannot finish must return, never retry forever.
9316
9529
 
9317
9530
  With `enable.observability({ detach })`, each export is scheduled onto a
9318
9531
  footprintjs detach driver. Scheduling happened inside a promise continuation,
9319
- so the detach handle reached footprintjs's registry a microtask *after* the
9532
+ so the detach handle reached footprintjs's registry a microtask _after_ the
9320
9533
  event was dispatched. `flushAllDetached()` drains until that registry is empty
9321
9534
  — and it was still empty when it looked.
9322
9535
 
@@ -9339,7 +9552,7 @@ the package either way.
9339
9552
  reach today (the declared type is still `Unsubscribe`); it exists so the
9340
9553
  no-subscription case is not the one path that breaks when that type widens.
9341
9554
  - Removed a dead loop condition in the CloudWatch drain (`lastFlushPromise !==
9342
- Promise.resolve()` compares against a freshly minted promise and is always
9555
+ Promise.resolve()` compares against a freshly minted promise and is always
9343
9556
  true).
9344
9557
 
9345
9558
  ## [8.11.0] - 2026-08-07
@@ -9376,7 +9589,7 @@ delivery — just an empty log group.
9376
9589
  The docstring for `logStreamName` had been promising `"Created on first put if
9377
9590
  it doesn't exist"` since the adapter shipped. It was never true. Worse, the
9378
9591
  convention the docs themselves recommended — `` `${HOSTNAME}/${Date.now()}` ``
9379
- — produces a name that *cannot* pre-exist, so following the documentation
9592
+ — produces a name that _cannot_ pre-exist, so following the documentation
9380
9593
  guaranteed the bug on every deploy. The only configuration that worked was the
9381
9594
  undocumented one.
9382
9595
 
@@ -9392,7 +9605,7 @@ instead of implying otherwise.
9392
9605
 
9393
9606
  The missing stream was one delivery failure. It turned out **every** delivery
9394
9607
  failure was silent: an IAM denial, a throttle, a rejected batch. Each adapter
9395
- installed its console fallback lazily *inside* its own `_onError` method — so
9608
+ installed its console fallback lazily _inside_ its own `_onError` method — so
9396
9609
  the delivery path, which read the hook rather than calling the method, found
9397
9610
  `undefined` and dropped the error on the floor. `cloudwatch`, `xray` and `otel`
9398
9611
  all had it.
@@ -9419,7 +9632,7 @@ fail. It is replaced by six that assert unconditionally.
9419
9632
 
9420
9633
  ### A knob the warning told you to turn, that did not exist
9421
9634
 
9422
- An over-budget context slot warned: *"Raise `budgetCap` on the slot config."*
9635
+ An over-budget context slot warned: _"Raise `budgetCap` on the slot config."_
9423
9636
  `budgetCap` was reachable from no public door. `buildMessagesSlot()` was called
9424
9637
  with no arguments at all four of its call sites, so its 10000-character cap was
9425
9638
  unreachable by construction. A warning you cannot act on is worse than no
@@ -9439,15 +9652,15 @@ budget is a signal, not a limiter. `LLMCallOptions` takes the same option
9439
9652
 
9440
9653
  A 429 is a **pre-execution rejection**: the rate limiter refused the request at
9441
9654
  the edge and the server never ran the tool, so a retry cannot double-execute
9442
- anything. That is exactly what is *not* true of a 500 or a timeout, where the
9655
+ anything. That is exactly what is _not_ true of a 500 or a timeout, where the
9443
9656
  call may have half-run and a retry could charge a card twice.
9444
9657
 
9445
9658
  That asymmetry is the entire license for this feature, so the policy is 429 and
9446
9659
  nothing else — pinned by a property test that walks twelve other statuses and a
9447
9660
  thrown transport error and asserts a single attempt for each. Managed gateways
9448
9661
  rate-limit per principal by design; without this, a designed and self-clearing
9449
- condition reached the model as a thrown tool error it reads as *"this tool is
9450
- broken"*, whereupon it apologises, picks another tool, or invents an answer.
9662
+ condition reached the model as a thrown tool error it reads as _"this tool is
9663
+ broken"_, whereupon it apologises, picks another tool, or invents an answer.
9451
9664
 
9452
9665
  It lives at the `fetch` seam because that is the only place `Retry-After` still
9453
9666
  exists — the MCP SDK reads the response, throws `StreamableHTTPError(status,
@@ -9474,18 +9687,22 @@ Per-attempt visibility is the `onRetry` callback — the contract `withRetry` an
9474
9687
  are consumer-called, the docs now say so, and a batching exporter loses its
9475
9688
  final batch and leaks its timer if you skip them:
9476
9689
  ```ts
9477
- process.on('SIGTERM', async () => { await telemetry.flush(); telemetry.stop(); stop(); });
9690
+ process.on('SIGTERM', async () => {
9691
+ await telemetry.flush();
9692
+ telemetry.stop();
9693
+ stop();
9694
+ });
9478
9695
  ```
9479
9696
  Wiring them into the framework lifecycle would change `run()` timing and
9480
9697
  misbehave for a strategy shared across two `enable` calls, so it is a design
9481
9698
  question on the ledger rather than a silent default.
9482
9699
  - **A Skill's tools are visible from iteration 1.** `DefineSkillOptions.tools`
9483
- said they were *"added to the tools slot once activated"*. They are added to
9700
+ said they were _"added to the tools slot once activated"_. They are added to
9484
9701
  the registry at build time; activation adds the Skill's **body**, not its
9485
9702
  tools. Gating is opt-in via `autoActivate: 'currentSkill'` (which
9486
9703
  `skillGraph().tree()` sets for you on every leaf) — and the docs said
9487
9704
  otherwise in twelve places, including a `process_refund` example claiming a
9488
- tool was *"locked away"*. That example now sets `autoActivate` and the prose
9705
+ tool was _"locked away"_. That example now sets `autoActivate` and the prose
9489
9706
  no longer implies a security boundary the default does not provide.
9490
9707
  - **`autoActivate` stopped calling itself a forward-compat marker** awaiting
9491
9708
  "v2.5 runtime wiring" — that wiring shipped in 2.5.0, six majors ago.
@@ -9495,7 +9712,7 @@ Per-attempt visibility is the `onRetry` callback — the contract `withRetry` an
9495
9712
  anything, and a lower tier is not a safer one: `'minimal'` still ships
9496
9713
  `agent.turn_start` (`userPrompt`), `agent.turn_end` (`finalContent`) and
9497
9714
  `agent.iteration_end` (the whole conversation `history[]`) — measured, it
9498
- carries user content in a *higher* share of its events than `'standard'` does.
9715
+ carries user content in a _higher_ share of its events than `'standard'` does.
9499
9716
  The docstring says this plainly now, points at `auditExport()` (bounded by
9500
9717
  default) and `otelObservability()` (omits `userPrompt`), and warns that
9501
9718
  `redactContent` does **not** apply to this channel — it operates on the
@@ -9589,12 +9806,12 @@ exists.
9589
9806
  `textLoader`, `markdownLoader` and `htmlLoader` need no dependency. `pdfLoader`
9590
9807
  needs one, and it was picked by measuring rather than by reputation:
9591
9808
 
9592
- | package | installed | packages | verdict |
9593
- |---|---|---|---|
9594
- | **`unpdf`** | **2.5 MB** | **1** | chosen — zero transitive deps, per-page text |
9595
- | `pdf-parse@2` | 86 MB | 3 | a native binary (`@napi-rs/canvas`), to read text |
9596
- | `pdf-parse@1` | 34 MB | 4 | unmaintained since 2018 |
9597
- | `pdfjs-dist@6` | 62 MB | 2 | 25× the size for the same engine |
9809
+ | package | installed | packages | verdict |
9810
+ | -------------- | ---------- | -------- | ------------------------------------------------- |
9811
+ | **`unpdf`** | **2.5 MB** | **1** | chosen — zero transitive deps, per-page text |
9812
+ | `pdf-parse@2` | 86 MB | 3 | a native binary (`@napi-rs/canvas`), to read text |
9813
+ | `pdf-parse@1` | 34 MB | 4 | unmaintained since 2018 |
9814
+ | `pdfjs-dist@6` | 62 MB | 2 | 25× the size for the same engine |
9598
9815
 
9599
9816
  It is an optional peer, lazily loaded, refusing with an install line when a PDF
9600
9817
  is actually met. Per-page text is why a PDF citation can name a page you can
@@ -9698,7 +9915,6 @@ the page its 150-character run-up borrowed from.
9698
9915
  - **`unpdf`** as a new optional peer. Only `pdfLoader` touches it, only when a
9699
9916
  PDF is actually read.
9700
9917
 
9701
-
9702
9918
  ## [8.9.0] - 2026-08-06
9703
9919
 
9704
9920
  **The durable index.** 8.8.0 made retrieval tell the truth about what it read.
@@ -9724,8 +9940,8 @@ await indexDocuments(store, embedder, docs, { embedderId: embedder.id });
9724
9940
  Embedding cost is not one number, and the split is the whole argument for a
9725
9941
  file. **Index time** embeds the corpus: once, scaling with how much you store.
9726
9942
  **Query time** embeds the user's question: per retrieval, scaling with traffic.
9727
- A 10,000-chunk corpus is 10,000 embeddings *once* and one per question
9728
- thereafter — with a `Map` it is 10,000 embeddings *per restart*.
9943
+ A 10,000-chunk corpus is 10,000 embeddings _once_ and one per question
9944
+ thereafter — with a `Map` it is 10,000 embeddings _per restart_.
9729
9945
 
9730
9946
  `agentfootprint.embedding.generated` has carried an `inputKind: 'document' |
9731
9947
  'query'` field since 2.x and nothing ever emitted it, so any dashboard built
@@ -9748,13 +9964,13 @@ top-K or it does not answer.
9748
9964
 
9749
9965
  Measured against this implementation on Node 22.16, Apple silicon:
9750
9966
 
9751
- | corpus | query | resident matrix | file | first search (hydration) |
9752
- |---|---|---|---|---|
9753
- | 10,000 × 384-d | 6 ms | 15 MB | 21 MB | 45 ms |
9754
- | 50,000 × 384-d | 31 ms | 77 MB | 105 MB | 251 ms |
9755
- | 100,000 × 384-d | 65 ms | 154 MB | 211 MB | 939 ms |
9756
- | 10,000 × 1536-d | 16 ms | 61 MB | 83 MB | 122 ms |
9757
- | 50,000 × 1536-d | 89 ms | 307 MB | 413 MB | **5.7 s** |
9967
+ | corpus | query | resident matrix | file | first search (hydration) |
9968
+ | --------------- | ----- | --------------- | ------ | ------------------------ |
9969
+ | 10,000 × 384-d | 6 ms | 15 MB | 21 MB | 45 ms |
9970
+ | 50,000 × 384-d | 31 ms | 77 MB | 105 MB | 251 ms |
9971
+ | 100,000 × 384-d | 65 ms | 154 MB | 211 MB | 939 ms |
9972
+ | 10,000 × 1536-d | 16 ms | 61 MB | 83 MB | 122 ms |
9973
+ | 50,000 × 1536-d | 89 ms | 307 MB | 413 MB | **5.7 s** |
9758
9974
 
9759
9975
  **The documented ceiling is 50,000 chunks** — under 100 ms per query at every
9760
9976
  embedder this library ships, under ~300 MB resident. It degrades linearly to
@@ -9837,13 +10053,12 @@ safe to answer with "no matches".**
9837
10053
  - The BLOB on disk keeps the **original** vector, so `get`/`list` round-trip
9838
10054
  exactly what was written; normalisation happens once, into the resident
9839
10055
  matrix, so search is a dot product without changing what is stored.
9840
- - Found while writing the schema-identity check: it originally ran *after* the
10056
+ - Found while writing the schema-identity check: it originally ran _after_ the
9841
10057
  indexes were created, so a foreign `af_vectors` table failed on a missing
9842
10058
  column and was reported as `'cannot-open'` — the right refusal for the wrong
9843
10059
  reason, telling the reader to check file permissions when the real problem was
9844
10060
  that the file belonged to something else. It runs before them now.
9845
10061
 
9846
-
9847
10062
  ## [8.8.0] - 2026-08-06
9848
10063
 
9849
10064
  **Retrieval tells the truth.** A retrieval computed a cosine score for every candidate
@@ -9878,12 +10093,14 @@ the two are registered separately, each with its own store:
9878
10093
  ```ts
9879
10094
  const agent = Agent.create({ provider })
9880
10095
  .rag(defineRAG({ id: 'product-docs', store: corpusStore, embedder }))
9881
- .memory(defineMemory({
9882
- id: 'chat',
9883
- type: MEMORY_TYPES.EPISODIC,
9884
- strategy: { kind: MEMORY_STRATEGIES.WINDOW, size: 10 },
9885
- store: conversationStore,
9886
- }))
10096
+ .memory(
10097
+ defineMemory({
10098
+ id: 'chat',
10099
+ type: MEMORY_TYPES.EPISODIC,
10100
+ strategy: { kind: MEMORY_STRATEGIES.WINDOW, size: 10 },
10101
+ store: conversationStore,
10102
+ }),
10103
+ )
9887
10104
  .build();
9888
10105
  ```
9889
10106
 
@@ -9905,7 +10122,7 @@ namespace it reads from — defaulting to the same `'_global'` the indexer write
9905
10122
  index with no options and retrieve with no options and the documents are found. Pass it
9906
10123
  explicitly for a per-tenant corpus, on both sides.
9907
10124
 
9908
- And a namespace that holds nothing is now *reported* rather than answered around:
10125
+ And a namespace that holds nothing is now _reported_ rather than answered around:
9909
10126
  `corpusEmpty: true` on the retrieval event, plus a once-per-process warning naming the
9910
10127
  namespace it searched and the usual cause.
9911
10128
 
@@ -9924,7 +10141,7 @@ namespace it searched and the usual cause.
9924
10141
  are admitted**: `search` returns score-descending, so either the whole pool clears the
9925
10142
  floor (admitted = first `k`, as before) or some entry fails it (every later entry fails
9926
10143
  too, so the pool already holds every entry that clears it). `rejectWindow` only controls
9927
- how many near-misses can be *shown*.
10144
+ how many near-misses can be _shown_.
9928
10145
  - **`agentfootprint.memory.retrieved`** (new, 72 typed events) — one per retrieval,
9929
10146
  carrying every candidate. `candidates: undefined` means the store ranked server-side
9930
10147
  and returned nothing comparable; it never means there were none.
@@ -9965,7 +10182,7 @@ recency ordering the best-scoring chunk can land last.
9965
10182
  ### Chunks the model can cite
9966
10183
 
9967
10184
  A retrieved page of a PDF rendered as `<memory role="unknown" turn="0">` under the header
9968
- *"Relevant context from prior conversations"* — three claims that were not true of a
10185
+ _"Relevant context from prior conversations"_ — three claims that were not true of a
9969
10186
  document, and no way to cite it. `defineRAG` renders a corpus as what it is:
9970
10187
 
9971
10188
  ```text
@@ -10017,13 +10234,12 @@ nothing to configure.
10017
10234
  `core/`; a second implementation is how two recordings of the same bytes end up
10018
10235
  disagreeing about their id.
10019
10236
 
10020
-
10021
10237
  ## [8.7.0] - 2026-08-06
10022
10238
 
10023
10239
  **The check-up stops being quiet, and a dead option stops pretending.** 8.4.0 stopped a
10024
10240
  skill graph from throwing away what the author declared; 8.5.0 stopped it telling the
10025
10241
  model things that were not so. This one is about the configurations the library
10026
- *watched you build and said nothing about* — an entry menu with no way to choose from
10242
+ _watched you build and said nothing about_ — an entry menu with no way to choose from
10027
10243
  it, a transition the cursor can never take, a tool name two sources claim, a scoped
10028
10244
  tool provider that returns nothing forever. Nine findings, one shape: the library knew,
10029
10245
  and did not say.
@@ -10031,7 +10247,7 @@ and did not say.
10031
10247
  ### An entry menu with no way to choose from it
10032
10248
 
10033
10249
  Declare two entries and no `.entryBy()` / `.entryByRead()`, and both of them load on
10034
- every call. An entry's compiled trigger is cursor-*independent* — no `when` compiles to
10250
+ every call. An entry's compiled trigger is cursor-_independent_ — no `when` compiles to
10035
10251
  `{ kind: 'always' }` — while exactly ONE of them can be the cursor: the first whose
10036
10252
  `when` passes. So the extras pay for their body and their tools on every iteration and
10037
10253
  route nothing, which is the opposite of what a skill graph is for.
@@ -10077,7 +10293,7 @@ all versus only bare edges incoming.
10077
10293
 
10078
10294
  ### `unreachable-skill` is told per trigger kind
10079
10295
 
10080
- The sentence *"it can only be reached by the model via read_skill"* is true for an
10296
+ The sentence _"it can only be reached by the model via read_skill"_ is true for an
10081
10297
  `llm-activated` trigger and for no other kind — `Agent.openSkillIds()` admits an open
10082
10298
  pick only for that one. But `deriveTrigger` returns null for an unwired skill, so a
10083
10299
  skill that arrived carrying a hand-authored `rule` trigger **kept it**, and the warning
@@ -10147,7 +10363,7 @@ const graphScoped = (id: string, tools: Tool[]): ToolProvider => ({
10147
10363
  - **`ToolDispatchContext.activeSkillIds`** — the real active set for this iteration.
10148
10364
  Optional, so a provider written before 8.7.0 sees `undefined` and behaves as it did.
10149
10365
  - **`agentfootprint.tools.shadowed`** (71 typed events now) — `{ toolName, iteration,
10150
- schemaFrom, schemaFromId?, dispatchTo, dispatchToId? }`. Names only: never args, never
10366
+ schemaFrom, schemaFromId?, dispatchTo, dispatchToId? }`. Names only: never args, never
10151
10367
  results, never a description body.
10152
10368
  - **`skillScopedToolsTarget` / `SKILL_SCOPED_TOOLS_ID_PREFIX`** — the provider-id
10153
10369
  convention, readable by anyone composing providers.
@@ -10165,7 +10381,7 @@ const graphScoped = (id: string, tools: Tool[]): ToolProvider => ({
10165
10381
  object-literal form since 8.4.0. **Behavior change.** A fluent graph with an
10166
10382
  error-level problem — `no-entry` or `unknown-skill`, i.e. a graph that cannot start a
10167
10383
  turn at all — built in silence outside dev mode and surfaced as a run that entered no
10168
- skill. What still builds: every graph whose check-up has no *error* (warnings never
10384
+ skill. What still builds: every graph whose check-up has no _error_ (warnings never
10169
10385
  throw, however many); every call passing `check: 'warn'` explicitly, which still never
10170
10386
  throws, so the mode keeps its name and its meaning; `check: 'off'` skips entirely.
10171
10387
  Only code that was already shipping a graph the library could not start is affected.
@@ -10174,7 +10390,7 @@ const graphScoped = (id: string, tools: Tool[]): ToolProvider => ({
10174
10390
  activates the skill — and no such tool has ever been built. The evaluator activates an
10175
10391
  `llm-activated` skill by matching `ctx.activatedInjectionIds`, which only `read_skill`
10176
10392
  writes, and it has never read the field. A skill declaring `viaToolName:
10177
- 'open_playbook'` activated through `read_skill` exactly like every other skill, so the
10393
+ 'open_playbook'` activated through `read_skill` exactly like every other skill, so the
10178
10394
  declaration described a door that does not exist. Nothing that worked stops working;
10179
10395
  a silent no-op becomes a named one, at `Agent.injection()` — the one funnel `.skill()`,
10180
10396
  `.skills()`, `.skillGraph()`, `skillsFromDir()` and a hand-built Injection all pass
@@ -10247,7 +10463,7 @@ it raises `CredentialConsentRequiredError`.
10247
10463
  back `authorization-required`.
10248
10464
  - **`CredentialConsentRequiredError`** (`ERR_CREDENTIAL_CONSENT_REQUIRED`, from
10249
10465
  `agentfootprint/identity`) — carries `service`, `sessionId`, `authorizationUrl`,
10250
- `tool` and `iteration`. The error *message* deliberately omits the URL, because
10466
+ `tool` and `iteration`. The error _message_ deliberately omits the URL, because
10251
10467
  a message is the one string that reliably reaches a log line.
10252
10468
  - **`pauseData.authorization`** — `{ service, authorizationUrl, sessionId }` on a
10253
10469
  consent pause, surfaced by `standingAgent` as `PendingAsk.pauseData`. The
@@ -10275,7 +10491,7 @@ it raises `CredentialConsentRequiredError`.
10275
10491
  This is the mirror image of a guarantee the library kept carefully everywhere
10276
10492
  else. `agentfootprint.credential.authorization_required` was designed to carry
10277
10493
  `{ service, sessionId }` and never the URL; OTel and X-Ray record the tool
10278
- result's *type* and never its value; the audit bundle's default `bounded` mode
10494
+ result's _type_ and never its value; the audit bundle's default `bounded` mode
10279
10495
  maps `tool_end.result` to `[type: string]`. Every observer channel was
10280
10496
  disciplined. The one channel nobody thought of as an observer — the
10281
10497
  conversation — was not, and it feeds all the others.
@@ -10415,7 +10631,7 @@ that gave the middleware-ask outcome union no `result` arm.
10415
10631
  ## [8.5.0] - 2026-08-06
10416
10632
 
10417
10633
  **`read_skill` tells the whole truth.** 8.4.0 stopped a skill graph from throwing
10418
- away what the author declared. This one stops it from telling the *model* things
10634
+ away what the author declared. This one stops it from telling the _model_ things
10419
10635
  that were not so. Five findings, all of the same shape: the library said a thing had
10420
10636
  happened, or offered a thing it would refuse, or recorded a cause that was not the
10421
10637
  cause. One is a build-time refusal, one is a gate refusal, three are fixes.
@@ -10424,8 +10640,8 @@ cause. One is a build-time refusal, one is a gate refusal, three are fixes.
10424
10640
 
10425
10641
  A `tree()` routes by predicate on every iteration. It has no cursor, so `read_skill`
10426
10642
  has nothing to move. But `graph.reachableSkills()` reported **all the leaves**, so
10427
- the gate accepted a leaf pick and `read_skill` answered *"Skill 'x' activated for the
10428
- next iteration"* — and nothing happened. A leaf compiles to a `rule` trigger; a
10643
+ the gate accepted a leaf pick and `read_skill` answered _"Skill 'x' activated for the
10644
+ next iteration"_ — and nothing happened. A leaf compiles to a `rule` trigger; a
10429
10645
  `read_skill` call writes only `activatedInjectionIds`; no `rule` trigger reads that.
10430
10646
  The leaf never activated, the tree re-decided by predicate, and the run then emitted
10431
10647
  `agentfootprint.skill.reroute_superseded` naming a winner that **did not exist** —
@@ -10451,7 +10667,7 @@ though the tool accepted the name. Answer with the skill the tree routed to, or
10451
10667
  **Behavior change:** `graph.reachableSkills()` now returns `[]` for a decision
10452
10668
  `tree()`, from every cursor. Its contract is "what `read_skill` may jump to", and
10453
10669
  all-leaves was the lie; use `graph.skills` to enumerate leaves, which is what it was
10454
- always for. `read_skill` is not dead under a tree — anything registered *beside* the
10670
+ always for. `read_skill` is not dead under a tree — anything registered _beside_ the
10455
10671
  graph (`.skill(x)`, `.skills(reg)`, `.selfExplain()`) is **open** and still admitted
10456
10672
  from anywhere, because those really do activate by `read_skill`. Two docstrings that
10457
10673
  promised "read_skill stays a full escape hatch there" are corrected.
@@ -10477,7 +10693,7 @@ Not reachable from here (read_skill for these will be refused):
10477
10693
  ```
10478
10694
 
10479
10695
  **The enum stays the full catalog, deliberately.** `toolArgValidation` defaults to
10480
- `'enforce'` and runs *before* the gate; an off-enum id is rejected with a generic
10696
+ `'enforce'` and runs _before_ the gate; an off-enum id is rejected with a generic
10481
10697
  schema error and never reaches it. Narrowing the enum would therefore have retired
10482
10698
  the gate's teaching refusal, the `agentfootprint.skill.rejected` event,
10483
10699
  `routeRecorder`'s rejection hops and the rejected-cap governor's only input — four
@@ -10511,7 +10727,7 @@ Use 'both' (system prompt AND tool result) or 'system-prompt'.
10511
10727
  ```
10512
10728
 
10513
10729
  Refusal rather than a quiet fall back to the system slot: the author wrote
10514
- `'tool-only'` to keep the body *out* of the system prompt, and silently putting it
10730
+ `'tool-only'` to keep the body _out_ of the system prompt, and silently putting it
10515
10731
  back would honour the activation while breaking the declaration — a different lie,
10516
10732
  not a fix. `'both'` already means "deliver it either way".
10517
10733
 
@@ -10539,7 +10755,7 @@ drift:
10539
10755
  - `routeRecorder()` reads it, and a `'model-pick'` hop carries **no** `edgeLabel`.
10540
10756
 
10541
10757
  This settles the one case no observer could reconstruct: an edge and a same-turn pick
10542
- naming the *same* skill resolves to `'route'` (`D1 > D2`), and only the resolver
10758
+ naming the _same_ skill resolves to `'route'` (`D1 > D2`), and only the resolver
10543
10759
  knows. Without `cursorMove` (an older graph, an older recording) the previous
10544
10760
  inference still stands.
10545
10761
 
@@ -10574,11 +10790,11 @@ refusal on the agent, and the gate one is a fix, not a refusal.
10574
10790
  cannot move the graph somewhere the graph doesn't go. That set is about the CURSOR,
10575
10791
  but it was being used as the whole catalog, so three shapes were dead:
10576
10792
 
10577
- | you wrote | before | now |
10578
- |---|---|---|
10579
- | `.skillGraph(g).selfExplain()` | `read_skill('self-explain')` rejected on every call — the debug skill and its six trace tools could never load | activates; the trace tools reach the model on the next iteration |
10580
- | `.skillGraph(g).skill(x)` / `.skills(reg)` | `x` was listed in `read_skill`'s own menu and refused every time; its body was unreachable | activates |
10581
- | `skillGraph({ skills: [..., x] })` with `x` wired to nothing | refused — while its check-up warning said *"it can only be reached by the model via read_skill"* | activates; the warning is true again |
10793
+ | you wrote | before | now |
10794
+ | ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
10795
+ | `.skillGraph(g).selfExplain()` | `read_skill('self-explain')` rejected on every call — the debug skill and its six trace tools could never load | activates; the trace tools reach the model on the next iteration |
10796
+ | `.skillGraph(g).skill(x)` / `.skills(reg)` | `x` was listed in `read_skill`'s own menu and refused every time; its body was unreachable | activates |
10797
+ | `skillGraph({ skills: [..., x] })` with `x` wired to nothing | refused — while its check-up warning said _"it can only be reached by the model via read_skill"_ | activates; the warning is true again |
10582
10798
 
10583
10799
  A skill is **open** when its trigger is `llm-activated` (the trigger `read_skill`
10584
10800
  actually activates — a rule-gated injection is still refused, because admitting it
@@ -10703,7 +10919,7 @@ dropped pick is reported rather than swallowed — see the new event below.
10703
10919
  ### `agentfootprint.skill.reroute_superseded` (new typed event — 70 total)
10704
10920
 
10705
10921
  Fires in exactly one case: a `read_skill` the gate accepted did not end up active
10706
- because a declared edge won the same turn (the model emitted a domain tool *and*
10922
+ because a declared edge won the same turn (the model emitted a domain tool _and_
10707
10923
  `read_skill` in one message). Payload: `{ volunteeredId, wonId, fromSkillId,
10708
10924
  iteration }`. It is derived from the real active set, not from which clause won,
10709
10925
  so it cannot fire for a pick that did take effect.
@@ -10747,7 +10963,7 @@ never engages any of this.
10747
10963
 
10748
10964
  **Durable compaction.** An agent that has been up for a week folds week one
10749
10965
  into a summary. Then it gets deployed over. It comes back, is handed the same
10750
- conversation — and now it can still tell you what week one was about, *and*
10966
+ conversation — and now it can still tell you what week one was about, _and_
10751
10967
  show you week one, word for word.
10752
10968
 
10753
10969
  The window half of that already worked: a summary is an ordinary message, so
@@ -10818,14 +11034,14 @@ surprise.
10818
11034
 
10819
11035
  ### New exports
10820
11036
 
10821
- | export | what it is |
10822
- | --- | --- |
10823
- | `foldedSpanFor(conversation, message)` | The span behind one summary, joined by **content fingerprint** rather than index — a later fold swallows an earlier summary and every index after it moves. `undefined` means "no fold was recorded for this message", never "there were no originals". |
10824
- | `foldedMessages(conversation)` | Every retained message from every span, oldest fold first. |
10825
- | `FoldedSpan` · `CompactionRetention` · `FoldedConversation` | The types. |
11037
+ | export | what it is |
11038
+ | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
11039
+ | `foldedSpanFor(conversation, message)` | The span behind one summary, joined by **content fingerprint** rather than index — a later fold swallows an earlier summary and every index after it moves. `undefined` means "no fold was recorded for this message", never "there were no originals". |
11040
+ | `foldedMessages(conversation)` | Every retained message from every span, oldest fold first. |
11041
+ | `FoldedSpan` · `CompactionRetention` · `FoldedConversation` | The types. |
10826
11042
 
10827
11043
  The fingerprint is also what makes the join **forgery-proof**: `isCompactedSummary`
10828
- answers "this *looks* like a frame", which is all a prefix check can see, and a
11044
+ answers "this _looks_ like a frame", which is all a prefix check can see, and a
10829
11045
  model that copies the frame's opening words passes it. `foldedSpanFor` answers
10830
11046
  the stronger question — different content, different fingerprint, no match.
10831
11047
 
@@ -10886,14 +11102,14 @@ openai({ baseURL: 'http://localhost:11434/v1', apiKey: 'ollama' });
10886
11102
 
10887
11103
  Two things go wrong with a local runtime, and each has a one-command answer. Both
10888
11104
  now raise a typed `OllamaUnavailableError` (discriminated by `reason`) whose
10889
- message *is* the instruction — never a raw `ECONNREFUSED`, never a bare `404`,
11105
+ message _is_ the instruction — never a raw `ECONNREFUSED`, never a bare `404`,
10890
11106
  and never a hang: a deadline bounds the wait for the daemon to answer (not
10891
11107
  generation, so a slow model is untouched).
10892
11108
 
10893
11109
  - **Daemon not running** — names the address it tried, `ollama serve`, the
10894
11110
  install link, and how to point somewhere else.
10895
11111
  - **Model not pulled** — names `ollama pull <model>`, and asks `/api/tags` so it
10896
- can also list what this machine *does* have.
11112
+ can also list what this machine _does_ have.
10897
11113
 
10898
11114
  ### Also in this release
10899
11115
 
@@ -10902,7 +11118,7 @@ generation, so a slow model is untouched).
10902
11118
  compaction and cost budgets work against a local model.
10903
11119
  - **Thinking blocks from local reasoning models.** `ollama('deepseek-r1', { think: true })`
10904
11120
  asks Ollama to lift reasoning out of the answer; the new `ollamaThinkingHandler`
10905
- auto-wires by provider name and normalizes it. When a model was *not* asked and
11121
+ auto-wires by provider name and normalizes it. When a model was _not_ asked and
10906
11122
  writes `<think>…</think>` into the answer instead, the library **recognizes the
10907
11123
  shape and surfaces the blocks, but does not edit the answer** — silently
10908
11124
  rewriting model output is a change of meaning, and that belongs to the
@@ -10948,32 +11164,32 @@ The doors are now named for the job:
10948
11164
 
10949
11165
  **Every old import path still works, unchanged, for all of 8.x.** They are
10950
11166
  marked `@deprecated` so your editor points at the new door; nothing is logged,
10951
- nothing breaks, and each one re-exports the *same symbols* the door carries —
11167
+ nothing breaks, and each one re-exports the _same symbols_ the door carries —
10952
11168
  not copies. `test/api-conformance/door-aliases.test.ts` drives the TypeScript
10953
11169
  checker over the shipped `.d.ts` files to prove it, name by name, so the
10954
11170
  aliases cannot drift. They are removed in 9.0.0.
10955
11171
 
10956
11172
  ### Migration
10957
11173
 
10958
- | you were importing from | import from |
10959
- |---|---|
10960
- | `agentfootprint/llm-providers` | `agentfootprint/providers` |
10961
- | `agentfootprint/embedders` | `agentfootprint/providers` |
10962
- | `agentfootprint/tool-providers` | `agentfootprint/providers` |
10963
- | `agentfootprint/thinking` | `agentfootprint/providers` |
10964
- | `agentfootprint/memory-providers` | `agentfootprint/memory` |
10965
- | `agentfootprint/observability-providers` | `agentfootprint/observe` |
10966
- | `agentfootprint/strategies` | `agentfootprint/observe` |
10967
- | `agentfootprint/stream` | `agentfootprint/observe` |
10968
- | `agentfootprint/status` | `agentfootprint/observe` |
10969
- | `agentfootprint/locales` | `agentfootprint/observe` |
10970
- | `agentfootprint/debug` | `agentfootprint/observe` |
10971
- | `agentfootprint/debug/finders` | `agentfootprint/observe` |
10972
- | `agentfootprint/observability/contextError/finders` | `agentfootprint/observe` |
10973
- | `agentfootprint/reliability` | `agentfootprint/resilience` |
10974
- | `agentfootprint/hosting-providers` | `agentfootprint/hosting` |
10975
- | `agentfootprint/injection-engine` | `agentfootprint/context` |
10976
- | `agentfootprint/identity` | `agentfootprint/security` |
11174
+ | you were importing from | import from |
11175
+ | --------------------------------------------------- | --------------------------- |
11176
+ | `agentfootprint/llm-providers` | `agentfootprint/providers` |
11177
+ | `agentfootprint/embedders` | `agentfootprint/providers` |
11178
+ | `agentfootprint/tool-providers` | `agentfootprint/providers` |
11179
+ | `agentfootprint/thinking` | `agentfootprint/providers` |
11180
+ | `agentfootprint/memory-providers` | `agentfootprint/memory` |
11181
+ | `agentfootprint/observability-providers` | `agentfootprint/observe` |
11182
+ | `agentfootprint/strategies` | `agentfootprint/observe` |
11183
+ | `agentfootprint/stream` | `agentfootprint/observe` |
11184
+ | `agentfootprint/status` | `agentfootprint/observe` |
11185
+ | `agentfootprint/locales` | `agentfootprint/observe` |
11186
+ | `agentfootprint/debug` | `agentfootprint/observe` |
11187
+ | `agentfootprint/debug/finders` | `agentfootprint/observe` |
11188
+ | `agentfootprint/observability/contextError/finders` | `agentfootprint/observe` |
11189
+ | `agentfootprint/reliability` | `agentfootprint/resilience` |
11190
+ | `agentfootprint/hosting-providers` | `agentfootprint/hosting` |
11191
+ | `agentfootprint/injection-engine` | `agentfootprint/context` |
11192
+ | `agentfootprint/identity` | `agentfootprint/security` |
10977
11193
 
10978
11194
  `agentfootprint`, `agentfootprint/memory`, `agentfootprint/observe`,
10979
11195
  `agentfootprint/security`, `agentfootprint/hosting` and
@@ -10987,7 +11203,7 @@ aliases cannot drift. They are removed in 9.0.0.
10987
11203
  executing those registrations and carrying them in every bundle. Side-effectful
10988
11204
  code stays behind its own plainly-named door.
10989
11205
  - **`agentfootprint/events`** is not folded into `/observe`. It is the typed
10990
- wire vocabulary observers *read*, not a tool for watching — and concretely,
11206
+ wire vocabulary observers _read_, not a tool for watching — and concretely,
10991
11207
  its `ContextSource` (the injection-flavour union: `'rag' | 'skill' | …`) is a
10992
11208
  completely different type from the `ContextSource` `/observe` already carries
10993
11209
  (the context-bisect record). Two incompatible shapes cannot share a door.
@@ -11032,7 +11248,7 @@ runtime door, and it still returns the `Unsubscribe` you own.
11032
11248
  `CombinedRecorder` keeps its export too.
11033
11249
 
11034
11250
  There is deliberately **no** `WATCH_MOMENTS`. `.act()`'s keys are a closed,
11035
- compiler-pinned list because a rule has to be *told* where it may speak; an
11251
+ compiler-pinned list because a rule has to be _told_ where it may speak; an
11036
11252
  observer attends the whole stream, and a list we published would be a
11037
11253
  vocabulary we then had to keep true against every event ever added.
11038
11254
 
@@ -11068,7 +11284,7 @@ vocabulary we then had to keep true against every event ever added.
11068
11284
  ## [7.28.0] - 2026-08-05
11069
11285
 
11070
11286
  A paused agent is a promise you made to a person. Until this release the library
11071
- handed you that promise as JSON and wished you luck: *store it anywhere.*
11287
+ handed you that promise as JSON and wished you luck: _store it anywhere._
11072
11288
  Anywhere was the whole of the offer.
11073
11289
 
11074
11290
  `sqliteSessions({ file })` is the first battery included — the same
@@ -11091,7 +11307,7 @@ and the next step up was "bring a Redis" — a service to run, secure, back up a
11091
11307
  pay for, to keep a few kilobytes of chat. Everyone in between wrote the same
11092
11308
  little file store themselves and each one re-decided what a half-written file
11093
11309
  means. A pause had no home at all: a question outstanding is the one piece of
11094
- agent state that *must* outlive the process, because the answer arrives on human
11310
+ agent state that _must_ outlive the process, because the answer arrives on human
11095
11311
  time — after lunch, after the deploy, tomorrow. Both land in one table here,
11096
11312
  because `CheckpointEnvelope` was already a union of the two and a session store
11097
11313
  has no business caring which half it is holding.
@@ -11101,8 +11317,7 @@ process (or a few) on ONE machine, writing ONE file. It survives anything that
11101
11317
  ends the process and leaves the disk alone. It is **not** a distributed store:
11102
11318
  two machines do not share a session by both opening a file over a network
11103
11319
  filesystem. WAL gives many readers plus **one writer at a time**, and that is
11104
- the ceiling — a second writer waits for the lock up to `busyTimeoutMs` (default
11105
- 5000) and then fails loudly rather than queueing forever. When you outgrow it,
11320
+ the ceiling — a second writer waits for the lock up to `busyTimeoutMs` (default 5000) and then fails loudly rather than queueing forever. When you outgrow it,
11106
11321
  one argument to `standingAgent` changes and nothing above it moves.
11107
11322
 
11108
11323
  **A refusal where a fallback would have been easier.** `node:sqlite` ships with
@@ -11118,7 +11333,7 @@ user.
11118
11333
  **"Unreadable is not absent", one level up.** The envelope law already said an
11119
11334
  unreadable stored conversation and an absent one are different facts, and only
11120
11335
  one is safe to answer with a fresh start. A file store can break that promise
11121
- higher up — point it at a log file and a careless adapter opens it as an *empty*
11336
+ higher up — point it at a log file and a careless adapter opens it as an _empty_
11122
11337
  store. So the file is checked at construction and refused with
11123
11338
  `UnreadableSessionFileError`, whose `problem` field is the fact to branch on:
11124
11339
  `'cannot-open'`, `'not-our-schema'` (somebody else's table of that name), or
@@ -11129,7 +11344,7 @@ session that was never written hydrates as `undefined`.
11129
11344
  well as fields inside the JSON, so during an incident `sqlite3` answers "which
11130
11345
  sessions are waiting on a person, and since when?" with no JSON parser and
11131
11346
  without this library. `journalMode` on the returned store reports what the file
11132
- *actually got* rather than what was asked for — a silent downgrade from WAL on a
11347
+ _actually got_ rather than what was asked for — a silent downgrade from WAL on a
11133
11348
  network filesystem is the kind of thing only ever discovered under load.
11134
11349
 
11135
11350
  Added, all on the existing `agentfootprint/hosting` door — no new subpath:
@@ -11161,7 +11376,7 @@ the machine took, and the machine is shared: the suite runs beside a build, a
11161
11376
  coverage pass, and two other vitest workers. Identical code takes three to five
11162
11377
  times longer under that load with nothing about the code having changed. So the
11163
11378
  assertion cannot tell "we got slower" from "the box was busy", and it fires
11164
- *exactly* when CI is busiest. Five of them had already been logged as flakes —
11379
+ _exactly_ when CI is busiest. Five of them had already been logged as flakes —
11165
11380
  `xray` P6, `withCircuitBreaker` P6, `locales/messages` Block D,
11166
11381
  `consumer-domain-events`, `SkillRegistryOptions` — always under concurrent
11167
11382
  build load, always passing in isolation. The failure mode of a guard nobody
@@ -11205,7 +11420,7 @@ still flaked when actually run under the reproduction condition. Three things
11205
11420
  had to be added, each because the proof run said so:
11206
11421
 
11207
11422
  - **Repeat until the sample is worth timing.** Below a scheduler quantum, one
11208
- preemption *is* the measurement: a 0.2ms operation that gets descheduled
11423
+ preemption _is_ the measurement: a 0.2ms operation that gets descheduled
11209
11424
  reads as a hundred times its real cost, while the 100ms operation beside it
11210
11425
  absorbs the same theft as a rounding error — and load stops cancelling. Each
11211
11426
  operation is now repeated inside one sample until the sample clears 20ms, and
@@ -11237,8 +11452,8 @@ load, which is what makes them worth asserting.
11237
11452
  **No perf claim was deleted. The form changed; the meaning stayed** — and in
11238
11453
  four places the meaning got sharper, because writing the claim down properly
11239
11454
  exposed what it had actually been asserting. Three sites keep a millisecond
11240
- ceiling on purpose and say so at the site: they are stated against a *configured
11241
- delay* (a mock's own thinking band, a slow branch's own timeout, a strategy's
11455
+ ceiling on purpose and say so at the site: they are stated against a _configured
11456
+ delay_ (a mock's own thinking band, a slow branch's own timeout, a strategy's
11242
11457
  own per-event block) rather than against a guess about the machine, because
11243
11458
  "did not sleep longer than it was told to" has no cheaper form.
11244
11459
 
@@ -11394,8 +11609,8 @@ each of those is now the failure of the thing that caused it. A 400, a 500, one
11394
11609
  refused upgrade, one ended conversation. Never the process.
11395
11610
 
11396
11611
  One of them was not even a throw. `serveOne`'s promise is held in a Set and
11397
- voided at the call site, so anything that escaped it was an *unhandled
11398
- rejection* — which on node's defaults is the same dead container reached by a
11612
+ voided at the call site, so anything that escaped it was an _unhandled
11613
+ rejection_ — which on node's defaults is the same dead container reached by a
11399
11614
  different road. It is total by construction now, and says so.
11400
11615
 
11401
11616
  **The audit's other finding is that the conversation door was already safe, and
@@ -11412,17 +11627,16 @@ serve. It is a great deal of ceremony when all you wanted was a `/debug/trace`
11412
11627
  beside the agent on the one port the container was given.
11413
11628
 
11414
11629
  ```ts
11415
- nodeHost({ port: 8080, onUnhandled: (req, res) => myRouter(req, res) })
11630
+ nodeHost({ port: 8080, onUnhandled: (req, res) => myRouter(req, res) });
11416
11631
  ```
11417
11632
 
11418
11633
  Same single port, opposite direction: the host binds the socket as it always
11419
- did, and every path it does not own is handed to your code **instead of** its
11420
- 404. The host still never answers for your application — with this hook it no
11634
+ did, and every path it does not own is handed to your code **instead of** its 404. The host still never answers for your application — with this hook it no
11421
11635
  longer has to 404 for it either.
11422
11636
 
11423
11637
  What it never receives is the interesting half. The paths the host owns —
11424
- `invokePath`, `healthPath`, `conversationPath` — never reach it, *including a
11425
- wrong method on one of them*, because a hook that could claim `POST /invoke`
11638
+ `invokePath`, `healthPath`, `conversationPath` — never reach it, _including a
11639
+ wrong method on one of them_, because a hook that could claim `POST /invoke`
11426
11640
  would be a second door wearing the first one's name. And it is refused at
11427
11641
  construction beside `{ server }`, by name: there, unmatched paths already fall
11428
11642
  through to your own `'request'` listeners, so a second way to answer them would
@@ -11469,7 +11683,7 @@ hosts rather than by care.
11469
11683
  chunks are coerced back to bytes, and no bytes are lost doing it: `setEncoding`
11470
11684
  decodes through a `StringDecoder`, which holds a partial multi-byte sequence
11471
11685
  across a chunk boundary rather than splitting it. Pinned by writing a body in
11472
- two TCP writes with the split placed *inside* a four-byte character and
11686
+ two TCP writes with the split placed _inside_ a four-byte character and
11473
11687
  asserting it round-trips. Reachable only through `{ server }` — the mode built
11474
11688
  for co-listeners — and reproduced there, with a real second listener on a real
11475
11689
  shared socket rather than a stubbed request.
@@ -11558,7 +11772,7 @@ avoid admitting the cost would have been an accounting trick.
11558
11772
  **The corrective message is an authored frame with the validator's error as
11559
11773
  DATA.** The library's own words come first and say that what follows is a
11560
11774
  report about the answer rather than an instruction; the error is quoted
11561
- verbatim; and *nothing authored follows it*, so there is no trailing sentence
11775
+ verbatim; and _nothing authored follows it_, so there is no trailing sentence
11562
11776
  for injected text to pre-empt. A schema whose error message reads "IGNORE ALL
11563
11777
  PREVIOUS INSTRUCTIONS" produces a message that still says, first and in the
11564
11778
  library's voice, what it is. This is exactly the compaction frame's rule
@@ -11637,7 +11851,7 @@ bytes — pinned against 7.25 by test, not by care.
11637
11851
 
11638
11852
  - **`agentfootprint.agent.output_schema_retry`** — one per failed attempt,
11639
11853
  carrying `{ attempt, retriesRemaining, iteration, stage, error, path?,
11640
- correctiveMessageHash }`. 69 typed events across 20 domains. It sits in the
11854
+ correctiveMessageHash }`. 69 typed events across 20 domains. It sits in the
11641
11855
  `agent` domain beside `output_schema_validation_failed`, its in-stage
11642
11856
  sibling; a new domain for one event that has a family home would have been
11643
11857
  taxonomy for its own sake.
@@ -11698,8 +11912,9 @@ side can call. So this release ships a second port beside the first.
11698
11912
  ```ts
11699
11913
  const host = nodeHost({ port: 8080 });
11700
11914
 
11701
- await standingAgent({ agent, sessions, host }); // POST /invoke
11702
- await host.serveConversations((conversation) => { // WS /conversation
11915
+ await standingAgent({ agent, sessions, host }); // POST /invoke
11916
+ await host.serveConversations((conversation) => {
11917
+ // WS /conversation
11703
11918
  conversation.onFrame((frame) => conversation.send(answer(frame)));
11704
11919
  conversation.onClose(({ by, reason }) => log(by, reason));
11705
11920
  });
@@ -11868,11 +12083,11 @@ So this release does two things, and the second is the reason for the first.
11868
12083
  ```ts
11869
12084
  Agent.create({ provider, model })
11870
12085
  .act({
11871
- input: [scrubSSNs], // the message, before the run commits it
11872
- beforeTool: [refundCeiling, fourEyes], // every call, before it is dispatched
11873
- afterTool: [stripPII], // every result, before the model reads it
11874
- window: slidingWindow({ keepRecentTurns: 12 }), // what the live window keeps
11875
- output: [noCodenames], // the answer, before the caller gets it
12086
+ input: [scrubSSNs], // the message, before the run commits it
12087
+ beforeTool: [refundCeiling, fourEyes], // every call, before it is dispatched
12088
+ afterTool: [stripPII], // every result, before the model reads it
12089
+ window: slidingWindow({ keepRecentTurns: 12 }), // what the live window keeps
12090
+ output: [noCodenames], // the answer, before the caller gets it
11876
12091
  })
11877
12092
  .build();
11878
12093
  ```
@@ -11886,8 +12101,8 @@ wire, same rows in the ledger.
11886
12101
 
11887
12102
  The canonical path is preserved by **demoting the doors, not deleting them**.
11888
12103
  `.toolMiddleware()`, `.messageMiddleware()`, `.window()` and `.compaction()`
11889
- are unchanged and stay open, and they are now documented under *Composing
11890
- incrementally* — because adding one rule to an agent somebody else built is a
12104
+ are unchanged and stay open, and they are now documented under _Composing
12105
+ incrementally_ — because adding one rule to an agent somebody else built is a
11891
12106
  real job, and a bundle that must be written all at once cannot do it. That
11892
12107
  division is the one-sentence answer to "which spelling": **`.act()` for an
11893
12108
  agent you own, a door for a piece you are adding to somebody else's.** A second
@@ -12136,8 +12351,8 @@ the honest pick is the one whose bytes come back unchanged.
12136
12351
  **The law, which is the part that outlives this vendor.** In the words of the
12137
12352
  field report that bought it:
12138
12353
 
12139
- > *An unreadable stored conversation and an absent one are different facts, and
12140
- > only one of them is safe to answer with a fresh start.*
12354
+ > _An unreadable stored conversation and an absent one are different facts, and
12355
+ > only one of them is safe to answer with a fresh start._
12141
12356
 
12142
12357
  A session nobody has used is absent, and answering it fresh is right. A session
12143
12358
  whose bytes are present and unreadable is not, and answering THAT fresh is
@@ -12155,7 +12370,7 @@ through to the fresh-start path.
12155
12370
  meant reading how this repo writes an event blob, and `AgentCoreStore` — the
12156
12371
  `MemoryStore` adapter — wrote them identically: `payload: [{ blob: entry }]`,
12157
12372
  read back as objects only. Same service, same mangling, same silence, different
12158
- loss: an entry that decodes to nothing was *skipped*, so `list()` came back one
12373
+ loss: an entry that decodes to nothing was _skipped_, so `list()` came back one
12159
12374
  memory short and `get()` came back `null`. Memory that silently stays empty is
12160
12375
  indistinguishable from memory that works, until somebody notices the assistant
12161
12376
  has forgotten a customer's address. Shipping the cure for one organ while the
@@ -12198,8 +12413,8 @@ and are still described that way.
12198
12413
  refused loudly rather than decoded to `undefined`.
12199
12414
 
12200
12415
  - **`hydrate` no longer answers "no session" for a session that HAS one.** The
12201
- adapter's decode step now distinguishes *no blob at all* (an absence, which
12202
- hydrates as `undefined`) from *a blob it cannot read* (which travels on to the
12416
+ adapter's decode step now distinguishes _no blob at all_ (an absence, which
12417
+ hydrates as `undefined`) from _a blob it cannot read_ (which travels on to the
12203
12418
  shared reading law and is refused by name). Both file and event modes pass the
12204
12419
  session id into `checkEnvelope`, so a refusal names the conversation.
12205
12420
 
@@ -12348,7 +12563,7 @@ than by an opinion.** See below.
12348
12563
  satisfy the container contract this repo documents for a managed agent
12349
12564
  runtime, and is not trying to.** It serves MCP — statelessly (it neither
12350
12565
  issues nor demands a session id, so replicas are interchangeable), on the path
12351
- and port you choose — and it answers *neither* of the contract's two routes:
12566
+ and port you choose — and it answers _neither_ of the contract's two routes:
12352
12567
  `GET /ping` and `POST /invocations` are 404s from it. Two protocols, two
12353
12568
  paths, two adapters: serve the container contract with
12354
12569
  `agentCoreRuntimeHost` and MCP with `mcpServe`. The one thing that is NOT
@@ -12363,7 +12578,7 @@ The refusal becomes acceptance.
12363
12578
  Three releases ago the messages slot was a lie: content declared for it was
12364
12579
  recorded as injected, counted in the slot composition, routed by the engine, and
12365
12580
  never sent. 7.19.1 refused the declaration by name rather than deliver it badly,
12366
- and said why in the same breath — the wire has no system role *inside* the
12581
+ and said why in the same breath — the wire has no system role _inside_ the
12367
12582
  message list on the Anthropic family (system is a separate top-level field)
12368
12583
  while the OpenAI family carries it, so wiring the slot straight through would
12369
12584
  have replaced one uniform gap with a **provider-dependent** one that nothing in
@@ -12388,7 +12603,7 @@ refused when the run starts, naming the provider and the roles it does. The role
12388
12603
  is never rewritten to one that fits — changing who appears to speak is a meaning
12389
12604
  change the app must make, not the library. **Position**: a delivered message goes
12390
12605
  at the end of the window, and if its role would repeat the turn already there, it
12391
- is *deferred* to the next boundary with a sentence on
12606
+ is _deferred_ to the next boundary with a sentence on
12392
12607
  `messagesDelivery.deferred`, never dropped and never reordered, and never
12393
12608
  inserted between a tool call and its result.
12394
12609
 
@@ -12444,7 +12659,7 @@ end. An honest limitation stated loudly beats a clever one hidden.
12444
12659
  ### Fixed
12445
12660
 
12446
12661
  - **The cache marker for `field: 'messages'` pointed at the wrong message.** It
12447
- counted entries in a per-slot list of *injections* and handed that count to
12662
+ counted entries in a per-slot list of _injections_ and handed that count to
12448
12663
  providers who read it as a position in `request.messages` — two index spaces
12449
12664
  under one name. It was unreachable while nothing could target the slot, and
12450
12665
  delivery makes it reachable, so it is recomputed against the actual wire array
@@ -12532,7 +12747,6 @@ end. An honest limitation stated loudly beats a clever one hidden.
12532
12747
  request's tool array. It is out of this release's scope, and it is now the only
12533
12748
  one of the pair left.
12534
12749
 
12535
-
12536
12750
  ## [7.20.0] - 2026-08-03
12537
12751
 
12538
12752
  Three small honesty fixes. No new machinery, no delivery change, no wire bytes
@@ -12642,7 +12856,7 @@ message-sequence rule; it is queued as a feature, with this gap as its evidence.
12642
12856
  / `.instruction` / `.fact` all pass through — refuses a hand-built `Injection`
12643
12857
  carrying `inject.messages`, so the refusal cannot be walked around. The
12644
12858
  message names the limitation and the working alternatives: `slot:
12645
- 'system-prompt'` (the default, delivered by every provider), a tool's return
12859
+ 'system-prompt'` (the default, delivered by every provider), a tool's return
12646
12860
  value (a tool result IS a recent message, at the recency the option was
12647
12861
  reaching for), and the text passed to `agent.run({ message })`.
12648
12862
 
@@ -12850,7 +13064,7 @@ Example: `examples/deploy/durable-sessions.ts`.
12850
13064
  ## [7.18.0] - 2026-08-03
12851
13065
 
12852
13066
  Every agent framework lets you wrap a tool call. Most of them let the wrapper
12853
- *answer* — return a canned string, a cached value, a "simulated" result — and
13067
+ _answer_ — return a canned string, a cached value, a "simulated" result — and
12854
13068
  the moment one does, the trace is fiction. The model was told a tool ran.
12855
13069
  Nothing ran.
12856
13070
 
@@ -12909,7 +13123,7 @@ taken afterwards: the trace would show text nobody ever sent.
12909
13123
 
12910
13124
  - **`ask` suspends on the SHIPPED pause machinery.** `isAskPause(outcome)`
12911
13125
  narrows a paused run and `outcome.ask` carries `{ question, detail?,
12912
- middleware }`. Resume with `checkInApproved` / `checkInDeclined` — the same
13126
+ middleware }`. Resume with `checkInApproved` / `checkInDeclined` — the same
12913
13127
  human-answer vocabulary check-ins use, deliberately, because a person
12914
13128
  approving is a person approving and one word for one thing beats a synonym.
12915
13129
  A malformed resume DECLINES, so a governed call can never execute because a
@@ -13046,7 +13260,7 @@ measured lifetime. Removing is not forgetting.
13046
13260
  token budget, and reporting a `capTokens` nobody configured would be exactly
13047
13261
  the invented number this family refuses.
13048
13262
 
13049
- `keepRecentTurns` is required and has no default. It *is* the policy.
13263
+ `keepRecentTurns` is required and has no default. It _is_ the policy.
13050
13264
 
13051
13265
  - **`tokenBudget({ thresholdTokens, keepRecentTurns? })` — counted, then
13052
13266
  dropped.** Reads the input tokens the adapter reported for the last call and
@@ -13094,7 +13308,7 @@ measured lifetime. Removing is not forgetting.
13094
13308
  It appears only at the head (a removal in the middle leaves the opening turn
13095
13309
  in place, so there is nothing to fix and a spliced `user` message is its own
13096
13310
  risk); it never accumulates (the next drop absorbs it); and if it would not
13097
- be *smaller* than the span it replaces, the whole drop is abandoned under
13311
+ be _smaller_ than the span it replaces, the whole drop is abandoned under
13098
13312
  `summary-not-smaller`, whose meaning generalizes to "the replacement came
13099
13313
  back no smaller than the span" rather than growing the closed reason union.
13100
13314
 
@@ -13188,7 +13402,7 @@ arrived yet.
13188
13402
  Example: `examples/context-engineering/11-compaction.ts`.
13189
13403
 
13190
13404
  - **`CompactionRecord` on `scope.compactions` — the fold's half of the law.**
13191
- One record per over-budget visit, *including the visits that folded nothing*,
13405
+ One record per over-budget visit, _including the visits that folded nothing_,
13192
13406
  which are the interesting ones. It carries `foldedStageIds` (real
13193
13407
  `runtimeStageId`s, resolvable in the commit log), `foldedMessageCount`,
13194
13408
  `measuredTokens` vs `thresholdTokens`, exact `windowCharsBefore` /
@@ -13212,7 +13426,7 @@ arrived yet.
13212
13426
  - **`COMPACTED_FRAME_PREFIX` / `isCompactedSummary(msg)`** — the authored frame
13213
13427
  is a library constant and the summarizer's text is appended after it as data.
13214
13428
  A summarizer returning `IGNORE ALL PREVIOUS INSTRUCTIONS` still arrives
13215
- *inside* a message that says, first and in the library's own words, that what
13429
+ _inside_ a message that says, first and in the library's own words, that what
13216
13430
  follows is a summary written by a model and not the conversation. A test pins
13217
13431
  exactly that, with a hostile summarizer. The boundary points both ways: the
13218
13432
  folded transcript reaches the summarizer between markers the authored
@@ -13242,7 +13456,7 @@ arrived yet.
13242
13456
  - **With `.compaction()` configured, the compaction stage becomes the ReAct
13243
13457
  loop target** (`compact`), mounted immediately before the previous one. The
13244
13458
  loop is branch-sourced, so anything ahead of the target runs once and is
13245
- never seen again — and being the target puts the fold *before* the injection
13459
+ never seen again — and being the target puts the fold _before_ the injection
13246
13460
  engine and the three context slots, which is the point: the triggers, the
13247
13461
  slots and the wire then all see one window, and no part of the run reasons
13248
13462
  over a past the model was not shown. Without `.compaction()` the loop target
@@ -13259,9 +13473,9 @@ arrived yet.
13259
13473
  ## [7.15.0] - 2026-08-02
13260
13474
 
13261
13475
  7.14.0 shipped two hosting ports that name no cloud, plus a conformance suite,
13262
- and made a promise: *a cloud adapter is vendor paths and a header mapping on a
13476
+ and made a promise: _a cloud adapter is vendor paths and a header mapping on a
13263
13477
  port that already worked; if writing one needs a change to a port, the port was
13264
- wrong.* A promise like that is worth nothing until somebody writes the adapter.
13478
+ wrong._ A promise like that is worth nothing until somebody writes the adapter.
13265
13479
 
13266
13480
  This release writes it. `agentCoreRuntimeHost` is a real cloud runtime's
13267
13481
  container contract — different paths, different body fields, the conversation id
@@ -13275,8 +13489,8 @@ policy store behind the existing permission port, per-request credential vending
13275
13489
  for Gateway tools, and the memory adapter's `search()` finally wired.
13276
13490
 
13277
13491
  Three seams did have to move, and none of them was a port. They are listed under
13278
- "Changed" rather than buried, because *where an adapter needs more than paths
13279
- and headers* is the interesting result of an exercise like this — and two of the
13492
+ "Changed" rather than buried, because _where an adapter needs more than paths
13493
+ and headers_ is the interesting result of an exercise like this — and two of the
13280
13494
  three turned out not to be about this vendor at all.
13281
13495
 
13282
13496
  ### Added
@@ -13308,7 +13522,7 @@ three turned out not to be about this vendor at all.
13308
13522
  Example: `examples/deploy/agentcore-runtime.ts`.
13309
13523
 
13310
13524
  - **`httpHost({ name, wire, invokePath, healthPath, port?, hostname?,
13311
- capabilities? })` — the HTTP work, parameterised by the JSON dialect it
13525
+ capabilities? })` — the HTTP work, parameterised by the JSON dialect it
13312
13526
  speaks.** Draining on close, aborting when the caller hangs up, failing a
13313
13527
  handler that throws, failing a handler that answers nothing, mapping refusal
13314
13528
  codes to status codes, and choosing between one JSON body and Server-Sent
@@ -13323,7 +13537,7 @@ three turned out not to be about this vendor at all.
13323
13537
  matching and gets it subtly wrong in exactly one deployment.
13324
13538
 
13325
13539
  - **`agentCorePolicy({ policyStoreId, region?, onUnavailable?, onWarning?,
13326
- principalFor?, name?, cacheSize? })` — an AgentCore policy store behind the
13540
+ principalFor?, name?, cacheSize? })` — an AgentCore policy store behind the
13327
13541
  existing `PermissionChecker` port** (`agentfootprint/security`). Every
13328
13542
  attempted tool call becomes one evaluation.
13329
13543
 
@@ -13371,7 +13585,7 @@ three turned out not to be about this vendor at all.
13371
13585
 
13372
13586
  Results are marked `metadata.source: 'agentcore-memory-record'`, because
13373
13587
  `search` reads a genuinely different population than `list`: the records
13374
- AgentCore's extraction strategies *derived from* your events, whose ids belong
13588
+ AgentCore's extraction strategies _derived from_ your events, whose ids belong
13375
13589
  to AgentCore, so `store.get(result.entry.id)` will not find them.
13376
13590
 
13377
13591
  There is **no `stream()`**. AgentCore Memory has no streaming data-plane
@@ -13488,7 +13702,7 @@ and if writing one ever needs a change to a port, the port was wrong.
13488
13702
  - **`standingAgent({ agent, sessions, host, onConcurrentInvoke? })` — the
13489
13703
  composer.** Per request: wake and hydrate the session, resume that
13490
13704
  conversation or start a fresh one, persist what the run left behind, reply.
13491
- Persist happens *before* the answer goes out, so a queued next turn can never
13705
+ Persist happens _before_ the answer goes out, so a queued next turn can never
13492
13706
  read state older than the answer already given.
13493
13707
 
13494
13708
  It restates the `resumeOnError` tool re-execution caveat **verbatim** in its
@@ -13502,7 +13716,7 @@ and if writing one ever needs a change to a port, the port was wrong.
13502
13716
  afterwards belongs to whichever started last, so one session's envelope can end
13503
13717
  up holding another session's conversation with nothing in the recording to say
13504
13718
  so. `ConcurrentInvokePolicy` is the separate question of a second turn of the
13505
- *same* conversation: `'reject'` (default) refuses with a `ConcurrentRunError`
13719
+ _same_ conversation: `'reject'` (default) refuses with a `ConcurrentRunError`
13506
13720
  naming the active run (`409`), `'enqueue'` queues it FIFO behind the run whose
13507
13721
  state it will then read. A request for a **different** session is never
13508
13722
  refused — it waits its turn.
@@ -13543,7 +13757,7 @@ person something, `standingAgent` answers with a `PauseNotCarriedError` and
13543
13757
  writes **nothing** — the session keeps exactly the conversation it had before the
13544
13758
  request. Over HTTP that is a `409`, not a `500`, because the agent did not break
13545
13759
  and every dashboard that sees a 500 will conclude otherwise. `'conversation-v1'`
13546
- stores a conversation; a paused run is a conversation *plus* an engine
13760
+ stores a conversation; a paused run is a conversation _plus_ an engine
13547
13761
  checkpoint, and storing half of it would be worse than storing none. Carrying a
13548
13762
  pause would be a NEW format name in the same envelope — which is precisely what
13549
13763
  the version in the format is for.
@@ -13558,7 +13772,7 @@ host can send any string there, including someone else's.
13558
13772
 
13559
13773
  **The conformance suite is the deliverable, not the tests for it.** One handler
13560
13774
  constant, served by `nodeHost` and by a minimal in-process host that declares
13561
- *no* capabilities so the buffering path is exercised rather than assumed, with a
13775
+ _no_ capabilities so the buffering path is exercised rather than assumed, with a
13562
13776
  final pair of cases invoking both and comparing directly. A future adapter —
13563
13777
  including a cloud one — is measured against that file.
13564
13778
 
@@ -13586,7 +13800,7 @@ other way, and a value committed where run-level values already commit.
13586
13800
  and the body still arrives only after it does. What changes is who can edit a
13587
13801
  playbook, and whether changing the refund policy shows up as a reviewable diff.
13588
13802
 
13589
- A skill body is *instructions to a model*, so where it came from is a security
13803
+ A skill body is _instructions to a model_, so where it came from is a security
13590
13804
  property rather than a convenience — content fetched at run time is content
13591
13805
  someone else can change after you reviewed it. The loader therefore accepts a
13592
13806
  local directory and nothing else: a URL is **refused by name**, not fetched.
@@ -13657,7 +13871,7 @@ other way, and a value committed where run-level values already commit.
13657
13871
 
13658
13872
  - **`McpClientOptions.signal` cancels a hung MCP tool call again — it never
13659
13873
  did.** The signal was being sent as part of the `tools/call` request
13660
- *params*, where an `AbortSignal` JSON-serializes to `{}`: the server received
13874
+ _params_, where an `AbortSignal` JSON-serializes to `{}`: the server received
13661
13875
  a meaningless field and the caller received no cancellation. The SDK takes
13662
13876
  per-request options in a separate trailing argument, which is where the signal
13663
13877
  now goes; it is threaded to `connect()` and `listTools()` for the same reason,
@@ -13696,7 +13910,7 @@ synchronous, and making it async to accommodate a resolver would shift every
13696
13910
  agent's timing for a feature most agents do not use. A resolver that needs I/O
13697
13911
  can do it before `run()` and close over the result.
13698
13912
 
13699
- Every debugging session starts at a *variable* — "where did that instruction come
13913
+ Every debugging session starts at a _variable_ — "where did that instruction come
13700
13914
  from?", "which loop wrote the history it answered from?" — and both halves of the
13701
13915
  answer already existed, in vocabularies that did not meet. footprintjs 9.13 records
13702
13916
  a variable's whole life in commit indices and runtimeStageIds; the localizer thinks
@@ -13707,7 +13921,7 @@ Joining them turned out to buy something bigger than a nicer read-out. The backw
13707
13921
  walk narrows each loop with embedding similarity — a proxy that points at a
13708
13922
  neighbourhood and cannot separate a planted instruction from an innocent same-topic
13709
13923
  sibling. But where the recording carries per-write provenance, one part of that guess
13710
- is unnecessary: the commit log *says* which write produced the value this loop read.
13924
+ is unnecessary: the commit log _says_ which write produced the value this loop read.
13711
13925
  So the walk stops guessing exactly there — and keeps saying so everywhere else.
13712
13926
 
13713
13927
  ### Added
@@ -13735,7 +13949,7 @@ So the walk stops guessing exactly there — and keeps saying so everywhere else
13735
13949
  same hops, same order, same verdicts (pinned by a deep-equal test).
13736
13950
 
13737
13951
  The proxy still picks WHO; dataflow picks WHERE. A stage-level edge never becomes
13738
- an exact hop, and a recorded edge outranks the *inferred* proximate-tool hop —
13952
+ an exact hop, and a recorded edge outranks the _inferred_ proximate-tool hop —
13739
13953
  better evidence wins, and the hop record says which kind it used.
13740
13954
 
13741
13955
  - **`AgentOptions.writeProvenance`** (`'off'` default, `'reads-prefix'` to enable) —
@@ -13752,7 +13966,7 @@ So the walk stops guessing exactly there — and keeps saying so everywhere else
13752
13966
  `coverage: 'exact'` requires **positive** evidence — at least one recorded per-write
13753
13967
  edge — not merely the absence of a conservative one. A key nothing ever reads back
13754
13968
  (the agent's `lastToolResult`: written by tool-calls, never read by `call-llm`) has
13755
- an empty edge set, so "no conservative edges" is *vacuously* true; scoring that as
13969
+ an empty edge set, so "no conservative edges" is _vacuously_ true; scoring that as
13756
13970
  exact would hand the walk its most confident hop on its least-evidenced key. Absence
13757
13971
  of dataflow is `'unknown'`, never exactness.
13758
13972
 
@@ -13776,7 +13990,7 @@ it with measurements instead of re-deriving why it was strict.
13776
13990
 
13777
13991
  ## [7.11.0] - 2026-08-02
13778
13992
 
13779
- A pipeline whose steps form a *shape* rather than a line — one step feeding two
13993
+ A pipeline whose steps form a _shape_ rather than a line — one step feeding two
13780
13994
  independent lookups, a third waiting for both — had no home here. You could nest
13781
13995
  a `Parallel` inside a `Sequence`, but then you were scheduling it by hand, and
13782
13996
  the values did not survive the trip.
@@ -13848,7 +14062,7 @@ switches keep compiling. Same reasoning as `workflow()` in 7.10.0.
13848
14062
  ## [7.10.0] - 2026-08-02
13849
14063
 
13850
14064
  Two routing-shaped gaps closed. Both were things the docs told you to hand-roll,
13851
- and both were fiddly in the same way: the wiring is easy to get *nearly* right,
14065
+ and both were fiddly in the same way: the wiring is easy to get _nearly_ right,
13852
14066
  and nearly right fails quietly — at run time, several steps away from the
13853
14067
  mistake.
13854
14068
 
@@ -13857,7 +14071,7 @@ mistake.
13857
14071
  - **`llmRouter` — the classic Swarm decision, packaged.** `swarm()` asks for a
13858
14072
  `route()` that is sync and pure, and it means it: the `Conditional` evaluates
13859
14073
  it once per branch predicate and the loop's exit guard evaluates it again
13860
- after every turn. So the LLM decision has to happen *somewhere else*, before
14074
+ after every turn. So the LLM decision has to happen _somewhere else_, before
13861
14075
  the message reaches `route` — and that placement is the part everyone
13862
14076
  re-invented, along with the prompt, the parsing, and a second copy of the
13863
14077
  agent roster that drifts from the first.
@@ -13871,8 +14085,8 @@ mistake.
13871
14085
  with a stale decision.
13872
14086
 
13873
14087
  The decision is validated JSON — `RoutingDecision` = `{ agentId?, message,
13874
- reason? }`. No `agentId` means "done", and the swarm halts through its own
13875
- halt sentinel. An id that isn't in the roster is kept verbatim, *not* quietly
14088
+ reason? }`. No `agentId` means "done", and the swarm halts through its own
14089
+ halt sentinel. An id that isn't in the roster is kept verbatim, _not_ quietly
13876
14090
  swapped for a plausible one: `swarm()`'s existing done/fallback law then ends
13877
14091
  the run, so a hallucinated agent shows up as a halt instead of a wrong answer.
13878
14092
  Unusable output throws `RoutingDecisionError` with the model's raw text
@@ -13907,7 +14121,7 @@ mistake.
13907
14121
  feeds the next step's `{ message }`, the house convention every LLM runner
13908
14122
  speaks; anything else must match exactly) — a chain that doesn't line up is a
13909
14123
  compile error, pinned by `@ts-expect-error` fixtures under `npm run
13910
- test:types`. At run time, values are handed over **unchanged**: objects stay
14124
+ test:types`. At run time, values are handed over **unchanged**: objects stay
13911
14125
  objects. `workflow(draft, edit)` over two `LLMCall`s reads exactly as it
13912
14126
  always did.
13913
14127
 
@@ -13937,7 +14151,7 @@ mistake.
13937
14151
 
13938
14152
  **If you pass `dimensions` today, your vectors change length.** They were the
13939
14153
  model's native length all along; now they are the length you asked for. Anything
13940
- you have already embedded and stored was written at the *old* length, so a store
14154
+ you have already embedded and stored was written at the _old_ length, so a store
13941
14155
  built with `openaiEmbedder({ dimensions: 256 })` on 7.8 holds 1536-long vectors
13942
14156
  and will not match new 256-long queries. **Re-embed, or drop `dimensions` to
13943
14157
  keep the old lengths.**
@@ -13955,7 +14169,7 @@ Also changed, in the same spirit of "`.dimensions` must not lie":
13955
14169
  1536 for everything, so `text-embedding-3-large` under-reported by half.
13956
14170
  - An **unknown model with no `dimensions` is now a construction-time error**
13957
14171
  instead of a silent 1536. This is the breaking edge: `openaiEmbedder({
13958
- baseURL, model: 'nomic-embed-text' })` against a gateway, an Ollama server or
14172
+ baseURL, model: 'nomic-embed-text' })` against a gateway, an Ollama server or
13959
14173
  an Azure deployment name now throws until you state the length. That
13960
14174
  population is exactly the one that was being lied to. One option fixes it:
13961
14175
  `{ dimensions: 768 }`.
@@ -13971,7 +14185,7 @@ Also changed, in the same spirit of "`.dimensions` must not lie":
13971
14185
  - **`localEmbedder({ backend })` / `staticEmbedder({ backend })` — pass an
13972
14186
  already-imported module, and the on-device embedders work in a browser.** To
13973
14187
  keep the heavy peer deps optional, both factories import them through a
13974
- *variable* specifier — which no bundler can see through. The bare name
14188
+ _variable_ specifier — which no bundler can see through. The bare name
13975
14189
  survived a production build and reached the browser unresolved:
13976
14190
  `TypeError: Failed to resolve module specifier '@huggingface/transformers'`.
13977
14191
  The capability was there all along; only the packaging blocked it. Now the
@@ -14008,7 +14222,7 @@ Also changed, in the same spirit of "`.dimensions` must not lie":
14008
14222
 
14009
14223
  - **Docs-truth check — an ongoing, honest answer to "do the docs describe what
14010
14224
  the code actually does?"** `npm run docs:truth` (new CI job `docs-truth`)
14011
- answers three *separate* questions for every capability the package exposes,
14225
+ answers three _separate_ questions for every capability the package exposes,
14012
14226
  because their combinations are different bugs: DECLARED (in the published
14013
14227
  surface), DOCUMENTED (described in prose on the site), EXERCISED (a real run
14014
14228
  produces it). Declared/documented/never-exercised is the shape a dead or
@@ -14022,7 +14236,7 @@ Also changed, in the same spirit of "`.dimensions` must not lie":
14022
14236
  therefore cannot see a single `agentfootprint/<subpath>` symbol. The
14023
14237
  surface is reported per subpath, since root-barrel-vs-subpath is itself a
14024
14238
  known source of user confusion. Events come from `ALL_EVENT_TYPES`.
14025
- - The DOCUMENTED column counts *only* prose on the 63 hand-written pages
14239
+ - The DOCUMENTED column counts _only_ prose on the 63 hand-written pages
14026
14240
  under `docs-next/content/docs`. Both TypeDoc trees
14027
14241
  (`docs-next/content/docs/api/`, `docs/api-reference/`) are excluded — they
14028
14242
  are generated from source, so every symbol appears in them by construction