agentfootprint 9.83.0 → 9.85.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 (137) hide show
  1. package/CHANGELOG.md +713 -512
  2. package/dist/artifacts/present.js +21 -3
  3. package/dist/artifacts/present.js.map +1 -1
  4. package/dist/artifacts/wants.js +19 -4
  5. package/dist/artifacts/wants.js.map +1 -1
  6. package/dist/core/Agent.js +10 -0
  7. package/dist/core/Agent.js.map +1 -1
  8. package/dist/core/agent/AgentBuilder.js.map +1 -1
  9. package/dist/core/agent/buildToolRegistry.js +137 -0
  10. package/dist/core/agent/buildToolRegistry.js.map +1 -1
  11. package/dist/core/agent/evidence/frames.js +15 -6
  12. package/dist/core/agent/evidence/frames.js.map +1 -1
  13. package/dist/core/agent/outputEnforcement.js +7 -4
  14. package/dist/core/agent/outputEnforcement.js.map +1 -1
  15. package/dist/core/agent/selfCallNotice.js +174 -0
  16. package/dist/core/agent/selfCallNotice.js.map +1 -0
  17. package/dist/core/agent/stages/routeTurn.js +28 -3
  18. package/dist/core/agent/stages/routeTurn.js.map +1 -1
  19. package/dist/core/agent/stages/toolCalls.js +87 -2
  20. package/dist/core/agent/stages/toolCalls.js.map +1 -1
  21. package/dist/core/agent/window/currentRequest.js +11 -13
  22. package/dist/core/agent/window/currentRequest.js.map +1 -1
  23. package/dist/core/agent/window/notice.js +9 -9
  24. package/dist/core/agent/window/notice.js.map +1 -1
  25. package/dist/core/agent/window/summarize.js +9 -9
  26. package/dist/core/agent/window/summarize.js.map +1 -1
  27. package/dist/core/codeRunnerTool.js +25 -6
  28. package/dist/core/codeRunnerTool.js.map +1 -1
  29. package/dist/doors/skill-graph.js +2 -1
  30. package/dist/doors/skill-graph.js.map +1 -1
  31. package/dist/esm/artifacts/present.js +21 -3
  32. package/dist/esm/artifacts/present.js.map +1 -1
  33. package/dist/esm/artifacts/wants.js +19 -4
  34. package/dist/esm/artifacts/wants.js.map +1 -1
  35. package/dist/esm/core/Agent.js +10 -0
  36. package/dist/esm/core/Agent.js.map +1 -1
  37. package/dist/esm/core/agent/AgentBuilder.d.ts +15 -0
  38. package/dist/esm/core/agent/AgentBuilder.js.map +1 -1
  39. package/dist/esm/core/agent/buildToolRegistry.d.ts +137 -0
  40. package/dist/esm/core/agent/buildToolRegistry.js +137 -0
  41. package/dist/esm/core/agent/buildToolRegistry.js.map +1 -1
  42. package/dist/esm/core/agent/evidence/frames.d.ts +9 -4
  43. package/dist/esm/core/agent/evidence/frames.js +13 -5
  44. package/dist/esm/core/agent/evidence/frames.js.map +1 -1
  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/routeTurn.js +28 -3
  52. package/dist/esm/core/agent/stages/routeTurn.js.map +1 -1
  53. package/dist/esm/core/agent/stages/toolCalls.d.ts +6 -0
  54. package/dist/esm/core/agent/stages/toolCalls.js +87 -2
  55. package/dist/esm/core/agent/stages/toolCalls.js.map +1 -1
  56. package/dist/esm/core/agent/window/currentRequest.d.ts +9 -3
  57. package/dist/esm/core/agent/window/currentRequest.js +11 -13
  58. package/dist/esm/core/agent/window/currentRequest.js.map +1 -1
  59. package/dist/esm/core/agent/window/notice.d.ts +1 -4
  60. package/dist/esm/core/agent/window/notice.js +5 -6
  61. package/dist/esm/core/agent/window/notice.js.map +1 -1
  62. package/dist/esm/core/agent/window/summarize.d.ts +1 -4
  63. package/dist/esm/core/agent/window/summarize.js +5 -6
  64. package/dist/esm/core/agent/window/summarize.js.map +1 -1
  65. package/dist/esm/core/codeRunnerTool.js +25 -6
  66. package/dist/esm/core/codeRunnerTool.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 +1 -0
  72. package/dist/esm/index.js +8 -0
  73. package/dist/esm/index.js.map +1 -1
  74. package/dist/esm/lib/injection-engine/index.d.ts +1 -0
  75. package/dist/esm/lib/injection-engine/index.js +6 -0
  76. package/dist/esm/lib/injection-engine/index.js.map +1 -1
  77. package/dist/esm/lib/injection-engine/skillToolDescriptors.d.ts +29 -0
  78. package/dist/esm/lib/injection-engine/skillToolDescriptors.js +138 -8
  79. package/dist/esm/lib/injection-engine/skillToolDescriptors.js.map +1 -1
  80. package/dist/esm/lib/injection-engine/types.d.ts +54 -0
  81. package/dist/esm/lib/injection-engine/types.js +28 -0
  82. package/dist/esm/lib/injection-engine/types.js.map +1 -1
  83. package/dist/esm/lib/saidByPerson.d.ts +99 -0
  84. package/dist/esm/lib/saidByPerson.js +104 -0
  85. package/dist/esm/lib/saidByPerson.js.map +1 -0
  86. package/dist/esm/maps/engagement/parkCard.js +7 -1
  87. package/dist/esm/maps/engagement/parkCard.js.map +1 -1
  88. package/dist/index.js +14 -3
  89. package/dist/index.js.map +1 -1
  90. package/dist/lib/injection-engine/index.js +9 -2
  91. package/dist/lib/injection-engine/index.js.map +1 -1
  92. package/dist/lib/injection-engine/skillToolDescriptors.js +140 -9
  93. package/dist/lib/injection-engine/skillToolDescriptors.js.map +1 -1
  94. package/dist/lib/injection-engine/types.js +30 -1
  95. package/dist/lib/injection-engine/types.js.map +1 -1
  96. package/dist/lib/saidByPerson.js +111 -0
  97. package/dist/lib/saidByPerson.js.map +1 -0
  98. package/dist/maps/engagement/parkCard.js +7 -1
  99. package/dist/maps/engagement/parkCard.js.map +1 -1
  100. package/dist/types/artifacts/present.d.ts.map +1 -1
  101. package/dist/types/artifacts/wants.d.ts.map +1 -1
  102. package/dist/types/core/Agent.d.ts.map +1 -1
  103. package/dist/types/core/agent/AgentBuilder.d.ts +15 -0
  104. package/dist/types/core/agent/AgentBuilder.d.ts.map +1 -1
  105. package/dist/types/core/agent/buildToolRegistry.d.ts +137 -0
  106. package/dist/types/core/agent/buildToolRegistry.d.ts.map +1 -1
  107. package/dist/types/core/agent/evidence/frames.d.ts +9 -4
  108. package/dist/types/core/agent/evidence/frames.d.ts.map +1 -1
  109. package/dist/types/core/agent/outputEnforcement.d.ts +1 -2
  110. package/dist/types/core/agent/outputEnforcement.d.ts.map +1 -1
  111. package/dist/types/core/agent/selfCallNotice.d.ts +141 -0
  112. package/dist/types/core/agent/selfCallNotice.d.ts.map +1 -0
  113. package/dist/types/core/agent/stages/routeTurn.d.ts.map +1 -1
  114. package/dist/types/core/agent/stages/toolCalls.d.ts +6 -0
  115. package/dist/types/core/agent/stages/toolCalls.d.ts.map +1 -1
  116. package/dist/types/core/agent/window/currentRequest.d.ts +9 -3
  117. package/dist/types/core/agent/window/currentRequest.d.ts.map +1 -1
  118. package/dist/types/core/agent/window/notice.d.ts +1 -4
  119. package/dist/types/core/agent/window/notice.d.ts.map +1 -1
  120. package/dist/types/core/agent/window/summarize.d.ts +1 -4
  121. package/dist/types/core/agent/window/summarize.d.ts.map +1 -1
  122. package/dist/types/doors/skill-graph.d.ts +1 -1
  123. package/dist/types/doors/skill-graph.d.ts.map +1 -1
  124. package/dist/types/events/payloads.d.ts +15 -0
  125. package/dist/types/events/payloads.d.ts.map +1 -1
  126. package/dist/types/index.d.ts +1 -0
  127. package/dist/types/index.d.ts.map +1 -1
  128. package/dist/types/lib/injection-engine/index.d.ts +1 -0
  129. package/dist/types/lib/injection-engine/index.d.ts.map +1 -1
  130. package/dist/types/lib/injection-engine/skillToolDescriptors.d.ts +29 -0
  131. package/dist/types/lib/injection-engine/skillToolDescriptors.d.ts.map +1 -1
  132. package/dist/types/lib/injection-engine/types.d.ts +54 -0
  133. package/dist/types/lib/injection-engine/types.d.ts.map +1 -1
  134. package/dist/types/lib/saidByPerson.d.ts +100 -0
  135. package/dist/types/lib/saidByPerson.d.ts.map +1 -0
  136. package/dist/types/maps/engagement/parkCard.d.ts.map +1 -1
  137. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,190 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [9.85.0] - 2026-09-04
11
+
12
+ A sentence composed once and read many times is not a fact — it is a prediction.
13
+
14
+ 9.84.0 fixed one such sentence and shipped a checker for the class. The checker
15
+ covered two surfaces. Five more sentences matching its own existing rule were
16
+ live elsewhere in the tree, unread by it, because coverage was decided by which
17
+ suite happened to import the helper. That is the same defect one level up: a
18
+ guard asserting a boundary it cannot verify.
19
+
20
+ ### Fixed
21
+
22
+ - **Five model-facing sentences that outlive the moment they were true.** Each is
23
+ now anchored to one named call, in the past tense, after tracing it to its
24
+ delivery point to confirm it really is re-read:
25
+
26
+ - `artifacts/present.ts` and `artifacts/wants.ts` — _"Nothing is live in this
27
+ run's scope right now"_ and its sibling inventory, _"Live refs in scope: …"_.
28
+ A census goes wrong in BOTH directions on re-read: entries sweep, new ones
29
+ appear.
30
+ - `core/codeRunnerTool.ts` — _"staged into this session"_. The anchor resolves,
31
+ and goes on resolving; the defect is that it resolves to a scope holding many
32
+ calls, so a per-call report cannot say which call it describes.
33
+ - `maps/engagement/parkCard.ts` — _"Its instructions and its tools are not being
34
+ sent right now"_, on a card that rides every call while a map is parked. Its
35
+ falsifier is compose order, not staleness: the card is written in the
36
+ injection-engine pass and the tools slot that acts on the park runs after it.
37
+ - The `read_skill` description no longer predicts what `read_skill` will do.
38
+ Naming the cursor is the fix; every sentence tried beside it turned out false
39
+ somewhere. The last one — _"You do not need read_skill to go on using it"_ —
40
+ was argued to be a necessity claim no posture, budget or hold-out could
41
+ falsify. The PARK falsifies it: a parked member keeps the cursor, loses its
42
+ body and its tools, and `read_skill` is then the only door back.
43
+
44
+ ### Added
45
+
46
+ - **Surfaces carry `channel` and `lifetime` separately.** Where a string is
47
+ delivered and how long it lives do not correlate: system text is rebuilt every
48
+ request, so a present-tense clause in it is a fact; a tool result persists, so
49
+ the same clause is a forecast. The rules judge lifetime. Both existing
50
+ exemptions turned out to be lifetime claims wearing channel clothes and are now
51
+ derivable rather than asserted.
52
+
53
+ - **A producer registry that fails when a surface is not exercised.** Coverage
54
+ decided by which suite imports a helper is a habit, not a guarantee. Its header
55
+ states what a green run does not prove: hand-maintained rows cannot see a
56
+ producer nobody registered.
57
+
58
+ - **The offer/dispatch divergence list is walked, not written.**
59
+ `test/core/agent/toolDivergenceWalk.test.ts` crosses every source that can put
60
+ a name on the wire or answer to one — static, provider, MCP, always-on skill,
61
+ active skill, inactive skill, stepped skill — against six narrowing states and
62
+ the framework's auto-attach names, and drives a real run per configuration.
63
+ Sixty configurations, thirty-six divergences, each with a mechanically derived
64
+ cause and a stated reason it is tolerated. New fails. Disappeared fails.
65
+ Vacuous fails, and is unbaselineable.
66
+
67
+ It replaced a hand-written enumeration that claimed completeness and was
68
+ falsified three rounds running. It then found three classes nobody seeded: a
69
+ provider tool whose name a registry holder already owns is dead in both
70
+ directions and the shadow report cannot see it; the auto-attach names disagree
71
+ about what they refuse; and `selfExplain` is a fourth family whose reservation
72
+ reads only the static registry.
73
+
74
+ ### Documentation
75
+
76
+ - **Three laws stated where the code lives**, epoch-scoped, after two earlier
77
+ phrasings were false in shipped configurations. Every offered capability
78
+ resolves to a dispatchable implementation with stable identity for that epoch;
79
+ attention may alter the offer, but omission from the offer must not be
80
+ presented as proof of permanent capability loss. Only static skill-registry
81
+ tools are known to remain dispatchable after leaving the offer.
82
+
83
+ - **`docs/design/2026-09-recorded-not-built.md`** — three real defects with
84
+ reproductions, deliberately not fixed: an inactive skill's tool shadows in
85
+ silence, the shadow report names the wrong source, and `skip_step` is
86
+ claimable by a provider. Each names what deciding to fix it would cost.
87
+
88
+ ## [9.84.0] - 2026-09-03
89
+
90
+ ### Fixed
91
+
92
+ - **`read_skill` refused the skill the model was already in.** A turn routed
93
+ decisively to `X`, the model called `read_skill("X")` to find out where it
94
+ stood, and the gate answered _"`read_skill("X")` is not reachable from here.
95
+ Reachable skills: …"_ — about the one skill whose body was in that call's
96
+ system prompt and whose tools were in that same call's tool list. The cursor
97
+ is in neither half of `hops ∪ open` by construction: `makeReachableSkills`
98
+ filters it out of its own successor set (a move to where you already are is
99
+ not a move) and `openSkillIds()` excludes every graph-wired skill. Nobody had
100
+ written the case for _"you asked for the room you are standing in."_ Read as a
101
+ claim about AVAILABILITY — which is how a model reads _"not reachable"_ — it
102
+ says the opposite of the request it arrived in. A field report recorded the
103
+ consequence three times in one day: the model concluded its capability was
104
+ gone and answered that it could not help, while the skill's tools sat on the
105
+ wire, loaded and callable.
106
+
107
+ A self-call now gets the truth instead of a refusal. It names where the model
108
+ stands and which tools it could call, taken from the merged wire list the LLM
109
+ stage actually sent, intersected with the skill's own declared tools — never
110
+ from the declaration alone. Every configuration that would make that false has
111
+ its own wording: a skill declaring no tools says so, a step or park hold-out
112
+ says the tools were withheld rather than naming them, and a call whose wire
113
+ cannot be established says nothing about tools at all. Mechanically it is still
114
+ a rejection — no activation, no cursor move, and the refusal budget still
115
+ counts it, because a self-call _loop_ is exactly the stuck model that budget
116
+ exists to escalate.
117
+
118
+ **Every clause is anchored to one named call.** A tool result is composed on
119
+ one iteration and re-read on every call after it, including the out-of-budget
120
+ wrap-up that carries no tools under _"Do not request tools."_ So the notice
121
+ makes no forward-looking claim at all: no exhortation to act, no offer of a
122
+ move, no clause conditioned on a budget or a posture that can change after the
123
+ sentence is written. Deixis counts as forward-looking — _"the call you just
124
+ made"_ denotes a different call on each re-read, so the anchor is named once
125
+ in the opening sentence and every later clause refers back to it.
126
+
127
+ - **The `read_skill` description said the same thing in the other channel.** The
128
+ current skill was listed under _"Not reachable from here (read_skill for these
129
+ will be refused)"_, where it appeared purely as an artefact of that same
130
+ filter. It is in neither column now, and the description names the cursor on
131
+ every call that has one — the positive signal whose absence was the root of the
132
+ field failure, since the system prompt carried the skill's body with nothing
133
+ saying which skill it was. A genuinely unreachable skill is still named there,
134
+ and a genuinely unreachable hop keeps its refusal word for word.
135
+
136
+ - **The description no longer predicts what `read_skill` will do.** Naming the
137
+ cursor is the fix; every sentence tried alongside it turned out false
138
+ somewhere. _"read_skill MOVES you to a DIFFERENT skill"_ is false at compose
139
+ time under `strictness: 'rails'` (every model hop refused) and under `'guard'`
140
+ off an outstanding menu, where the posture arm contradicts it head-on. Its
141
+ replacement — _"You do not need read_skill to go on using it"_ — was argued to
142
+ be a claim about necessity that no posture, budget or hold-out could falsify,
143
+ and the **park** falsifies it: a parked map member keeps the cursor, loses its
144
+ body and its tools, and `read_skill` is then the only door back. The
145
+ description is composed before the hold-outs run, so it cannot know when such a
146
+ claim would be lying. Outside an outstanding menu it now states the name and
147
+ stops. The menu's stay clause is unchanged.
148
+
149
+ - **Role visibility now covers the cursor.** The description read the cursor id
150
+ past the `hiddenIds` filter, so a role denied `skill_read` on the skill the
151
+ graph had routed to was still told _"You are in '\<that skill\>'"_ — leaking
152
+ the name of a capability no cursor move would ever grant it. A hidden cursor is
153
+ named nowhere: not as reachable, not as refusable, not as the cursor, and not
154
+ in the menu's stay clause. The security suite's _"a hidden skill is never
155
+ named"_ property is now driven on the `.skillGraph()` path as well as
156
+ `.skill()`; it stayed green through the leak because its agents used `.skill()`
157
+ only, so the leaking line never executed.
158
+
159
+ ### Added
160
+
161
+ - **`saidByPerson(ctx)` / `isSaidByPerson(msg)` — telling what a person said from
162
+ what the library wrote.** Five classes of `role: 'user'` message are authored
163
+ by this library, not by a person: the compaction frame, the drop notice (whose
164
+ text names tools), the schema-check and evidence-check corrections, and any
165
+ injection-delivered message. The window's own refusal engine has always applied
166
+ that rule; a `when` predicate could not, because `InjectionContext.history`
167
+ exposes only `{ role, content, toolName? }`. An author writing an entry rule
168
+ that reads history was silently matching on our own bookkeeping. One
169
+ implementation, reused by both — the rule cannot drift between routing and the
170
+ window.
171
+
172
+ - **`SkillRejectedPayload.reason`** — `'self-call' | 'unreachable' | 'posture'`,
173
+ optional and additive, so a consumer can tell a self-call from a genuine
174
+ unreachable hop without comparing two fields.
175
+
176
+ ### Documentation
177
+
178
+ - **`strictness` says what a posture governs, exactly.** A posture governs the
179
+ model's `read_skill` door and nothing else. Two doors stay open under all three
180
+ postures: OPEN skills, already stated, and a tool's `propose-transition`, now
181
+ stated with its reason and its reachability check. `'rails'` means _the model
182
+ never routes_ — never _"nothing but my declared edges routes"_; a tool of yours
183
+ that proposes is a route you declared in code instead of in the graph. No
184
+ behaviour changed: the exemption is recorded in three places and pinned by a
185
+ test whose title is the argument.
186
+
187
+ - **The injection-engine README's runtime picture matched an older engine.** Its
188
+ diagram drew one box that "evaluates triggers"; the engine is a four-stage
189
+ footprintjs subflow — Gather, Evaluate, Route, Delta — and the cursor, the step
190
+ pointer, the instruction leases and map engagement all advance inside Evaluate.
191
+ The events table placed `context.evaluated` at subflow exit; the code emits it
192
+ in stage 2 of 4. Both corrected, along with three counts that had drifted.
193
+
10
194
  ## [9.83.0] - 2026-09-03
11
195
 
12
196
  ### Fixed
@@ -14,13 +198,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
14
198
  - **The evidence gate claimed a boundary it did not measure.** Both of its
15
199
  user-facing sentences — the correction it sends the model
16
200
  (`buildEvidenceCorrection`) and the warning it prints an operator
17
- (`evidenceRefusalSentence`) — said the flagged values *"appear in NO tool
18
- result **from this turn**"*. The index behind them has never been turn-scoped:
201
+ (`evidenceRefusalSentence`) — said the flagged values _"appear in NO tool
202
+ result **from this turn**"_. The index behind them has never been turn-scoped:
19
203
  it walks every `role: 'tool'` turn in the history. The library was asserting a
20
204
  scope it could not honour, in the two places that assertion is read.
21
205
 
22
- Both now say what the check really reaches — *"appear in no tool result this
23
- run read"* — which is both true and the stronger claim, and the operator
206
+ Both now say what the check really reaches — _"appear in no tool result this
207
+ run read"_ — which is both true and the stronger claim, and the operator
24
208
  sentence adds the two facts a reader needs: that the corpus is the LIVE
25
209
  WINDOW (a window strategy rewrites `scope.history` in place, so a dropped
26
210
  result is not in it), and that `noticePriorTurnEvidence` is what answers the
@@ -34,8 +218,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
34
218
 
35
219
  The measured failure: a consumer's agent answered a data question with **zero
36
220
  tool calls**, and the gate approved it — `LLM calls 1 · Tool calls 0 ·
37
- Iterations 1`, then *"All 7 values in the answer were found in what the tools
38
- returned — the answer stands."* They were found: in an inventory result from
221
+ Iterations 1`, then _"All 7 values in the answer were found in what the tools
222
+ returned — the answer stands."_ They were found: in an inventory result from
39
223
  four turns earlier, fetched for a different question. The user had asked about
40
224
  array performance; the answer recommended enabling a collector that had been
41
225
  running for months. Two turns did it back to back. Every rail passed honestly
@@ -55,14 +239,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
55
239
  .namesAndNumbersFromEvidence() // ← the other half: it owns the extractor
56
240
  .build();
57
241
 
58
- await agent.run('what arrays are there?'); // fetches, answers, files nothing
242
+ await agent.run('what arrays are there?'); // fetches, answers, files nothing
59
243
  await agent.followUp('how is array performance?'); // no tool call, answers from turn 1
60
244
  // → prior-turn-evidence: 3 grounded value(s), all last served in turn 1,
61
245
  // and this turn called no tool at all.
62
246
  ```
63
247
 
64
248
  **The corpus is deliberately NOT narrowed to this turn.** That would have made
65
- the old sentence true and been the wrong fix: *"and what about that disk?"*
249
+ the old sentence true and been the wrong fix: _"and what about that disk?"_
66
250
  leans on the previous turn's rows legitimately, and a check that cries wolf is
67
251
  a check somebody switches off. ONE grounded value from this turn's own results
68
252
  files nothing — not a threshold to tune, but the falsification of the claim
@@ -106,7 +290,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
106
290
  is generated from what the run itself said: the branch descriptions the chart
107
291
  declared, and the rule labels this run's `decide()` evidence carried. For one
108
292
  branch, both sources are silent by construction — the DEFAULT. It is the
109
- branch chosen by *no rule* (it fires exactly when every rule failed, so no
293
+ branch chosen by _no rule_ (it fires exactly when every rule failed, so no
110
294
  `label` describes it), and when the decider lives inside a dynamically
111
295
  generated fan-out branch the branch chart does not exist at build time either,
112
296
  so there is no declared description to fall back on.
@@ -183,7 +367,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
183
367
  sdk: { Client, StreamableHTTPClientTransport },
184
368
  transport: { transport: 'http', url: '/py/mcp' },
185
369
  });
186
- const tools = await sidecar.tools(); // the same readonly Tool[], _meta and all
370
+ const tools = await sidecar.tools(); // the same readonly Tool[], _meta and all
187
371
  ```
188
372
 
189
373
  **`connection?: McpConnection`** — the full escape hatch: you connect the
@@ -257,8 +441,8 @@ arms through a real socket against the real SDK.
257
441
 
258
442
  **What none of that proves: a browser.** This repo has no browser test
259
443
  environment, so nobody has yet driven initialize/listTools/callTool from an
260
- actual page. The honest status is *proven in Node, fenced at the graph, not gated
261
- in a browser.* Three costs land on the app, not here: your server must send CORS
444
+ actual page. The honest status is _proven in Node, fenced at the graph, not gated
445
+ in a browser._ Three costs land on the app, not here: your server must send CORS
262
446
  headers (every MCP request preflights, and `Mcp-Session-Id` must be in
263
447
  `Access-Control-Expose-Headers`) — `mcpServe` sends none; SSE through a dev or
264
448
  production proxy is unproven; and the SDK's client path adds roughly 260 KB
@@ -277,8 +461,8 @@ first time a tool with an `outputSchema` is validated.
277
461
  had a standing rule against exactly that. The library was overruling it.
278
462
 
279
463
  The instruction was ours. Every verdict projection shipped `table`
280
- pre-rendered with `VERDICT_RENDER_NOTE`: *"table is PRE-RENDERED over the
281
- same rows as `verdicts` — output it VERBATIM."* That note is RIGHT wherever
464
+ pre-rendered with `VERDICT_RENDER_NOTE`: _"table is PRE-RENDERED over the
465
+ same rows as `verdicts` — output it VERBATIM."_ That note is RIGHT wherever
282
466
  the model's words are the rows' only surface — a chat client, a log line, an
283
467
  email — because the alternative there is retyping, and a retyped identifier
284
468
  that looks right and matches nothing is the failure the note exists to stop.
@@ -291,7 +475,7 @@ first time a tool with an `outputSchema` is validated.
291
475
  it is in. So the caller says, in one word:
292
476
 
293
477
  ```ts
294
- runbookAsTool({ /* … */ presentation: 'panel' }); // default: 'prose'
478
+ runbookAsTool({ /* … */ presentation: 'panel' }); // default: 'prose'
295
479
  ```
296
480
 
297
481
  - **`'prose'` (the default)** — today's envelope, key for key: `table`
@@ -357,7 +541,7 @@ first time a tool with an `outputSchema` is validated.
357
541
  `recording_note`. The wire ops that already redeem the walk redeem this with
358
542
  **zero new operations**.
359
543
 
360
- **OPT-IN, and that is the honest default.** A walk carries *sentences about*
544
+ **OPT-IN, and that is the honest default.** A walk carries _sentences about_
361
545
  what happened and no payload from it — values are off by construction
362
546
  (`narrative({ includeValues: false })`). A recording is the run: shared
363
547
  state, the whole commit log, every attached recorder's data — **whatever the
@@ -393,7 +577,7 @@ first time a tool with an `outputSchema` is validated.
393
577
  the spine exists to prevent.
394
578
 
395
579
  **`events` is empty by construction, and says so.** It is the typed
396
- *agentfootprint* stream, fired by an agent turn; what ran here is a
580
+ _agentfootprint_ stream, fired by an agent turn; what ran here is a
397
581
  footprintjs chart on its own executor, which fires none. All three keys are
398
582
  present (that is what a viewer reads), the empty array is the honest count,
399
583
  and the note states it so nobody reads it as a dropped stream — the walk's
@@ -412,8 +596,8 @@ first time a tool with an `outputSchema` is validated.
412
596
 
413
597
  - **`Tool.resultColumns` + `checkColumnTypes` — the column-type contract: a
414
598
  tool declares what its rows contain, and the library checks the rows against
415
- it at the boundary.** Three recorded failures, and they are one shape — *a
416
- number became something else, and nothing noticed at the seam*:
599
+ it at the boundary.** Three recorded failures, and they are one shape — _a
600
+ number became something else, and nothing noticed at the seam_:
417
601
 
418
602
  1. A mapping report wrote `str(m.get("logical_unit_number") or "")`. **LUN 0
419
603
  is falsy**, so LUN 0 was stored as an EMPTY STRING on 2,094 mappings, and
@@ -442,19 +626,19 @@ first time a tool with an `outputSchema` is validated.
442
626
  `CostBudget` two-spellings pattern, normalized once).
443
627
 
444
628
  **THE CEILING**, exported as `COLUMN_TYPE_CEILING` and quoted verbatim into
445
- every finding, the `EMPTY_LOOKUP_CEILING` law: *"This judges TYPE, never
629
+ every finding, the `EMPTY_LOOKUP_CEILING` law: _"This judges TYPE, never
446
630
  MEANING — it can see that a column declared `number` holds a string, and it
447
631
  can never see that the string should have been 0, or that a 0.0 should have
448
632
  been an 8; a column whose every value has its declared type passes here and
449
- can still be wrong."* Failures 1 and 3 are caught. **Failure 2 is not, and
633
+ can still be wrong."_ Failures 1 and 3 are caught. **Failure 2 is not, and
450
634
  never will be** — `0.0` is a perfectly good number — and the check says so
451
635
  out loud rather than letting a green row imply otherwise.
452
636
 
453
637
  **TWO finding kinds, because the field bug turned on the difference.** New
454
638
  `ContextErrorKind`s at the **write seam**: **`column-type-mismatch`** (the
455
639
  column is THERE and holds the wrong thing) and **`missing-column`** (the
456
- declared column is in NONE of the rows). *"The value is not what it should
457
- be"* sends a person to the mapping code; *"the column was never delivered"*
640
+ declared column is in NONE of the rows). _"The value is not what it should
641
+ be"_ sends a person to the mapping code; _"the column was never delivered"_
458
642
  sends them to the query. A checker that said only "something is off with
459
643
  logical_unit_number" would have helped with neither. Each finding names the
460
644
  column, the offending value quoted, the rows affected of the rows read, and
@@ -524,7 +708,7 @@ first time a tool with an `outputSchema` is validated.
524
708
  it came back empty.** A triage agent's reverse-lookup tool filtered a column
525
709
  before a pivot, so the column did not exist yet and EVERY reverse lookup
526
710
  returned an empty result — for every identifier, always. The tool then
527
- answered *successfully* with an empty list, and the agent reported in a
711
+ answered _successfully_ with an empty list, and the agent reported in a
528
712
  table, with confidence, that the device was not logged in to any port on any
529
713
  collected switch, advising a check of the physical cabling. It was logged in
530
714
  the whole time. Every rail passed, and passed honestly: nothing errored,
@@ -548,9 +732,9 @@ first time a tool with an `outputSchema` is validated.
548
732
  for the broken filter and for the honest absence. The bound ships as one
549
733
  exported string, `EMPTY_LOOKUP_CEILING`, quoted verbatim into every message
550
734
  so it cannot drift out of one doc and leave a reader thinking the library
551
- knows more than it does: *"An empty result can be perfectly true — the thing
735
+ knows more than it does: _"An empty result can be perfectly true — the thing
552
736
  may exist and simply have nothing to show right now — so this is a place to
553
- look, never a verdict that anything is wrong."*
737
+ look, never a verdict that anything is wrong."_
554
738
 
555
739
  Deliberately NOT `dangling-reference`, whose meaning is the opposite: there
556
740
  the ground has left reach; here the ground IS in reach and the lookup found
@@ -566,7 +750,7 @@ first time a tool with an `outputSchema` is validated.
566
750
  make impossible.
567
751
 
568
752
  **Armed by two halves**, and the second one is why: `noticeEmptyLookups:
569
- true` on `Agent.create` **and** at least one tool declaring `argumentsFrom`.
753
+ true` on `Agent.create` **and** at least one tool declaring `argumentsFrom`.
570
754
  The declaration alone is not enough — it already arms two other checks, and
571
755
  an advisory that armed itself off a declaration made for something else
572
756
  would not be opt-in at all. **Default off is byte-identical**: no finding, no
@@ -637,7 +821,7 @@ first time a tool with an `outputSchema` is validated.
637
821
  pre-rendered table, and the recorded walk that lets a reader CHECK the
638
822
  verdict instead of trusting it. `runbookAsTool` is that envelope as one
639
823
  declaration bag; the smallest legal call is `{ name, description,
640
- procedure }` and it still yields the honest spine.
824
+ procedure }` and it still yields the honest spine.
641
825
 
642
826
  **The mandatory honesty spine**, on every answer whatever the runbook's
643
827
  shape: `af_coverage` (three lists + a sentence naming the rule set and
@@ -673,7 +857,7 @@ first time a tool with an `outputSchema` is validated.
673
857
  tools are invisible — there is no build-time list, the stated 9.72.0
674
858
  caveat), so stages compose registered sources instead of importing modules
675
859
  and building a second query stack. Inner calls run with `hasArtifacts:
676
- false` (one answer, one ticket — never competing chips), a derived
860
+ false` (one answer, one ticket — never competing chips), a derived
677
861
  toolCallId naming the outer call, `needs` resolved on the fail-closed
678
862
  non-interactive path, and `checkIn`/`wants` tools refused BY NAME (an inner
679
863
  call cannot pause, and must never silently skip a consent gate).
@@ -697,8 +881,8 @@ first time a tool with an `outputSchema` is validated.
697
881
  run: four tool results carried real numbers, a compute tool that could sum
698
882
  them was registered — with `wants` declared over the staged dataset kind —
699
883
  and the app's prompt said to use it. The model summed the numbers in its
700
- head anyway and stated the total; the evidence gate recorded *"appears in no
701
- tool result"* and the answer shipped, because the posture only observed. The
884
+ head anyway and stated the total; the evidence gate recorded _"appears in no
885
+ tool result"_ and the answer shipped, because the posture only observed. The
702
886
  app patched it with more prose. The library-shaped fix is two mechanisms it
703
887
  already owns, on the one dial it already has:
704
888
 
@@ -732,9 +916,9 @@ first time a tool with an `outputSchema` is validated.
732
916
  `'guard'` allows the one bounded revision then delivers with both attempts
733
917
  on the record, `'rails'` refuses with `UnsupportedValuesError`. What the
734
918
  revision gains: when the flagged turn holds staged refs a served `wants`
735
- tool can spend, the correction now names them — *"pass 'art_…'
919
+ tool can spend, the correction now names them — _"pass 'art\_…'
736
920
  (dataset/rows) to `compute` — compute the number there and answer with what
737
- it returns"* — inside the authored frame, so the quoted values still come
921
+ it returns"_ — inside the authored frame, so the quoted values still come
738
922
  last and the exempt-corpus fence is untouched. The `revision-asked`
739
923
  `evidence_checked` event carries the same facts additively (`stagedRefs`,
740
924
  `spenderTools`). Absent everything — no gate, `nudge` unset, or no
@@ -762,7 +946,7 @@ first time a tool with an `outputSchema` is validated.
762
946
 
763
947
  **`foundry()` — the project-endpoint provider** (`agentfootprint/providers`).
764
948
  The JS answer to Microsoft's `FoundryChatClient(project_endpoint, model,
765
- credential)`: point it at a Foundry project endpoint (or let the hosted
949
+ credential)`: point it at a Foundry project endpoint (or let the hosted
766
950
  platform's auto-injected `FOUNDRY_PROJECT_ENDPOINT` supply it), name the
767
951
  deployment (`AZURE_AI_MODEL_DEPLOYMENT_NAME ?? MODEL_NAME`), and auth is an
768
952
  Entra `TokenCredential`, an api key, or — given neither — the platform's own
@@ -888,7 +1072,6 @@ first time a tool with an `outputSchema` is validated.
888
1072
  tests fail against the previous source (proven by restoring it); no public
889
1073
  API change; `OllamaUnavailableError` is byte-identical.
890
1074
 
891
-
892
1075
  ## [9.73.0] - 2026-08-27
893
1076
 
894
1077
  ### Fixed
@@ -925,13 +1108,15 @@ first time a tool with an `outputSchema` is validated.
925
1108
 
926
1109
  ```ts
927
1110
  const agent = Agent.create({
928
- provider, model,
1111
+ provider,
1112
+ model,
929
1113
  // DECLARED, never ambient — this option is the only door.
930
- externalGrounds: () => viewerSelection.cells.map((cell) => ({
931
- value: cell.text, // verified by the app against the artifact
932
- source: 'viewer-selection', // the audit label that travels
933
- })),
934
- })
1114
+ externalGrounds: () =>
1115
+ viewerSelection.cells.map((cell) => ({
1116
+ value: cell.text, // verified by the app against the artifact
1117
+ source: 'viewer-selection', // the audit label that travels
1118
+ })),
1119
+ });
935
1120
  ```
936
1121
 
937
1122
  The provider is consulted once per LLM response that contains an armed call,
@@ -1008,25 +1193,25 @@ first time a tool with an `outputSchema` is validated.
1008
1193
  namespaced key (`MCP_TOOL_EXTRAS_KEY`, the string `agentfootprint`), and
1009
1194
  `mcpClient` / `mockMcpClient` read them back onto the registered `Tool`:
1010
1195
 
1011
- | field | what it arms on the consuming side |
1012
- |---|---|
1013
- | `argumentsFrom` | the dangling-reference and unsupported-argument checks |
1014
- | `resultKind` | placement's mint — a placed result a `wants` argument can spend |
1015
- | `owner` | the identity edge subject-joined checks read |
1016
- | `resultClass` | the per-class `check:semantics` rules |
1017
- | `resultCeiling` | the author's refusing ceiling on an oversized result |
1196
+ | field | what it arms on the consuming side |
1197
+ | --------------- | --------------------------------------------------------------- |
1198
+ | `argumentsFrom` | the dangling-reference and unsupported-argument checks |
1199
+ | `resultKind` | placement's mint — a placed result a `wants` argument can spend |
1200
+ | `owner` | the identity edge subject-joined checks read |
1201
+ | `resultClass` | the per-class `check:semantics` rules |
1202
+ | `resultCeiling` | the author's refusing ceiling on an oversized result |
1018
1203
 
1019
1204
  ```ts
1020
1205
  const fleet = await mcpClient({ name: 'fleet-mcp', transport });
1021
1206
  const agent = Agent.create({ provider, model })
1022
- .tools(await fleet.tools()) // backup_status declares argumentsFrom: ['fleet_report']
1207
+ .tools(await fleet.tools()) // backup_status declares argumentsFrom: ['fleet_report']
1023
1208
  .build();
1024
1209
  // …and the choice seam now files `unsupported-argument` for it, exactly as
1025
1210
  // it would for a local defineTool — pinned end to end, disposition row included.
1026
1211
  ```
1027
1212
 
1028
- **The inclusion bar, stated where the list lives:** *a declaration a
1029
- consumer-side check or rail reads; nothing that governs execution.* `needs`
1213
+ **The inclusion bar, stated where the list lives:** _a declaration a
1214
+ consumer-side check or rail reads; nothing that governs execution._ `needs`
1030
1215
  (credentials), `checkIn` (human consent) and the session hooks are excluded
1031
1216
  and always will be — they decide how a tool RUNS, and the tool runs on the
1032
1217
  server. A client holding a consent gate the only executor already held is
@@ -1088,7 +1273,7 @@ first time a tool with an `outputSchema` is validated.
1088
1273
  });
1089
1274
 
1090
1275
  // elsewhere — resolves now, and would have been a kind mismatch before
1091
- defineTool({ name: 'chart', wants: { dataset: 'dataset/rows' }, /* … */ });
1276
+ defineTool({ name: 'chart', wants: { dataset: 'dataset/rows' } /* … */ });
1092
1277
  ```
1093
1278
 
1094
1279
  - **The matcher is untouched.** Nothing here loosens `wants`; exact match is
@@ -1157,9 +1342,9 @@ first time a tool with an `outputSchema` is validated.
1157
1342
  that earns the port its keep —
1158
1343
 
1159
1344
  ```ts
1160
- await session.handControlTo('person'); // the automation stream stops
1345
+ await session.handControlTo('person'); // the automation stream stops
1161
1346
  // …they sign in, clear the CAPTCHA, approve the consent screen, watching live
1162
- await session.handControlTo('agent'); // and the agent carries on
1347
+ await session.handControlTo('agent'); // and the agent carries on
1163
1348
  ```
1164
1349
 
1165
1350
  Pair it with a check-in and the agent **pauses** rather than guesses: the
@@ -1177,7 +1362,7 @@ first time a tool with an `outputSchema` is validated.
1177
1362
  **What verification changed.** The `InvokeBrowser` action union, read off a
1178
1363
  real install of `@aws-sdk/client-bedrock-agentcore` 3.1118.0, is exactly
1179
1364
  `mouseClick | mouseMove | mouseDrag | mouseScroll | keyType | keyPress |
1180
- keyShortcut | screenshot` — with **no navigate member at all**. An adapter
1365
+ keyShortcut | screenshot` — with **no navigate member at all**. An adapter
1181
1366
  written from memory would have invented page verbs for a door that has none.
1182
1367
  The same pass fixed `MouseClickArguments` (`{ x, y, button?, clickCount? }`,
1183
1368
  buttons `LEFT|MIDDLE|RIGHT`), `KeyPressArguments` (`{ key, presses? }`) and
@@ -1213,6 +1398,7 @@ first time a tool with an `outputSchema` is validated.
1213
1398
  another agent (Strands, LangGraph, Google ADK, a Marketplace listing)
1214
1399
  discovers yours through its agent card and calls it. The same split as 9.65.0,
1215
1400
  for the third time:
1401
+
1216
1402
  - **`a2aWire()`** is the A2A PROTOCOL — JSON-RPC 2.0, `message/send`, text
1217
1403
  parts, artifacts, `A2A_PROTOCOL_VERSION` `0.3.0` — an open protocol with no
1218
1404
  vendor in it, exported in its own right along with
@@ -1252,8 +1438,8 @@ first time a tool with an `outputSchema` is validated.
1252
1438
  - **A capability this library claimed and could not honour.** `httpHost`
1253
1439
  declares `['streaming']` by default, so the A2A host inherited it while
1254
1440
  `message/send` has nowhere to put a chunk: `requireCapability(host,
1255
- 'streaming')` would have passed for a host that then delivered none. The
1256
- conformance suite caught it — it asserts chunks *if and only if* the
1441
+ 'streaming')` would have passed for a host that then delivered none. The
1442
+ conformance suite caught it — it asserts chunks _if and only if_ the
1257
1443
  capability is declared — and the adapter now declares `[]`.
1258
1444
 
1259
1445
  ### Changed
@@ -1271,6 +1457,7 @@ first time a tool with an `outputSchema` is validated.
1271
1457
  looking anything up** (`agentfootprint/providers`). `gatewayTransport` says of
1272
1458
  itself that nothing in it is vendor-specific, and that stays true because the
1273
1459
  four facts which ARE AgentCore's now live in one file beside it:
1460
+
1274
1461
  - **the endpoint** — `agentCoreGatewayUrl({ gatewayId, region })` builds
1275
1462
  `https://{gatewayId}.gateway.bedrock-agentcore.{region}.amazonaws.com/mcp`,
1276
1463
  a hostname nobody recalls correctly, which is why it is a function and not a
@@ -1336,6 +1523,7 @@ first time a tool with an `outputSchema` is validated.
1336
1523
  install of `@aws-sdk/client-bedrock-agentcore` **3.1118.0** — names, request
1337
1524
  shapes and enum values read off the package rather than remembered, which is
1338
1525
  the 9.4.0 law:
1526
+
1339
1527
  - **`userFlow: 'consent' | 'exchange'`** — `'exchange'` sends
1340
1528
  `ON_BEHALF_OF_TOKEN_EXCHANGE`, trading the person's existing login for a
1341
1529
  scoped downstream token with no consent screen at any point. Default stays
@@ -1492,7 +1680,7 @@ first time a tool with an `outputSchema` is validated.
1492
1680
 
1493
1681
  The field story: a tool result ended with an offer — "I can also map these
1494
1682
  ids to volume names" — and the person answered "yes please". The model bound
1495
- *that sentence* as the identifier argument and dispatched. The tool's schema
1683
+ _that sentence_ as the identifier argument and dispatched. The tool's schema
1496
1684
  DECLARED the identifier's shape, in a `pattern` that "yes please" could never
1497
1685
  match; the pre-dispatch validator simply did not read the keyword. So the
1498
1686
  call went out, failed downstream, and cost a round trip — and the consumer
@@ -1637,7 +1825,7 @@ first time a tool with an `outputSchema` is validated.
1637
1825
  check, so one busy check could carry the total while another check looked
1638
1826
  at nothing at all — the per-check rows said so underneath (`⚠ ran 0×`), but
1639
1827
  a reader who stops at the headline got the rosier story. The headline now
1640
- states coverage: either *all* registered checks ran, or it says COVERAGE IS
1828
+ states coverage: either _all_ registered checks ran, or it says COVERAGE IS
1641
1829
  PARTIAL and names the checks that checked nothing. Same law as the rows —
1642
1830
  a check that never saw a subject is silent about its seam, never a pass.
1643
1831
 
@@ -1667,38 +1855,40 @@ first time a tool with an `outputSchema` is validated.
1667
1855
  - **Six defects in the Context Integrity family, caught by an adversarial
1668
1856
  review before release** — each survived two independent attempts to refute
1669
1857
  it, and each is now pinned by a red-proved regression test.
1670
- - *A claim finding's identity ignored the FIELD*, so a contract naming two
1858
+
1859
+ - _A claim finding's identity ignored the FIELD_, so a contract naming two
1671
1860
  fields of one entity (the shape `.claims()` itself tells you to write)
1672
1861
  filed one event and swallowed the rest — while the disposition ledger
1673
1862
  counted them all, leaving the two accounts of one run disagreeing.
1674
1863
  `ContextError.predicate` now rides the identity, mirroring the
1675
1864
  substrate's own `assertionKey`; findings that never set it keep exactly
1676
1865
  the identity they had.
1677
- - *The dangling-reference check was DEAD under `reactMode:
1678
- 'dynamic-grouped'`* — `compactions` was threaded into the wrong mapper,
1866
+ - _The dangling-reference check was DEAD under `reactMode:
1867
+ 'dynamic-grouped'`_ — `compactions` was threaded into the wrong mapper,
1679
1868
  so the check saw an empty window ledger every pass and filed a healthy
1680
1869
  verdict. Chart-shape parity is now pinned by tests that run the same
1681
1870
  trap under both dynamic shapes.
1682
- - *The dev canary structurally disabled the wiring-rot theorem.* A minted
1871
+ - _The dev canary structurally disabled the wiring-rot theorem._ A minted
1683
1872
  canary proves the pure function still works; it says nothing about
1684
1873
  whether the pipeline ever calls it (`beginIntegrityRun` mints by calling
1685
1874
  the function directly). Masking theorem (i) with it meant the alarm this
1686
1875
  ledger exists for could never fire.
1687
- - *`workExisted` was hardcoded `true`* on every exit path, so a run that
1876
+ - _`workExisted` was hardcoded `true`_ on every exit path, so a run that
1688
1877
  died or paused before its first LLM call reported every registered
1689
1878
  checker as dead. It is now measured from a signal the integrity code
1690
1879
  does not itself write.
1691
- - *An answer agreeing with a settled non-reading was filed as an advisory*
1880
+ - _An answer agreeing with a settled non-reading was filed as an advisory_
1692
1881
  — `null` reported for a fact whose settled value is `null` is agreement,
1693
1882
  not doubt.
1694
- - *The claim ledger accumulated for agents that never declared a
1695
- contract*, and appended by whole-array spread. It is now gated on
1883
+ - _The claim ledger accumulated for agents that never declared a
1884
+ contract_, and appended by whole-array spread. It is now gated on
1696
1885
  `.claims()` and appends without the quadratic copy — restoring the
1697
1886
  zero-delta promise for every agent that configured none of this.
1698
1887
 
1699
1888
  - **Two honesty defects in `find_context_errors`**, both caught by the same
1700
1889
  adversarial review and both red-proved by a failing test first.
1701
- - *The green headline never read the counts it claimed to summarise.*
1890
+
1891
+ - _The green headline never read the counts it claimed to summarise._
1702
1892
  "The checkers below RAN; nothing they cover was violated" was printed on
1703
1893
  the mere absence of a finding, so a run whose every encounter was
1704
1894
  `unreachable` (the check could not see the evidence), whose checks all
@@ -1711,7 +1901,7 @@ first time a tool with an `outputSchema` is validated.
1711
1901
  all, each keep their own sentence. A checker ROW with zero `checked`
1712
1902
  stopped saying "the checker ran and found nothing at this seam" for the
1713
1903
  same reason.
1714
- - *The tool advertised defect classes no check can file.* The `kind` enum
1904
+ - _The tool advertised defect classes no check can file._ The `kind` enum
1715
1905
  was pinned to the whole `ContextErrorKind` union, so it offered
1716
1906
  `unsupported-argument` and `duplicate-execution` — classes no check in
1717
1907
  this build files — and answering one returned a negative verdict about
@@ -1776,7 +1966,7 @@ first time a tool with an `outputSchema` is validated.
1776
1966
  run's disposition rows ride along, so "the checkers ran and found nothing"
1777
1967
  and "no checker was registered for that seam" stay different answers, and
1778
1968
  a registered check with zero encounters is named as wiring rot. Honest
1779
- absence has its own sentences: no event tail is *no finding evidence*, a
1969
+ absence has its own sentences: no event tail is _no finding evidence_, a
1780
1970
  tail with no integrity events says the channel is empty and why, and rows
1781
1971
  reporting findings the tail no longer carries say **evidence missing** —
1782
1972
  none of them ever reads as "no context errors found". Synthetic canaries
@@ -1812,7 +2002,7 @@ first time a tool with an `outputSchema` is validated.
1812
2002
  - **`Tool.argumentsFrom` + the dangling-reference check** (the closure
1813
2003
  check's decidable fragment). A tool author can now declare where a tool's
1814
2004
  arguments come from — `defineTool({ name: 'screen_fire', argumentsFrom:
1815
- ['whats_here'] })` — and `callLLM` checks at request assembly that every
2005
+ ['whats_here'] })` — and `callLLM` checks at request assembly that every
1816
2006
  served tool's declared grounds still have results in the window: a ground
1817
2007
  the window ledger says was evicted (`droppedObservations`) with nothing
1818
2008
  re-established files a `dangling-reference` finding at seam `'compose'`,
@@ -1856,10 +2046,10 @@ first time a tool with an `outputSchema` is validated.
1856
2046
 
1857
2047
  - **The assertion algebra and the one visible finding type.** An
1858
2048
  `Assertion` is keyed by `(subject, predicate, epoch)` with two rules
1859
- that do the work: *serving is asserting; history is quotation* (checks
2049
+ that do the work: _serving is asserting; history is quotation_ (checks
1860
2050
  never fire across the quoted stratum — the whole stale-but-honest
1861
- false-positive class, closed structurally) and *single-valued by
1862
- default* (you declare exemptions, never rules). Unknown `Claim`s never
2051
+ false-positive class, closed structurally) and _single-valued by
2052
+ default_ (you declare exemptions, never rules). Unknown `Claim`s never
1863
2053
  participate in a comparison. `conflictsOf()` is the pure exclusion
1864
2054
  comparison; `ContextError` is the uniform finding — plain kinds
1865
2055
  (`invariant-violation`, `unsupported-argument`, `dangling-reference`,
@@ -1913,16 +2103,16 @@ when this patch was cut, and they are additive.
1913
2103
  ### Fixed
1914
2104
 
1915
2105
  - **The cache report no longer invents a cause for a turn it could not
1916
- measure.** *What changed:* when `cacheRecorder().report()` hands back an
2106
+ measure.** _What changed:_ when `cacheRecorder().report()` hands back an
1917
2107
  unknown hit rate, the sentence attached to it is now the one the calls
1918
2108
  themselves gave — most often "no CacheStrategy was given to
1919
2109
  `cacheRecorder()`, so nothing read the usage". When the calls disagree about
1920
2110
  why, the summary says they disagreed and lists the reasons (up to three, then
1921
- a count of the rest) rather than silently choosing one. *Why it was not
1922
- there:* the summary sentence was typed into the code as a fixed string, "the
2111
+ a count of the rest) rather than silently choosing one. _Why it was not
2112
+ there:_ the summary sentence was typed into the code as a fixed string, "the
1923
2113
  provider reported no cache fields" — true for the case the author had in
1924
2114
  mind, a guess for every other, and it overwrote what each call had already
1925
- stated. *How it improves:* run without a strategy against a provider that DID
2115
+ stated. _How it improves:_ run without a strategy against a provider that DID
1926
2116
  report cache traffic, and 9.59.0 told you your provider was reporting
1927
2117
  nothing — sending you off to debug a provider that was working fine, instead
1928
2118
  of naming the one line missing from your own setup. You now read the real
@@ -1945,113 +2135,113 @@ the published bytes and is unchanged.
1945
2135
 
1946
2136
  ### Fixed
1947
2137
 
1948
- - **A red gate could reach npm, and did.** *What changed:* the docs-truth
2138
+ - **A red gate could reach npm, and did.** _What changed:_ the docs-truth
1949
2139
  ratchet now runs inside the build job that the publishing job depends on,
1950
2140
  so a red gate fails the build and the publish never starts — however the
1951
- release was created. *Why it was not there:* the gate existed, but CI ran it
2141
+ release was created. _Why it was not there:_ the gate existed, but CI ran it
1952
2142
  on `push` while the publish workflow runs on `release: published`, and the
1953
2143
  release script fires the release seconds after the push. The two raced, and
1954
- npm never waited for a verdict. *How it improves:* 9.58.0 shipped with a red
2144
+ npm never waited for a verdict. _How it improves:_ 9.58.0 shipped with a red
1955
2145
  ratchet; that is now structurally impossible.
1956
- - **A generated report was being edited by hand.** *What changed:*
2146
+ - **A generated report was being edited by hand.** _What changed:_
1957
2147
  `npm run docs:truth:report` regenerates the report **without** touching the
1958
2148
  accepted-debt baseline, and a test fails if the committed report is not what
1959
- the generator produces. *Why it was not there:* the report could only be
2149
+ the generator produces. _Why it was not there:_ the report could only be
1960
2150
  regenerated by also re-recording the debt, so an author who merely wanted the
1961
2151
  numbers to match reality had to choose between accepting unrelated debt and
1962
2152
  editing the file. They edited the file: the 9.58.0 release commit changed
1963
2153
  "103 typed events" to "105" and nothing else, leaving the export count stale
1964
- and the ratchet red. *How it improves:* restating the truth and accepting
2154
+ and the ratchet red. _How it improves:_ restating the truth and accepting
1965
2155
  debt are now separate acts, and the file cannot silently drift.
1966
2156
  - **The cache meter reported 0% for turns that hit cache on every call.**
1967
- *What changed:* the strategies read the framework's normalised usage instead
2157
+ _What changed:_ the strategies read the framework's normalised usage instead
1968
2158
  of raw provider field names, and every number in the report is now a `Claim`
1969
- — a value that says how it knows itself. *Why it was not there:* the
2159
+ — a value that says how it knows itself. _Why it was not there:_ the
1970
2160
  strategies parsed `cache_read_input_tokens` off a value that has never
1971
2161
  carried it, so every field read as missing and nothing was recorded; and the
1972
2162
  report typed its totals as plain numbers, so "nobody measured" and "measured,
1973
2163
  and it was zero" looked identical. The test fixtures were themselves
1974
2164
  provider-shaped, which is how it survived a release with a green suite.
1975
- *How it improves:* a real hit rate, an unmeasured turn that renders as
2165
+ _How it improves:_ a real hit rate, an unmeasured turn that renders as
1976
2166
  unmeasured, and a rate that always states its own denominator ("3 of 20
1977
2167
  calls"). A **silent non-cache** — a prompt below the model's minimum
1978
2168
  cacheable size, which providers process without caching and without an error
1979
2169
  — is now visibly different from a turn nobody measured.
1980
- - **A meter was attached to a provider that cannot feed it.** *What changed:*
2170
+ - **A meter was attached to a provider that cannot feed it.** _What changed:_
1981
2171
  the Bedrock strategy declares itself disabled, passes requests through
1982
- untouched, and answers "not applicable" with the reason. *Why it was not
1983
- there:* it claimed full support and clamped cache markers onto a request
2172
+ untouched, and answers "not applicable" with the reason. _Why it was not
2173
+ there:_ it claimed full support and clamped cache markers onto a request
1984
2174
  field our Bedrock provider discards, reporting markers that never reached a
1985
- wire. *How it improves:* it stays registered, so a Bedrock user is told the
2175
+ wire. _How it improves:_ it stays registered, so a Bedrock user is told the
1986
2176
  truth by name instead of guessing. The same honesty is applied to OpenAI,
1987
2177
  which is the costlier gap because it caches automatically.
1988
2178
 
1989
2179
  ### Changed
1990
2180
 
1991
- - **A parked map now tells the model it is parked.** *What changed:* while
2181
+ - **A parked map now tells the model it is parked.** _What changed:_ while
1992
2182
  anything is parked, the model receives a short status card naming the cursor
1993
2183
  and the engagement as **separate** things, the reason, and the way back as a
1994
- concrete call. *Why it was not there:* every honesty signal landed on the
2184
+ concrete call. _Why it was not there:_ every honesty signal landed on the
1995
2185
  record, which the model never reads — so re-engagement was reachable in
1996
2186
  principle and unreachable in practice, because nothing told the model that
1997
- re-picking a skill it appears to already be in means anything. *How it
1998
- improves:* a door the model can see. It also learns the distinction the
2187
+ re-picking a skill it appears to already be in means anything. _How it
2188
+ improves:_ a door the model can see. It also learns the distinction the
1999
2189
  kernel is built on instead of inferring a contradiction.
2000
- - **A parked map's tools actually leave the wire.** *What changed:* parking now
2190
+ - **A parked map's tools actually leave the wire.** _What changed:_ parking now
2001
2191
  holds the parked map's tool schemas off the request on its own authority.
2002
- *Why it was not there:* `.maps()` promised parking stops "the prompt fragment
2192
+ _Why it was not there:_ `.maps()` promised parking stops "the prompt fragment
2003
2193
  and tools", but on the default posture for flat graphs only the fragment
2004
2194
  stopped — the model was shown tools for a skill whose instructions had just
2005
- vanished. *How it improves:* the promise is true on every posture. This is
2195
+ vanished. _How it improves:_ the promise is true on every posture. This is
2006
2196
  **not** a change to `scopeTools` and does not touch the 10.0.0 ledger: those
2007
2197
  dials answer different questions.
2008
2198
  - **A pick of the skill you are already on is now a legal re-engagement.**
2009
- *What changed:* a `read_skill` pick is routed by intent — of a parked map's
2010
- member it re-engages the map and does not move the cursor. *Why it was not
2011
- there:* the reachability gate refuses a pick of the node the cursor occupies,
2199
+ _What changed:_ a `read_skill` pick is routed by intent — of a parked map's
2200
+ member it re-engages the map and does not move the cursor. _Why it was not
2201
+ there:_ the reachability gate refuses a pick of the node the cursor occupies,
2012
2202
  which is right for a move and wrong for this; since parking never moves the
2013
2203
  cursor, a parked map is parked exactly where the model wants to return. For a
2014
- single-member map that made parking permanent. *How it improves:* the
2204
+ single-member map that made parking permanent. _How it improves:_ the
2015
2205
  documented recovery door is real.
2016
- - **An engagement's founding cause is never rewritten.** *What changed:* the
2206
+ - **An engagement's founding cause is never rewritten.** _What changed:_ the
2017
2207
  record now keeps three separate facts — why the map is participating at all,
2018
2208
  why the cursor is on this member, and why this contribution is being served
2019
- right now. *Why it was not there:* one field answered all three, so
2209
+ right now. _Why it was not there:_ one field answered all three, so
2020
2210
  confirming a guess silently overwrote the guess, and a record founded on a
2021
- keyword at iteration 1 later read as system-backed since iteration 1. *How it
2022
- improves:* an incident review can still ask "was this founded on a guess?"
2211
+ keyword at iteration 1 later read as system-backed since iteration 1. _How it
2212
+ improves:_ an incident review can still ask "was this founded on a guess?"
2023
2213
  and get the true answer, however much has happened since.
2024
- - **A declared route no longer forges a user request.** *What changed:* when a
2214
+ - **A declared route no longer forges a user request.** _What changed:_ when a
2025
2215
  declared edge moves the cursor to a different member, that member's
2026
2216
  eligibility is worked out from **its own** evidence rather than inherited.
2027
- *Why it was not there:* the next member inherited the previous one's
2217
+ _Why it was not there:_ the next member inherited the previous one's
2028
2218
  standing, and an explicit request never decays — so one explicit pick at the
2029
2219
  top of a turn silently warranted every skill the graph walked to afterwards.
2030
- *How it improves:* a member entered weakly can park, exactly as if it had
2220
+ _How it improves:_ a member entered weakly can park, exactly as if it had
2031
2221
  been entered that way from the start. The founding cause still says, forever,
2032
2222
  that the engagement began explicitly on the skill that was actually asked for.
2033
- - **A new turn is judged on the new turn's evidence.** *What changed:* a cursor
2223
+ - **A new turn is judged on the new turn's evidence.** _What changed:_ a cursor
2034
2224
  carried into a new turn with nothing explaining it is recorded as `assumed`
2035
2225
  — nobody said why — which is the weakest rung and expires like any guess.
2036
- *Why it was not there:* an absent explanation was recorded as system-backed,
2226
+ _Why it was not there:_ an absent explanation was recorded as system-backed,
2037
2227
  the strongest and non-decaying category, so turn one's mistaken keyword match
2038
- became a permanent warrant on turn two and every turn after. *How it
2039
- improves:* cursor continuity and engagement continuity have independent
2228
+ became a permanent warrant on turn two and every turn after. _How it
2229
+ improves:_ cursor continuity and engagement continuity have independent
2040
2230
  lifetimes, and both are now pinned by tests — half the defect was that
2041
2231
  nothing pinned either.
2042
- - **The idle test checks all three of the conditions it documents.** *What
2043
- changed:* a map only accrues idle when its contribution was actually served,
2044
- none of its tools was called, and the turn went elsewhere. *Why it was not
2045
- there:* only the last was checked, while the refusal text asserted all three.
2046
- *How it improves:* nothing claims a fact it did not check. (Making it honest
2232
+ - **The idle test checks all three of the conditions it documents.** _What
2233
+ changed:_ a map only accrues idle when its contribution was actually served,
2234
+ none of its tools was called, and the turn went elsewhere. _Why it was not
2235
+ there:_ only the last was checked, while the refusal text asserted all three.
2236
+ _How it improves:_ nothing claims a fact it did not check. (Making it honest
2047
2237
  needed the kernel to carry its own record of what was served — reading it
2048
2238
  from the existing delta machinery was silently empty in the grouped chart
2049
2239
  shape, which would have disabled parking there entirely.)
2050
- - **A map that cannot explain its cursor moves is refused at mount.** *What
2051
- changed:* `.maps()` requires a map that reports why the cursor moved. *Why it
2052
- was not there:* without it, no explanation ever arrives, and a kernel whose
2053
- whole job is weighing evidence had none to weigh — silently. *How it
2054
- improves:* the failure is named at build time instead of at 3am.
2240
+ - **A map that cannot explain its cursor moves is refused at mount.** _What
2241
+ changed:_ `.maps()` requires a map that reports why the cursor moved. _Why it
2242
+ was not there:_ without it, no explanation ever arrives, and a kernel whose
2243
+ whole job is weighing evidence had none to weigh — silently. _How it
2244
+ improves:_ the failure is named at build time instead of at 3am.
2055
2245
 
2056
2246
  ### Added
2057
2247
 
@@ -2061,9 +2251,9 @@ the published bytes and is unchanged.
2061
2251
  shipped and was unreachable: the kernel's data model carried the field and
2062
2252
  no option ever set it. It suppresses the park, not the measurement — the
2063
2253
  record still shows a map riding every call unused.
2064
- - **Two documentation pages**: *Mounted maps* (the kernel's why, the evidence
2065
- ladder, the idle test, the three facts, turns, and the park card) and *The
2066
- cache meter* (how to read a report, and exactly which providers can feed it).
2254
+ - **Two documentation pages**: _Mounted maps_ (the kernel's why, the evidence
2255
+ ladder, the idle test, the three facts, turns, and the park card) and _The
2256
+ cache meter_ (how to read a report, and exactly which providers can feed it).
2067
2257
  - **A measurement, not a fix, for the prompt-cache cost of `read_skill`.** The
2068
2258
  tool's description is rebuilt from the cursor every iteration, and changing a
2069
2259
  tool definition invalidates a provider's entire prompt cache. A shipped test
@@ -2081,7 +2271,6 @@ through `isKnown(...)`, or render them with `describeClaim(...)`. This is a
2081
2271
  signature change in a minor release, and deliberately so: the old bare numbers
2082
2272
  were not a contract worth keeping, because the value was zero on every turn.
2083
2273
 
2084
-
2085
2274
  ## [9.58.0] - 2026-08-20
2086
2275
 
2087
2276
  **A map's cursor says where it stands. Nothing said whether it had earned
@@ -2177,7 +2366,7 @@ to do, and no longer had the evidence to do it.
2177
2366
  What it did next was invent. It took an entity name it remembered plus the
2178
2367
  shape of an id it had used earlier, assembled one that has never existed, and
2179
2368
  was refused — a wasted action out of a small budget. In one archived run the
2180
- final answer to the *person* named a host that appears in no tool result at
2369
+ final answer to the _person_ named a host that appears in no tool result at
2181
2370
  all.
2182
2371
 
2183
2372
  Nothing in the conversation said the evidence had gone. That is the release.
@@ -2200,7 +2389,7 @@ Nothing in the conversation said the evidence had gone. That is the release.
2200
2389
  already inside `keepRecentTurns` costs nothing at all. Nothing at or before
2201
2390
  the current request is pinnable, so a new user turn releases the whole
2202
2391
  previous loop. The floor is `1 request + keepLastToolResults pins +
2203
- keepRecentTurns turns`, whatever your tool count, iteration count or run
2392
+ keepRecentTurns turns`, whatever your tool count, iteration count or run
2204
2393
  length.
2205
2394
 
2206
2395
  And a pin that BLOCKS is worse than a pin that misses: when two consecutive
@@ -2211,9 +2400,9 @@ Nothing in the conversation said the evidence had gone. That is the release.
2211
2400
  `keepLastToolResults: false` (or `0`) reproduces 9.56.0 byte for byte.
2212
2401
 
2213
2402
  - **A drop now says whose results it took.** The authored notice gains one
2214
- sentence: *"Tool results are among them (whats_here, pan_view) — call the
2403
+ sentence: _"Tool results are among them (whats_here, pan_view) — call the
2215
2404
  tool again if you need its output; do not reconstruct ids or values from
2216
- memory."* The drop is now STATED rather than silent — whether that sentence
2405
+ memory."_ The drop is now STATED rather than silent — whether that sentence
2217
2406
  changes what a model does next is **not measured here**: the five archived
2218
2407
  runs have not been re-run with it on, so it ships as an honesty fix and not
2219
2408
  as a performance claim. Tool names are the only caller data that reaches it, and they
@@ -2239,13 +2428,13 @@ Nothing in the conversation said the evidence had gone. That is the release.
2239
2428
  > You are on action 25 of 30; 5 remain. Finish what you have rather than
2240
2429
  > start something new.
2241
2430
 
2242
- Measured, not decorative: given its remaining budget a model wrote *"I have 5
2243
- steps left, enough to finish this properly"* and landed the task, where
2431
+ Measured, not decorative: given its remaining budget a model wrote _"I have 5
2432
+ steps left, enough to finish this properly"_ and landed the task, where
2244
2433
  before it spiralled and produced no answer at all.
2245
2434
 
2246
2435
  The vocabulary is closed rather than a `(ctx) => string` because of
2247
2436
  **absence**. Given a function, an author writes `${ctx.maxIterations}` and
2248
- ships *"23 of undefined"*, or writes `?? 0` and ships a fabricated
2437
+ ships _"23 of undefined"_, or writes `?? 0` and ships a fabricated
2249
2438
  denominator that nothing — and no model — can tell from a real zero. With
2250
2439
  named slots the library owns absence and applies one rule: if any named fact
2251
2440
  is unavailable, the whole instruction is skipped, by name, as
@@ -2269,7 +2458,7 @@ Nothing in the conversation said the evidence had gone. That is the release.
2269
2458
  message that would become the head was already a user turn (the pinned
2270
2459
  request, or an older turn of a restored conversation) no notice was owed at
2271
2460
  all. Its 245–358 characters were nevertheless allowed to veto a legitimate
2272
- drop; and because the removable span is the longest *contiguous* run, the
2461
+ drop; and because the removable span is the longest _contiguous_ run, the
2273
2462
  same verdict came back at every boundary while the window grew without
2274
2463
  bound.
2275
2464
 
@@ -2474,11 +2663,11 @@ they do.
2474
2663
  `unknown`, so a surface that guessed at it would put words in a tool's
2475
2664
  mouth. One rule, and it is the whole rule:
2476
2665
 
2477
- | your payload | the line a person reads |
2478
- | --- | --- |
2479
- | `{ message: 'Hop 3 of 12' }` | `Hop 3 of 12` — your sentence, verbatim |
2480
- | `{ done: 3, total: 12 }` | `` `walk_graph` reported progress (3 so far)… `` |
2481
- | `'a bare string'` | the same generic line |
2666
+ | your payload | the line a person reads |
2667
+ | ---------------------------- | ------------------------------------------------ |
2668
+ | `{ message: 'Hop 3 of 12' }` | `Hop 3 of 12` — your sentence, verbatim |
2669
+ | `{ done: 3, total: 12 }` | `` `walk_graph` reported progress (3 so far)… `` |
2670
+ | `'a bare string'` | the same generic line |
2482
2671
 
2483
2672
  A top-level string field named **`message`** is shown verbatim, trimmed, and
2484
2673
  cut at **120 characters** with the cut stated (`… (+N more)`) — `message` is
@@ -2504,8 +2693,8 @@ they do.
2504
2693
  `tool_end` used to be able to clear a SIBLING call's status, leaving the
2505
2694
  bubble blank while a tool was still working.
2506
2695
 
2507
- - **Commentary narrates the middle**, so recordings replay it: *"The
2508
- `walk_graph` tool reported progress while it was still running."* The
2696
+ - **Commentary narrates the middle**, so recordings replay it: _"The
2697
+ `walk_graph` tool reported progress while it was still running."_ The
2509
2698
  teaching voice states the fact and never the payload — the same split the
2510
2699
  Lens teaching view keeps, in the same words.
2511
2700
 
@@ -2559,8 +2748,10 @@ tool's name on it.
2559
2748
  series: [{ t: '2026-08-19T10:00:00Z', entity: 'fc1/3', metric: 'avg_iops', value: 18450 }],
2560
2749
  grain: { interval: '30m', aggregation: 'avg', is_counter: false },
2561
2750
  provenance: { measured_at: '2026-08-19T10:20:00Z', source: 'InfluxDB SwitchPortStats' },
2562
- coverage: { checked: ['fabric A: all 48 ports'],
2563
- notChecked: [{ what: 'the peer fabric', why: 'collector scoped to one fabric' }] },
2751
+ coverage: {
2752
+ checked: ['fabric A: all 48 ports'],
2753
+ notChecked: [{ what: 'the peer fabric', why: 'collector scoped to one fabric' }],
2754
+ },
2564
2755
  render: { default: 'table', columns: ['entity', 'value'], sort: 'value desc' },
2565
2756
  });
2566
2757
  ```
@@ -2612,7 +2803,7 @@ tool's name on it.
2612
2803
  - Zero-cost when unused: recognition is strict (`af_semantics: true` AND the
2613
2804
  whole rule set), so every value any tool has ever returned keeps its bytes.
2614
2805
  Runnable example: `examples/features/66-semantic-envelope.ts`. Guide:
2615
- docs *Build → Semantic tool results*.
2806
+ docs _Build → Semantic tool results_.
2616
2807
 
2617
2808
  ## [9.52.0] - 2026-08-19
2618
2809
 
@@ -2692,7 +2883,7 @@ opaque function on a route edge.
2692
2883
 
2693
2884
  - **`guard:` on route edges — guards as data (the `when` predicate's declared
2694
2885
  twin).** `.route(a, b, { guard: { riskLevel: { in: ['high','critical'] },
2695
- score: { gte: 0.7 } } })` — conditions over the hop (`toolName`, `result`,
2886
+ score: { gte: 0.7 } } })` — conditions over the hop (`toolName`, `result`,
2696
2887
  `status`, `iteration`, `userMessage`, `currentSkillId`) and over the tool
2697
2888
  result's own top-level JSON fields, operators `eq/ne/gt/gte/lt/lte/in/notIn`
2698
2889
  (deliberately footprintjs's `WhereFilter` grammar, mirrored door-locally —
@@ -2702,6 +2893,7 @@ opaque function on a route edge.
2702
2893
  outcome, AND these conditions"). ONE compilation produces the predicate
2703
2894
  that routes, the serializable `SkillGuardData`, and the evidence evaluator
2704
2895
  — so the three can never describe different guards. What being data buys:
2896
+
2705
2897
  - **the check-up proves contradictions** — new ERROR
2706
2898
  `guard-unsatisfiable`: crossed bounds (`gt: 5, lt: 3`), `eq` a same-key
2707
2899
  `ne`/`in`/`notIn` excludes, a `status` outside the closed result-status
@@ -2722,7 +2914,7 @@ opaque function on a route edge.
2722
2914
  evaluation: `guard` on the taken hop (verdict `true`) and `guardsClosed`
2723
2915
  for refusals (verdict `false`, at most one per edge per iteration, on
2724
2916
  whatever move resulted — a stay says `score gte 0.7 — saw "0.2" →
2725
- failed`). Agents without guards keep byte-identical events.
2917
+ failed`). Agents without guards keep byte-identical events.
2726
2918
 
2727
2919
  - **SkillMap & SkillWalker are now the official names.** You declare the
2728
2920
  **SkillMap**; the agent is the **SkillWalker**; the recording carries both.
@@ -2805,7 +2997,7 @@ is the whole release.
2805
2997
  ### Notes for recording consumers (the lens, triage platforms)
2806
2998
 
2807
2999
  - Draw the declared topology from `skill.graph_declared` (`declaredSource:
2808
- 'recording'` can now mean COMPLETE); filter `from !== null` for
3000
+ 'recording'` can now mean COMPLETE); filter `from !== null` for
2809
3001
  node-to-node edges, exactly as with a built graph's `edges`.
2810
3002
  - Fill per-beat reachability from `cursorMove.reachable` first; the refusal's
2811
3003
  `allowed` and declared-edge folds remain as fallbacks for older eras.
@@ -2864,7 +3056,7 @@ is the whole release.
2864
3056
  `recording.json`, the manifest carries a note naming the fact and the line
2865
3057
  that supplies it, and `BugReportUnit.enveloped` says per conversation which
2866
3058
  shape it got. Nothing is stamped that was not known.
2867
- - **The evidence is never packed twice** — an envelope *or* a bare recording,
3059
+ - **The evidence is never packed twice** — an envelope _or_ a bare recording,
2868
3060
  never both. The zip is store-only, so a duplicated recording is duplicated
2869
3061
  bytes against the size ceiling the trim hints exist to keep a reporter under.
2870
3062
  - The GitHub issue body names the file that is really in the bundle, including
@@ -3415,10 +3607,9 @@ answer for. These are ours, built from that evidence. The credit is the field's.
3415
3607
  ### Fixed
3416
3608
 
3417
3609
  - Two unanchored `coverage` rules in `.gitignore` matched `src/core/agent/
3418
- coverage/`, so a new source directory would have been absent from every clone.
3610
+ coverage/`, so a new source directory would have been absent from every clone.
3419
3611
  Anchored to the vitest report directory, with a comment naming why.
3420
3612
 
3421
-
3422
3613
  ## [9.42.0] - 2026-08-15
3423
3614
 
3424
3615
  **Comparing strategies with statistics, and retention on the session port.**
@@ -3504,7 +3695,6 @@ answer for. These are ours, built from that evidence. The credit is the field's.
3504
3695
  from a counter, so they differ in their prefix and the fold never shows.
3505
3696
  Fixing it re-keys stored sessions. Next release.
3506
3697
 
3507
-
3508
3698
  ## [9.41.0] - 2026-08-15
3509
3699
 
3510
3700
  **Comparing strategies, and proving a store.** An audit asked whether this
@@ -3574,7 +3764,6 @@ key, and the proof obligations for the second port.
3574
3764
  - `MemoryDefinition` gains optional declared `strategy`, `retrieval` and
3575
3765
  `embedderId` — additive, and what makes a memory row worth grouping on.
3576
3766
 
3577
-
3578
3767
  ## [9.40.0] - 2026-08-15
3579
3768
 
3580
3769
  **Two identity bugs, one of which had been paying out zeros since v2.8.**
@@ -3625,7 +3814,6 @@ key, and the proof obligations for the second port.
3625
3814
  `provider` now flows through to `CostTick` as well (optional; absent stays
3626
3815
  absent rather than becoming `'unknown'`).
3627
3816
 
3628
-
3629
3817
  ## [9.39.0] - 2026-08-14
3630
3818
 
3631
3819
  **Three false doors closed, and two promises the recording did not keep.** An
@@ -3664,7 +3852,7 @@ forking. Mostly yes — but the first thing such an author reads was wrong.
3664
3852
  was reserved. Not re-exported from any barrel, not an `exports` subpath, not
3665
3853
  in the shipped-surface baseline — so nothing public could reach it and its
3666
3854
  removal is not breaking. The by-instance door (`agent.enable.observability({
3667
- strategy })`) is real, first-class, and now the only one. The cache twin
3855
+ strategy })`) is real, first-class, and now the only one. The cache twin
3668
3856
  legitimately has two doors; this one had one door and a sign pointing at a
3669
3857
  wall.
3670
3858
 
@@ -3688,7 +3876,6 @@ forking. Mostly yes — but the first thing such an author reads was wrong.
3688
3876
  `enable.cost()` receives zeros. This release makes `model` resolve there; the
3689
3877
  numbers are still zero. Its own ticket.
3690
3878
 
3691
-
3692
3879
  ## [9.38.0] - 2026-08-14
3693
3880
 
3694
3881
  **Two behaviour changes that are bug fixes — a call that used to run now
@@ -4056,12 +4243,13 @@ labels." Ours had no rule at all. Now it does, and it is enforced, not
4056
4243
  promised.
4057
4244
 
4058
4245
  ### What it is, and — because the name invites the wrong reading — what it
4246
+
4059
4247
  ### is provably not
4060
4248
 
4061
4249
  It is a **fabrication detector, not a correctness judge.** If a value in
4062
4250
  the answer never appeared in anything a tool returned this turn, the model
4063
4251
  typed it rather than read it, and that is all this checks. It cannot catch
4064
- a false claim built entirely from real values — *"fc1/3 is healthy"* when
4252
+ a false claim built entirely from real values — _"fc1/3 is healthy"_ when
4065
4253
  the data says the port is down uses two grounded tokens and sails through.
4066
4254
  It cannot catch a real value attached to the wrong thing, a fabricated
4067
4255
  quantity under the digit threshold, or a fabricated name spelled in
@@ -4116,7 +4304,7 @@ is not judged at all, and a turn the iteration or cost limit just cut
4116
4304
  short does not get to spend a revision it can't afford. It composes with,
4117
4305
  rather than collides with, `.reliability()`: reliability governs whether a
4118
4306
  call is retried before anything is committed; this governs an answer
4119
- *after* it has already been committed to the transcript.
4307
+ _after_ it has already been committed to the transcript.
4120
4308
 
4121
4309
  ### Measured, not assumed
4122
4310
 
@@ -4195,7 +4383,7 @@ never touched `footprintjs` itself but imports the file that now did.
4195
4383
  - `isDevMode` (a `footprintjs` import) → a bound `devWarn()` / `devMode()`
4196
4384
  reader that the host supplies; every existing warning reads verbatim, and
4197
4385
  the existing `enableDevMode()` tests pass unchanged — the proof that
4198
- nothing about *what gets warned* moved, only *how it's asked*.
4386
+ nothing about _what gets warned_ moved, only _how it's asked_.
4199
4387
  - `ToolResultStatus` → pulled out to a zero-import leaf and re-exported from
4200
4388
  its old home, so the envelope grammar is unchanged for every existing
4201
4389
  caller. The fence caught four more inline crossings of this type that
@@ -4358,7 +4546,7 @@ was in the bundle — an empty bundle read as "nobody was turned away" when
4358
4546
  it only meant "nobody ran." `standingAgent({ onIngressDecision })` now
4359
4547
  hands your sink one `IngressRecord` per request, filed at the terminal the
4360
4548
  reply actually reached. The honest contract is stated in the type itself:
4361
- `'served'` means **delivered**, not *admitted* — a request the door let
4549
+ `'served'` means **delivered**, not _admitted_ — a request the door let
4362
4550
  through whose run, store or provider then broke files as `'failed'`, and
4363
4551
  the record carries the admission verdict (`allow` / `queue` / `refuse`)
4364
4552
  either way. It is a stream you chain into your own sink, not a join onto
@@ -4490,7 +4678,7 @@ read/write." Both, plus `decayPolicy`, are now carried under prefixed
4490
4678
  metadata keys and restored verbatim on read. A caller's own value under one
4491
4679
  of the three keys this adapter generates (`source`, `resourceName`,
4492
4680
  `distance`) is refused by name rather than silently shadowed — recognized by
4493
- *identity*, not shape, so a caller's own `distance: 12` cannot be mistaken
4681
+ _identity_, not shape, so a caller's own `distance: 12` cannot be mistaken
4494
4682
  for this adapter's. An oversized carried field is refused rather than
4495
4683
  truncated: provenance that came back shortened would be provenance nobody
4496
4684
  could tell was shortened.
@@ -4680,7 +4868,7 @@ effects. Every line follows the same rules as the rest of the layer:
4680
4868
  retry happened — no event attests one, so the words don't either.
4681
4869
  - **Sizes humanized, and the two units told apart.** `humanizeBytes` /
4682
4870
  `humanizeChars` — `41.0 KB` where the ceiling counts bytes, `240,000
4683
- characters` where it counts characters (`tools.result_refused` counts
4871
+ characters` where it counts characters (`tools.result_refused` counts
4684
4872
  characters, because that's what the limit does).
4685
4873
  - **Refs and digests stay out of prose.** They identify a row for the details
4686
4874
  panel; a reader doesn't read them. The repeated-call nudge's fingerprints
@@ -4734,8 +4922,8 @@ import { googleIdentity } from 'agentfootprint/security';
4734
4922
  now declares the same `ranksBy: 'server-text'` for the identical reason, so
4735
4923
  the two server-ranked stores no longer disagree on how they say what they
4736
4924
  are.)
4737
- - **`googleIdentity`** — a narrow `CredentialProvider`: it vends *Google*
4738
- access tokens for *Google* APIs from whatever credential the environment
4925
+ - **`googleIdentity`** — a narrow `CredentialProvider`: it vends _Google_
4926
+ access tokens for _Google_ APIs from whatever credential the environment
4739
4927
  already has (ADC, workload identity, an impersonated service account).
4740
4928
  `mode: 'user'` is refused by name rather than quietly served a machine
4741
4929
  token, since Google's user-token equivalent has no Node surface yet.
@@ -4810,7 +4998,9 @@ provider scopes a vault on:
4810
4998
 
4811
4999
  ```ts
4812
5000
  await standingAgent({
4813
- agent, sessions, host: nodeHost({ port: 8080 }),
5001
+ agent,
5002
+ sessions,
5003
+ host: nodeHost({ port: 8080 }),
4814
5004
  identity: {
4815
5005
  verify: jwksIdentity({
4816
5006
  jwksUrl: 'https://idp.example.com/.well-known/jwks.json',
@@ -4823,7 +5013,7 @@ await standingAgent({
4823
5013
 
4824
5014
  **Configured is closed-by-default.** A request with no `Authorization`
4825
5015
  header is refused (401) unless `allowAnonymous: true` is set, and a request
4826
- that *names* a `userId` without proving it is refused either way — a door
5016
+ that _names_ a `userId` without proving it is refused either way — a door
4827
5017
  that verifies a token when offered and waves the request through when it is
4828
5018
  not is a door anybody opens by sending less. `jwksIdentity` is the one
4829
5019
  adapter this release ships (`jose`, loaded lazily, pinned against a real
@@ -4842,7 +5032,9 @@ Nothing else reads them; they do not enter the run's own identity tuple.
4842
5032
 
4843
5033
  ```ts
4844
5034
  await standingAgent({
4845
- agent, sessions, host,
5035
+ agent,
5036
+ sessions,
5037
+ host,
4846
5038
  identity: { verify },
4847
5039
  admission: turnsPerHour({ limit: 60 }),
4848
5040
  });
@@ -5027,8 +5219,12 @@ wire, scoped to the session that asks.**
5027
5219
  ### Added — two wire operations on the existing invoke path
5028
5220
 
5029
5221
  ```ts
5030
- { op: 'artifact-head', ref } // → meta
5031
- { op: 'artifact-get', ref } // → meta + data
5222
+ {
5223
+ op: 'artifact-head', ref;
5224
+ } // → meta
5225
+ {
5226
+ op: 'artifact-get', ref;
5227
+ } // → meta + data
5032
5228
  ```
5033
5229
 
5034
5230
  Resolved under the requesting session's identity-composed scope — exactly
@@ -5079,7 +5275,9 @@ lines.**
5079
5275
  defineTool({
5080
5276
  name: 'summarize',
5081
5277
  wants: { dataset: 'dataset/rows' },
5082
- execute: async (args, ctx) => { /* args.dataset is the RESOLVED DATA */ },
5278
+ execute: async (args, ctx) => {
5279
+ /* args.dataset is the RESOLVED DATA */
5280
+ },
5083
5281
  });
5084
5282
  ```
5085
5283
 
@@ -5267,7 +5465,7 @@ on `llm_start`.
5267
5465
  Precedence is stated and enforced, most specific wins: **escalation
5268
5466
  brain > per-skill brain > `.configure()`'s run model > the build
5269
5467
  default.** A brain naming only a model inherits the agent's own provider;
5270
- a brain naming a *foreign* provider without a model is refused at
5468
+ a brain naming a _foreign_ provider without a model is refused at
5271
5469
  `Agent.build()` — the run's configured model belongs to another vendor's
5272
5470
  namespace and would fail mid-turn, on exactly the iteration the cursor
5273
5471
  enters the skill.
@@ -5304,12 +5502,12 @@ returns today — stay byte-identical; the envelope is recognized only by
5304
5502
  its own strict shape.
5305
5503
 
5306
5504
  - **`propose-transition`** — `{ kind: 'propose-transition', targetSkillId,
5307
- reason }`. The typed replacement for a string routing marker: the
5308
- *graph* decides. A same-batch declared edge still wins; an unreachable
5505
+ reason }`. The typed replacement for a string routing marker: the
5506
+ _graph_ decides. A same-batch declared edge still wins; an unreachable
5309
5507
  target is refused out loud, not silently dropped.
5310
5508
  - **`require-instruction`** — `{ kind: 'require-instruction',
5311
- instructionId, deliveryLease: 'next-call' | 'until-skill-exit' }`.
5312
- Pushes a *registered* instruction into the coming call(s) —
5509
+ instructionId, deliveryLease: 'next-call' | 'until-skill-exit' }`.
5510
+ Pushes a _registered_ instruction into the coming call(s) —
5313
5511
  `read_skill` stays the pull door for optional knowledge; this is the
5314
5512
  push door for mandatory procedure, and it only pushes what was
5315
5513
  registered at build. An unknown id is refused, never improvised.
@@ -5493,7 +5691,7 @@ unmatched, never as an uncontested winner.
5493
5691
  ### Zero-cost when unused
5494
5692
 
5495
5693
  Graphs that use none of the new options are byte-identical in behavior
5496
- *and* events to 9.16.0 — pinned by regression tests. 78+ new tests cover
5694
+ _and_ events to 9.16.0 — pinned by regression tests. 78+ new tests cover
5497
5695
  the cascade, the scorers, the strictness postures, and continuity.
5498
5696
 
5499
5697
  ### Deferred
@@ -5528,6 +5726,7 @@ ordering bug with no error, no log line, just a silently different cursor.
5528
5726
 
5529
5727
  On the record, so a trace answers "why didn't the second call route?"
5530
5728
  instead of leaving the reader to guess.
5729
+
5531
5730
  - Single-tool iterations are byte-identical to 9.15.0 — this only changes
5532
5731
  behavior when a batch actually contains more than one result.
5533
5732
  - New `AgentState.toolResults` / `InjectionContext.toolResults`: the full
@@ -5542,7 +5741,7 @@ ordering bug with no error, no log line, just a silently different cursor.
5542
5741
  Wiring a `.skillGraph(...)` onto a classic agent meant the graph would still
5543
5742
  route and the trace would still show an activation — but the model never saw
5544
5743
  the newly-active skill's prompt or tools, because the slot it would have
5545
- changed was already frozen. The configuration *looked* like it worked and
5744
+ changed was already frozen. The configuration _looked_ like it worked and
5546
5745
  didn't.
5547
5746
 
5548
5747
  `Agent.build()` now refuses this combination outright, naming both the
@@ -5596,7 +5795,7 @@ const graph = skillGraph({
5596
5795
 
5597
5796
  What previously required `autoActivate: 'currentSkill'` typed on every single
5598
5797
  skill is now one graph-level line. A skill's own explicit `autoActivate` always
5599
- wins — the graph sets a default, never an override. Only *wired* skills (named
5798
+ wins — the graph sets a default, never an override. Only _wired_ skills (named
5600
5799
  by an entry or a route) are stamped: an unwired skill's tools would otherwise
5601
5800
  never appear at all. With the dial absent or `false`, compiled skills are
5602
5801
  byte-identical to 9.14.0 (pinned by test). On a `tree()` graph the flat-arm
@@ -5677,10 +5876,10 @@ const memory = defineMemory({
5677
5876
  type: MEMORY_TYPES.EPISODIC,
5678
5877
  strategy: {
5679
5878
  kind: MEMORY_STRATEGIES.SUMMARIZE,
5680
- recent: 6, // the 6 newest entries stay verbatim
5681
- size: 20, // how much history to load per turn
5682
- llm: anthropic(), // its OWN instance, not the agent's
5683
- model: 'claude-haiku-4-5', // named explicitly — no fallback
5879
+ recent: 6, // the 6 newest entries stay verbatim
5880
+ size: 20, // how much history to load per turn
5881
+ llm: anthropic(), // its OWN instance, not the agent's
5882
+ model: 'claude-haiku-4-5', // named explicitly — no fallback
5684
5883
  },
5685
5884
  store,
5686
5885
  });
@@ -5710,8 +5909,8 @@ quietly bills your MAIN model for compression, and a different vendor is sent a
5710
5909
  model id it has never heard of, mid-conversation, on a paid run.
5711
5910
 
5712
5911
  `Agent.memory()` now also refuses a summarizer that is the agent's own provider
5713
- **instance** at the agent's own model (the narrow 8.14.0 rule; a *second
5714
- instance* of the same vendor at the same model is allowed and sometimes right).
5912
+ **instance** at the agent's own model (the narrow 8.14.0 rule; a _second
5913
+ instance_ of the same vendor at the same model is allowed and sometimes right).
5715
5914
  `defineMemory` cannot make that check — it has never heard of an agent — so a
5716
5915
  `MemoryDefinition` now declares `billing: { provider, model }` and the builder
5717
5916
  reads it, the same field and shape `WindowStrategy.billing` already used. One
@@ -5824,7 +6023,7 @@ why, with a date.
5824
6023
  import { gemini } from 'agentfootprint/providers';
5825
6024
 
5826
6025
  const vertex = gemini({ project: 'my-project', location: 'us-central1' }); // ADC
5827
- const studio = gemini({ apiKey: process.env.GEMINI_API_KEY! }); // one key
6026
+ const studio = gemini({ apiKey: process.env.GEMINI_API_KEY! }); // one key
5828
6027
  ```
5829
6028
 
5830
6029
  Two doors, one adapter, and neither is guessed: a project selects Vertex, a key
@@ -5877,8 +6076,8 @@ Three decisions worth reading before you rely on them:
5877
6076
  that arrives anyway is kept out of the visible answer on both paths.
5878
6077
  - **A stream that reports no usage reports ZERO, never an estimate.**
5879
6078
  `models.countTokens` is on the namespace, is not called, and is named in the pin
5880
- as not called: it answers what a request *tokenises to*, not what the call was
5881
- *billed for*. Same law as `openai()` and `ollama()`. (Usage is read off the
6079
+ as not called: it answers what a request _tokenises to_, not what the call was
6080
+ _billed for_. Same law as `openai()` and `ollama()`. (Usage is read off the
5882
6081
  closing chunk BEFORE any content guard — the bug that made streamed turns bill
5883
6082
  as zero on two earlier adapters.)
5884
6083
 
@@ -5950,8 +6149,8 @@ new and load-bearing.
5950
6149
 
5951
6150
  ### Added — Gemini's over-long-request sentence joins the typed error
5952
6151
 
5953
- `ContextWindowExceededError` (9.6.0) now translates *"The input token count
5954
- (1200293) exceeds the maximum number of tokens allowed (1048576)."* — a word order
6152
+ `ContextWindowExceededError` (9.6.0) now translates _"The input token count
6153
+ (1200293) exceeds the maximum number of tokens allowed (1048576)."_ — a word order
5955
6154
  the existing patterns did not match — and reads both numbers out of it, including
5956
6155
  the case where Google ships the first parenthesis empty. Detection stays
5957
6156
  conservative: "INPUT token count" is what keeps it off a `max_tokens` validation
@@ -6000,8 +6199,8 @@ concurrency-and-sessions section, and a status row per boundary.
6000
6199
  - A `gemini()` error never prints the API key it was constructed with. The
6001
6200
  redaction is narrow by design — the exact string you passed, removed from the
6002
6201
  message, the stack and the wrapped cause — and is not a heuristic scrubber:
6003
- a thrown provider error reaches the model as a tool result *and* the commit log
6004
- *and* every observability sink, so one interpolation would leak to all of them.
6202
+ a thrown provider error reaches the model as a tool result _and_ the commit log
6203
+ _and_ every observability sink, so one interpolation would leak to all of them.
6005
6204
 
6006
6205
  ## [9.12.0] - 2026-08-12
6007
6206
 
@@ -6022,7 +6221,7 @@ provider signed:
6022
6221
  const credentials = agentCoreIdentity({
6023
6222
  region: 'us-west-2',
6024
6223
  workloadName: 'workflow_assistant_agent',
6025
- requireUserToken: true, // optional — refuse a delegated call with no proof
6224
+ requireUserToken: true, // optional — refuse a delegated call with no proof
6026
6225
  });
6027
6226
 
6028
6227
  // inside a tool
@@ -6037,7 +6236,7 @@ await ctx.credentials.getCredential({ service: 'google', mode: 'user', userToken
6037
6236
  - **Nothing downstream changed.** `GetWorkloadAccessTokenForJWT` answers with the
6038
6237
  same `workloadAccessToken` the by-userId exchange does, so it feeds the same
6039
6238
  `GetResourceOauth2Token` call, the same `Credential`, the same `toHeaders()`.
6040
- The vault entry at the end belongs to the *person* rather than to the agent,
6239
+ The vault entry at the end belongs to the _person_ rather than to the agent,
6041
6240
  which is what makes revoking their access actually revoke it.
6042
6241
  - **Verified against the real SDK before it shipped**, names and shapes both:
6043
6242
  `{ workloadName, userToken }` in, `{ workloadAccessToken }` out. It joins the
@@ -6121,7 +6320,7 @@ refuses a call it used to allow, or emits a field it did not before.
6121
6320
  ### Added — `maxToolResultChars`: a ceiling on ONE tool result
6122
6321
 
6123
6322
  ```ts
6124
- Agent.create({ provider, model, maxToolResultChars: 20_000 })
6323
+ Agent.create({ provider, model, maxToolResultChars: 20_000 });
6125
6324
  ```
6126
6325
 
6127
6326
  Over the cap, the result is **replaced** by a marker that tells the model what
@@ -6161,8 +6360,8 @@ happened and what to do about it:
6161
6360
  ### Added — WHO the run was for, on every event
6162
6361
 
6163
6362
  `EventMeta.principal` and `EventMeta.tenant` (9.11.0) join `sessionId` (9.4.0) on
6164
- every event's meta. The stream has always said *what* happened and *when*; this
6165
- is the *who*, and the three together are an audit record rather than a debug log.
6363
+ every event's meta. The stream has always said _what_ happened and _when_; this
6364
+ is the _who_, and the three together are an audit record rather than a debug log.
6166
6365
 
6167
6366
  ```ts
6168
6367
  await agent.run(message, {
@@ -6183,7 +6382,7 @@ await agent.run(message, {
6183
6382
  and `sessionId` beside it is the fact the transport delivered.
6184
6383
  - **Which sinks carry it, checked rather than assumed.** `fileObservability`,
6185
6384
  `cloudwatchObservability`, `agentcoreObservability` and `auditExport` serialize
6186
- the whole envelope and inherit it for free — in `auditExport`'s case *inside*
6385
+ the whole envelope and inherit it for free — in `auditExport`'s case _inside_
6187
6386
  the hash chain, so editing who breaks the same verification as editing what.
6188
6387
  `otelObservability` maps signals onto spans rather than serializing, so the
6189
6388
  actor is PLACED there: `agentfootprint.principal.id` /
@@ -6209,7 +6408,7 @@ const policy = PermissionPolicy.fromRoles(roles, 'support', {
6209
6408
  ```
6210
6409
 
6211
6410
  - **`Tool.capabilities`** (`'memory_read' | 'memory_write' | 'external_net' |
6212
- 'user_data'`) is a declaration, never an inference. A tool's reach is not
6411
+ 'user_data'`) is a declaration, never an inference. A tool's reach is not
6213
6412
  knowable from its name, schema or description, and guessing would rest a policy
6214
6413
  decision on a heuristic.
6215
6414
  - **`PermissionChecker.governs`** is an optional, feature-detected member —
@@ -6244,8 +6443,8 @@ PermissionPolicy.fromRoles(roles, 'support', {
6244
6443
  - The refusal lands **before `execute`**, so a `surfaceMode: 'tool-only'` skill's
6245
6444
  body is never even computed.
6246
6445
  - **Hidden means unnamed.** The graph offer lists unreachable skills as "not
6247
- reachable from here" because a cursor can move; a hidden skill is about *who is
6248
- asking*, and naming it would tell one role about another role's capabilities.
6446
+ reachable from here" because a cursor can move; a hidden skill is about _who is
6447
+ asking_, and naming it would tell one role about another role's capabilities.
6249
6448
  - **The enum stays the full catalog.** `toolArgValidation` runs before the gate,
6250
6449
  so narrowing it would turn a policy refusal into a generic schema error and the
6251
6450
  model would never read the policy's own message — the reasoning 8.5.0 recorded
@@ -6295,7 +6494,7 @@ await standingAgent({
6295
6494
  agentFactory: () => Agent.create({ provider, model }).system('…').build(),
6296
6495
  sessions: sqliteSessions({ file: './sessions.db' }),
6297
6496
  host: nodeHost({ port: 8080 }),
6298
- maxActiveSessions: 200, // default 100
6497
+ maxActiveSessions: 200, // default 100
6299
6498
  });
6300
6499
  ```
6301
6500
 
@@ -6393,7 +6592,6 @@ nodeHost({ sessionCookie: 'af_session' }); // …or no client code at all
6393
6592
  - New example: `examples/deploy/multi-user.ts` — two people served at once,
6394
6593
  proving the overlap in wall clock and that neither saw the other's memory.
6395
6594
 
6396
-
6397
6595
  ## [9.9.0] - 2026-08-12
6398
6596
 
6399
6597
  **A bug report IS the evidence.**
@@ -6411,7 +6609,7 @@ with the prose attached.
6411
6609
  ```ts
6412
6610
  import { describeBugReport, exportBugReport } from 'agentfootprint/observe';
6413
6611
 
6414
- const offer = describeBugReport(recording); // measure — nothing has left yet
6612
+ const offer = describeBugReport(recording); // measure — nothing has left yet
6415
6613
  // …show offer.units to the human; they tick some…
6416
6614
  const report = exportBugReport(recording, {
6417
6615
  include: ['conv-1', 'file-conversation', 'file-environment'],
@@ -6420,7 +6618,7 @@ const report = exportBugReport(recording, {
6420
6618
  expected: 'the updated price',
6421
6619
  actual: 'the price from before the update',
6422
6620
  });
6423
- fs.writeFileSync(report.filename, report.zip); // a real .zip
6621
+ fs.writeFileSync(report.filename, report.zip); // a real .zip
6424
6622
  ```
6425
6623
 
6426
6624
  - **The manifest is SELECTABLE UNITS, not a blob.** Each conversation is a unit
@@ -6438,7 +6636,7 @@ fs.writeFileSync(report.filename, report.zip); // a real .zip
6438
6636
  - **What was left out is STATED.** `manifest.excluded` counts the conversations,
6439
6637
  files, events and turns that were withheld, names their unit ids, and the
6440
6638
  issue body repeats it. A maintainer reading turn 4 must be able to tell that
6441
- turns 1–3 were *withheld*, not *lost*.
6639
+ turns 1–3 were _withheld_, not _lost_.
6442
6640
  - **Redacted keys, BY NAME.** The recording arrives already redacted (footprintjs
6443
6641
  scrubs at commit time), so nothing here scrubs anything — it would be too late
6444
6642
  to matter and a second policy could only disagree with the first. Instead the
@@ -6474,9 +6672,9 @@ plain `fetch` with no SDK:
6474
6672
 
6475
6673
  ```ts
6476
6674
  const reporter = githubBugReporter({
6477
- issueRepo: 'footprintjs/agentfootprint', // public — the conversation
6478
- evidenceRepo: 'acme/af-bug-evidence', // private — the run
6479
- }); // token: GITHUB_TOKEN, or `token`
6675
+ issueRepo: 'footprintjs/agentfootprint', // public — the conversation
6676
+ evidenceRepo: 'acme/af-bug-evidence', // private — the run
6677
+ }); // token: GITHUB_TOKEN, or `token`
6480
6678
  const { issueUrl, zipUrl } = await reporter.file(report);
6481
6679
  ```
6482
6680
 
@@ -6517,13 +6715,13 @@ const { issueUrl, zipUrl } = await reporter.file(report);
6517
6715
  calls — browser-safe and server-safe, no client secret, no dependency:
6518
6716
 
6519
6717
  ```ts
6520
- const signIn = await githubDeviceSignIn({ clientId }); // returns at once
6718
+ const signIn = await githubDeviceSignIn({ clientId }); // returns at once
6521
6719
  show(`Open ${signIn.verificationUri} and enter ${signIn.userCode}`);
6522
- const { token, login } = await signIn.completed; // resolves on approve
6720
+ const { token, login } = await signIn.completed; // resolves on approve
6523
6721
  ```
6524
6722
 
6525
- A server PAT files every report as the *application*; this files it as the
6526
- *reporter*, which is what a field tester filing upstream needs. It honours
6723
+ A server PAT files every report as the _application_; this files it as the
6724
+ _reporter_, which is what a field tester filing upstream needs. It honours
6527
6725
  `slow_down`, respects the code's expiry, takes an `AbortSignal`, and fetches
6528
6726
  `/user` for attribution (a `/user` that refuses is not fatal — the token still
6529
6727
  works, the login is simply absent). The token it returns is handed to
@@ -6593,7 +6791,9 @@ agent.enable.observability({
6593
6791
  maxBytes: 64 * 1024 * 1024,
6594
6792
  }),
6595
6793
  });
6596
- process.on('SIGTERM', async () => { await agent.shutdown(); }); // flushes
6794
+ process.on('SIGTERM', async () => {
6795
+ await agent.shutdown();
6796
+ }); // flushes
6597
6797
  ```
6598
6798
 
6599
6799
  Four things it is deliberate about:
@@ -6637,8 +6837,8 @@ import { vaultCredentials } from 'agentfootprint/security';
6637
6837
 
6638
6838
  const credentials = vaultCredentials({
6639
6839
  address: 'https://vault.internal:8200',
6640
- paths: { github: 'ci/github' }, // …or resolve(service), or neither
6641
- }); // token: `token`, else VAULT_TOKEN
6840
+ paths: { github: 'ci/github' }, // …or resolve(service), or neither
6841
+ }); // token: `token`, else VAULT_TOKEN
6642
6842
  ```
6643
6843
 
6644
6844
  The tool code does not change from the `staticTokens` version — same port, same
@@ -6672,7 +6872,7 @@ service)` as the seam for a shop whose field names are its own.
6672
6872
  the service, the mount path and the HTTP status — and nothing from the
6673
6873
  response body, nothing from the token, not even the field names the secret
6674
6874
  carries. This is the 8.6.0 law applied one adapter down: a thrown message
6675
- reaches the model as a tool result *and* rides
6875
+ reaches the model as a tool result _and_ rides
6676
6876
  `agentfootprint.credential.failed` to every observer. It is pinned by a
6677
6877
  grep-shaped test that walks every failure path — unknown service, 401, 403,
6678
6878
  404, 503, a non-JSON reply, a KV v1 response, an unrecognised field set, and a
@@ -6685,21 +6885,21 @@ service)` as the seam for a shop whose field names are its own.
6685
6885
  - **`infrastructure/on-premises.mdx`** — the provider page beside AWS. The
6686
6886
  local-first ladder (mock → local model → your gateway → a paid API) as the
6687
6887
  opening frame, then a service-by-service map: LLM (`ollama`, `openai({
6688
- baseURL })` for vLLM / llama.cpp / a corporate gateway, or the two-method
6888
+ baseURL })` for vLLM / llama.cpp / a corporate gateway, or the two-method
6689
6889
  port), stores (`sqliteVectorStore`, `pgVectorStore`, `staticVectorStore`,
6690
6890
  `RedisStore`), embedders (`localEmbedder`, `staticEmbedder`), hosting
6691
6891
  (`httpHost` / `nodeHost` + `sqliteSessions` / `memorySessions`), code
6692
- execution (`localCodeRunner` — *isolation, not a sandbox*), telemetry
6892
+ execution (`localCodeRunner` — _isolation, not a sandbox_), telemetry
6693
6893
  (`otelObservability` to any OTLP collector, `fileObservability` when there is
6694
6894
  none, `auditExport` for evidence), credentials (`staticTokens`,
6695
6895
  `vaultCredentials`, and the port for everything else) and tools (`mcpClient`
6696
6896
  over stdio or Streamable HTTP). It ends with **what is NOT here** — no
6697
6897
  Kubernetes-native anything, no second secret-manager adapter, no metrics
6698
6898
  exporter, no retention policy, no air-gapped model distribution.
6699
- - **The status vocabulary gained one honest rung.** *Contract-shaped and tested;
6700
- awaiting field use* is what `fileObservability` and `vaultCredentials` carry:
6899
+ - **The status vocabulary gained one honest rung.** _Contract-shaped and tested;
6900
+ awaiting field use_ is what `fileObservability` and `vaultCredentials` carry:
6701
6901
  their ports and refusals are pinned by tests, and neither has met a real
6702
- production disk or vault. *Verified in a production field deployment* now
6902
+ production disk or vault. _Verified in a production field deployment_ now
6703
6903
  appears in exactly one place, describing a deployment **shape** — a standing
6704
6904
  agent over `httpHost` + `sqliteSessions` against an OpenAI-compatible gateway,
6705
6905
  the shape several past releases exist because of — and never an adapter. The
@@ -6730,14 +6930,14 @@ handed that said "this is over".
6730
6930
 
6731
6931
  Three optional fields, sourced from what the engine already stamps:
6732
6932
 
6733
- | field | source | absent when |
6734
- |---|---|---|
6735
- | `ctx.runId` | the run in flight | there is no run — a call served over `mcpServe` is one call, not a turn |
6736
- | `ctx.sessionId` | `run({ sessionId })` ← `HostRequest.sessionId` | the run is not session-bound |
6737
- | `ctx.identity` | the identity the CALLER passed | the caller passed none |
6933
+ | field | source | absent when |
6934
+ | --------------- | ---------------------------------------------- | ----------------------------------------------------------------------- |
6935
+ | `ctx.runId` | the run in flight | there is no run — a call served over `mcpServe` is one call, not a turn |
6936
+ | `ctx.sessionId` | `run({ sessionId })` ← `HostRequest.sessionId` | the run is not session-bound |
6937
+ | `ctx.identity` | the identity the CALLER passed | the caller passed none |
6738
6938
 
6739
6939
  Every one is **absent rather than invented**, which is the 9.4.0 rule applied one
6740
- layer down. `ctx.identity` is deliberately *not* the run's internal
6940
+ layer down. `ctx.identity` is deliberately _not_ the run's internal
6741
6941
  `runIdentity`: that is always populated, defaulting to
6742
6942
  `{ conversationId: '<runId>' }`, and handing a tool a synthesized conversation as
6743
6943
  "the identity" would let it isolate a live sandbox on a fiction.
@@ -6767,12 +6967,12 @@ tool that wants a run-scoped session needs to know it is at a door with no runs
6767
6967
  BEFORE it opens one. Asking for a scope a door cannot honour throws, naming the
6768
6968
  door.
6769
6969
 
6770
- | scope | fires |
6771
- |---|---|
6772
- | `'call'` | when `tool.execute` settles — resolve **or** throw. Every door, including `mcpServe`. |
6773
- | `'run'` | at a run terminal that is **not a pause**. |
6774
- | `'session'` | `agent.closeToolSessions({ sessionId })`. |
6775
- | `'shutdown'` | `agent.shutdown()`. |
6970
+ | scope | fires |
6971
+ | ------------ | ------------------------------------------------------------------------------------- |
6972
+ | `'call'` | when `tool.execute` settles — resolve **or** throw. Every door, including `mcpServe`. |
6973
+ | `'run'` | at a run terminal that is **not a pause**. |
6974
+ | `'session'` | `agent.closeToolSessions({ sessionId })`. |
6975
+ | `'shutdown'` | `agent.shutdown()`. |
6776
6976
 
6777
6977
  Seven laws, each pinned: at most once ever · idempotent by `(tool, scope, key)`
6778
6978
  with the FIRST registration winning (it holds the live handle) · reverse
@@ -6783,7 +6983,7 @@ tolerates "already gone" · nothing live is ever persisted into a checkpoint.
6783
6983
  **A pause is not a terminal.** `'run'` teardown deliberately does not hang off
6784
6984
  `finally`, which also runs on both pause shapes. A `checkIn` on a code
6785
6985
  interpreter stops the run so a person can approve the code; tearing the sandbox
6786
- down there destroys the exact state the resume needs, and it fails *quietly* — as
6986
+ down there destroys the exact state the resume needs, and it fails _quietly_ — as
6787
6987
  a resumed run that "just re-ran everything". An error IS a terminal.
6788
6988
 
6789
6989
  ### Added — `agent.closeToolSessions({ sessionId })`
@@ -6872,7 +7072,7 @@ the rows, and what comes back is the finding.
6872
7072
  `client.send(new Command(...))`, pinned in `test/adapters/aws/awsCommandPin.ts`
6873
7073
  and **verified against a real install of the SDK before shipping** — including
6874
7074
  two shapes a design could only have guessed at: `Invoke` answers with an EVENT
6875
- STREAM, and seven of its nine union members are modelled *exceptions* (folded
7075
+ STREAM, and seven of its nine union members are modelled _exceptions_ (folded
6876
7076
  in as empty output, an `AccessDenied` would have reported a clean run that
6877
7077
  "printed nothing"), and `Stop` takes the session id, not a URI.
6878
7078
  - **`codeRunnerTool({ runner, scope })`** (main barrel) — holds one session per
@@ -6937,8 +7137,8 @@ and now shares the paging with the general one.
6937
7137
  **Behaviour change, named loudly: multi-turn memory starts actually
6938
7138
  retaining.** A six-turn conversation stores twelve message entries where it
6939
7139
  stored two, and the window injects up to `size` of them instead of the last
6940
- exchange — so prompts get longer and stores get bigger *because the agent is
6941
- now remembering what it was asked to remember*. Turn it down deliberately
7140
+ exchange — so prompts get longer and stores get bigger _because the agent is
7141
+ now remembering what it was asked to remember_. Turn it down deliberately
6942
7142
  (`size`, `DECAY`, `.compaction()`) rather than by accident.
6943
7143
 
6944
7144
  Cost: one paged `list()` per store per run, and only when a memory actually
@@ -7187,7 +7387,7 @@ compaction that runs.
7187
7387
  **AWS adapters tell the truth.**
7188
7388
 
7189
7389
  A production field report tested 9.3.0 against a real account and found two
7190
- adapters *dispatching calls that were never made against AWS* — one sending a
7390
+ adapters _dispatching calls that were never made against AWS_ — one sending a
7191
7391
  command that does not exist, one calling a method that a command-based client
7192
7392
  does not have. Both compiled. Both had green tests. Every one of those tests
7193
7393
  injected a double past the SDK, which is exactly why the bug class survives: the
@@ -7284,9 +7484,9 @@ are untouched.
7284
7484
 
7285
7485
  ### Changed — a fail-closed refusal now READS final
7286
7486
 
7287
- When a `PermissionChecker` throws, the call is denied. What the model was *told*
7487
+ When a `PermissionChecker` throws, the call is denied. What the model was _told_
7288
7488
  was the checker's own thrown message — and those are written for operators:
7289
- *"not available right now"*, `ECONNREFUSED`, *"timed out"*. **Measured in
7489
+ _"not available right now"_, `ECONNREFUSED`, _"timed out"_. **Measured in
7290
7490
  production: a real model read that as weather and retried the same tool to
7291
7491
  `maxIterations`, then returned the empty string.** Against the local policy's
7292
7492
  long-standing bracketed form the same model adapted cleanly on the first
@@ -7327,8 +7527,8 @@ calls did so in a silence that read like health.
7327
7527
  ### Added — `EventMeta.sessionId`: which CONVERSATION an event belongs to
7328
7528
 
7329
7529
  `meta.runId` is per `run()` / `resume()`; a session spans many. A shipped
7330
- telemetry stream could answer *"what happened in this run?"* and not *"what
7331
- happened in this conversation?"* — the question a session-oriented host is built
7530
+ telemetry stream could answer _"what happened in this run?"_ and not _"what
7531
+ happened in this conversation?"_ — the question a session-oriented host is built
7332
7532
  around, and one the events alone cannot be joined back into afterwards.
7333
7533
 
7334
7534
  `standingAgent` now threads the caller's own session id onto every event the run
@@ -7348,13 +7548,13 @@ three preconditions it had no evidence for, all three of which were wrong at
7348
7548
  once:
7349
7549
 
7350
7550
  - **A euclidean index was accepted.** The construction refusal read
7351
- `options.distanceMetric`, a *claim by the caller* about an index the store did
7551
+ `options.distanceMetric`, a _claim by the caller_ about an index the store did
7352
7552
  not create, so the default (undeclared) sailed through. Measured live: a
7353
7553
  vector queried against itself returned **0.9991630113800056**, where a true
7354
7554
  cosine self-similarity is exactly `1.0` — the very "number that READS like a
7355
7555
  cosine and is not one" this adapter's own header warns about.
7356
7556
  - **A missing `nonFilterableMetadataKeys: ['af']`** surfaced as a raw AWS
7357
- `ValidationException` — *"Filterable metadata must have at most 2048 bytes"* —
7557
+ `ValidationException` — _"Filterable metadata must have at most 2048 bytes"_ —
7358
7558
  **mid-import**, with documents already written and success already reported
7359
7559
  for them.
7360
7560
  - **The index dimension was never compared with the embedder.**
@@ -7378,11 +7578,11 @@ index whose metric, layout and dimension it would be guessing at.
7378
7578
 
7379
7579
  **Three promises the code had already made, kept.**
7380
7580
 
7381
- `MemoryStore`'s own docstring has named its backends since 2.x — *"Every storage
7581
+ `MemoryStore`'s own docstring has named its backends since 2.x — _"Every storage
7382
7582
  backend (InMemory, Redis, DynamoDB, **Postgres**, Bedrock AgentCore) implements
7383
- this interface"* — and named the queries too, in the two places an implementer
7384
- would look: *"**Postgres**: multi-row INSERT … ON CONFLICT DO UPDATE"* for
7385
- `putMany`, and *"**pgvector**: `ORDER BY embedding <=> query LIMIT k`"* for
7583
+ this interface"_ — and named the queries too, in the two places an implementer
7584
+ would look: _"**Postgres**: multi-row INSERT … ON CONFLICT DO UPDATE"_ for
7585
+ `putMany`, and _"**pgvector**: `ORDER BY embedding <=> query LIMIT k`"_ for
7386
7586
  `search`. Every one of those sentences was true about the design and false about
7387
7587
  the shipped package. Two of the three items below are the same shape: a
7388
7588
  documented promise with nothing behind it.
@@ -7546,8 +7746,8 @@ are now pinned; the rest are below.
7546
7746
  ### The conversation has a door with its own name
7547
7747
 
7548
7748
  ```ts
7549
- await agent.run({ message: 'Book me a table for two.' }); // one turn
7550
- await agent.followUp('Make it three.'); // the next one
7749
+ await agent.run({ message: 'Book me a table for two.' }); // one turn
7750
+ await agent.followUp('Make it three.'); // the next one
7551
7751
 
7552
7752
  // …or hand the conversation around — plain JSON, any store, any machine:
7553
7753
  const conversation = agent.checkpoint();
@@ -7605,7 +7805,7 @@ already broken in effect, so the throw is a fix, not a new restriction.
7605
7805
  is what makes `checkpoint()`, `getLastSnapshot()` and `followUp()` possible.
7606
7806
  Two overlapping runs both finished, both returning plausible answers, and the
7607
7807
  state afterwards belonged to whichever finished last — so `checkpoint()` could
7608
- hand back the *other* run's conversation, with nothing in either recording
7808
+ hand back the _other_ run's conversation, with nothing in either recording
7609
7809
  saying so. That is corruption, not concurrency. `standingAgent` has serialized
7610
7810
  runs since it existed and calls it "a correctness requirement rather than a
7611
7811
  tuning choice"; the guarantee now lives in the primitive. Two turns at once:
@@ -7619,16 +7819,16 @@ a consent gate any later message could walk around. Answer it with
7619
7819
  `resume(checkpoint, decision)`, or say plainly that it is being dropped:
7620
7820
 
7621
7821
  ```ts
7622
- const dropped = agent.abandonPause(); // { toolName, toolCallId, question }
7822
+ const dropped = agent.abandonPause(); // { toolName, toolCallId, question }
7623
7823
  await agent.run({ message: 'never mind, different question' });
7624
7824
  ```
7625
7825
 
7626
- `abandonPause()` *returns* what it dropped, so the abandonment can be logged
7826
+ `abandonPause()` _returns_ what it dropped, so the abandonment can be logged
7627
7827
  rather than performed blind.
7628
7828
 
7629
7829
  **A pause belongs to a session, not to the instance.** `standingAgent` shares
7630
7830
  one `Agent` across every session, so the instance guard alone would have let
7631
- session A's unanswered question refuse session B's *first* message — a
7831
+ session A's unanswered question refuse session B's _first_ message — a
7632
7832
  different conversation, a different person, an answer they were never asked
7633
7833
  for. The composer now releases the instance at the moment ownership moves: once
7634
7834
  the pause is in the store, the store owns it, and a later request carrying a
@@ -7649,7 +7849,7 @@ No shipped example, test or doc called it twice.
7649
7849
  ### `agent.canExplain()`
7650
7850
 
7651
7851
  `.selfExplain()` was already honest to the **model** with no record bound — the
7652
- trace tools answer *"No completed run is available yet"* and the skill body says
7852
+ trace tools answer _"No completed run is available yet"_ and the skill body says
7653
7853
  to say so plainly. It had no answer for the **program**. `canExplain()` returns
7654
7854
  `false` for two honest reasons — not built with `.selfExplain()`, or built with
7655
7855
  it and no turn completed — so a caller can route a why-question before spending
@@ -7677,7 +7877,7 @@ of this class quietly. Three kinds of pin:
7677
7877
  bare `throw`.
7678
7878
  2. **Adapted** — pinned by what reaches the model / the store / the caller,
7679
7879
  observed on the wire, never by mocking internals.
7680
- 3. **Stated** — pinned **twice**: the behavior, *and* the sentence in the source
7880
+ 3. **Stated** — pinned **twice**: the behavior, _and_ the sentence in the source
7681
7881
  that states it. A stated behavior whose statement was deleted is back to
7682
7882
  being a silent success, and only the second assertion catches that.
7683
7883
 
@@ -7696,7 +7896,7 @@ decision somebody makes in a diff.
7696
7896
  prints the messages the provider actually received for each. The model's own
7697
7897
  "this is your first message" reply is the evidence.
7698
7898
  - `examples/features/49-self-explain-live.ts` — turn 2 now goes through
7699
- `followUp()`. It was a second `run()`, and the scripted mock read the *first*
7899
+ `followUp()`. It was a second `run()`, and the scripted mock read the _first_
7700
7900
  user message, so the demo's own scripting was masking the restart. Live, the
7701
7901
  why-question used to arrive with no subject.
7702
7902
  - `docs-next/content/docs/build/conversations.mdx` — the conversation, the two
@@ -7708,13 +7908,13 @@ decision somebody makes in a diff.
7708
7908
  Additive except for the three refusals, and all three fire on code that was
7709
7909
  already wrong:
7710
7910
 
7711
- | if you… | you now get | do this |
7712
- | --- | --- | --- |
7713
- | call `run()` twice expecting continuity | the same behavior as before (a new conversation) | `followUp(message)` or `run({ message, continueFrom })` |
7714
- | overlap two `run()` calls on one agent | `RunInFlightError` | await the first, or build a second agent |
7715
- | send a message while a pause is open | `PendingQuestionError` | `resume(checkpoint, decision)`, or `abandonPause()` first |
7716
- | call `.system()` twice | build-time throw | join the strings, or `.steering()` / `.configure()` |
7717
- | continue a stored conversation on a differently-**named** agent | `ConversationMismatchError` | continue it on the agent whose id recorded it |
7911
+ | if you… | you now get | do this |
7912
+ | --------------------------------------------------------------- | ------------------------------------------------ | --------------------------------------------------------- |
7913
+ | call `run()` twice expecting continuity | the same behavior as before (a new conversation) | `followUp(message)` or `run({ message, continueFrom })` |
7914
+ | overlap two `run()` calls on one agent | `RunInFlightError` | await the first, or build a second agent |
7915
+ | send a message while a pause is open | `PendingQuestionError` | `resume(checkpoint, decision)`, or `abandonPause()` first |
7916
+ | call `.system()` twice | build-time throw | join the strings, or `.steering()` / `.configure()` |
7917
+ | continue a stored conversation on a differently-**named** agent | `ConversationMismatchError` | continue it on the agent whose id recorded it |
7718
7918
 
7719
7919
  `checkpoint()` payloads written by 9.1.0 and earlier continue to work; they
7720
7920
  simply carry no `identity` or `agent`, which is the honest answer for a
@@ -7745,13 +7945,13 @@ embedder might have — which then cuts every larger embedder short.
7745
7945
  characters, that this embedder represents faithfully. Every shipped embedder
7746
7946
  fills it in:
7747
7947
 
7748
- | embedder | `maxInputChars` | where the number comes from |
7749
- |---|---|---|
7750
- | `localEmbedder()` | `2000` | measured — the default model's 512-wordpiece-token cliff |
7751
- | `openaiEmbedder()` | `32000` | the documented 8,191-token window, at 4 characters a token |
7752
- | `bedrockEmbedder()` | `32000` | Titan's documented 8,192-token window, same conversion |
7753
- | `staticEmbedder()` | `1000000` | no transformer, so no context window — nothing is ever clipped |
7754
- | `mockEmbedder()` | `1000000` | reads every character in a loop |
7948
+ | embedder | `maxInputChars` | where the number comes from |
7949
+ | ------------------- | --------------- | -------------------------------------------------------------- |
7950
+ | `localEmbedder()` | `2000` | measured — the default model's 512-wordpiece-token cliff |
7951
+ | `openaiEmbedder()` | `32000` | the documented 8,191-token window, at 4 characters a token |
7952
+ | `bedrockEmbedder()` | `32000` | Titan's documented 8,192-token window, same conversion |
7953
+ | `staticEmbedder()` | `1000000` | no transformer, so no context window — nothing is ever clipped |
7954
+ | `mockEmbedder()` | `1000000` | reads every character in a loop |
7755
7955
 
7756
7956
  `indexCorpus`, `indexFolder` and `indexDocuments` read the embedder's declared
7757
7957
  ceiling **in preference to** their own 2,000-character default. An explicit
@@ -7769,7 +7969,7 @@ is accepted because the cliff belongs to the **model**, not to the factory.
7769
7969
  ### Truncation became visible
7770
7970
 
7771
7971
  A run that clipped anything now says so — **once**, on `console.warn`, naming
7772
- the count, the ceiling in effect *and where that ceiling came from*, and the
7972
+ the count, the ceiling in effect _and where that ceiling came from_, and the
7773
7973
  two fixes (re-split smaller, or raise `maxChunkChars`). `IndexReport` gains
7774
7974
  **`truncatedCount`** beside the existing `truncated` list: the list is what you
7775
7975
  debug with, the count is what you assert on and what a dashboard row can hold.
@@ -7817,27 +8017,27 @@ deprecation warnings on 8.20.0, it compiles and runs unchanged on 9.0.0.**
7817
8017
 
7818
8018
  ### Removed — the sixteen door aliases
7819
8019
 
7820
- Each removed path re-exported the *same symbols* the door carries, never copies,
8020
+ Each removed path re-exported the _same symbols_ the door carries, never copies,
7821
8021
  so this is a find-and-replace on import lines. No name moved; no name was lost.
7822
8022
 
7823
- | you were importing from | import from |
7824
- |---|---|
7825
- | `agentfootprint/llm-providers` | `agentfootprint/providers` |
7826
- | `agentfootprint/embedders` | `agentfootprint/providers` |
7827
- | `agentfootprint/tool-providers` | `agentfootprint/providers` |
7828
- | `agentfootprint/thinking` | `agentfootprint/providers` |
7829
- | `agentfootprint/memory-providers` | `agentfootprint/memory` |
7830
- | `agentfootprint/observability-providers` | `agentfootprint/observe` |
7831
- | `agentfootprint/strategies` | `agentfootprint/observe` |
7832
- | `agentfootprint/stream` | `agentfootprint/observe` |
7833
- | `agentfootprint/status` | `agentfootprint/observe` |
7834
- | `agentfootprint/locales` | `agentfootprint/observe` |
7835
- | `agentfootprint/debug` | `agentfootprint/observe` |
7836
- | `agentfootprint/debug/finders` | `agentfootprint/observe` |
7837
- | `agentfootprint/observability/contextError/finders` | `agentfootprint/observe` |
7838
- | `agentfootprint/hosting-providers` | `agentfootprint/hosting` |
7839
- | `agentfootprint/injection-engine` | `agentfootprint/context` |
7840
- | `agentfootprint/identity` | `agentfootprint/security` |
8023
+ | you were importing from | import from |
8024
+ | --------------------------------------------------- | -------------------------- |
8025
+ | `agentfootprint/llm-providers` | `agentfootprint/providers` |
8026
+ | `agentfootprint/embedders` | `agentfootprint/providers` |
8027
+ | `agentfootprint/tool-providers` | `agentfootprint/providers` |
8028
+ | `agentfootprint/thinking` | `agentfootprint/providers` |
8029
+ | `agentfootprint/memory-providers` | `agentfootprint/memory` |
8030
+ | `agentfootprint/observability-providers` | `agentfootprint/observe` |
8031
+ | `agentfootprint/strategies` | `agentfootprint/observe` |
8032
+ | `agentfootprint/stream` | `agentfootprint/observe` |
8033
+ | `agentfootprint/status` | `agentfootprint/observe` |
8034
+ | `agentfootprint/locales` | `agentfootprint/observe` |
8035
+ | `agentfootprint/debug` | `agentfootprint/observe` |
8036
+ | `agentfootprint/debug/finders` | `agentfootprint/observe` |
8037
+ | `agentfootprint/observability/contextError/finders` | `agentfootprint/observe` |
8038
+ | `agentfootprint/hosting-providers` | `agentfootprint/hosting` |
8039
+ | `agentfootprint/injection-engine` | `agentfootprint/context` |
8040
+ | `agentfootprint/identity` | `agentfootprint/security` |
7841
8041
 
7842
8042
  What ships now is exactly: the root barrel, the ten doors (`/providers`,
7843
8043
  `/memory`, `/rag`, `/cache`, `/observe`, `/events`, `/context`, `/resilience`,
@@ -7853,23 +8053,23 @@ implementation barrel is served by its door as the same object.
7853
8053
 
7854
8054
  ### Removed — options, strings, methods, fields
7855
8055
 
7856
- | removed | replacement | since |
7857
- |---|---|---|
7858
- | `AgentBuilder.recorder(rec)` | `AgentBuilder.watch(rec)` — same list, same order, same attachment, and variadic | deprecated 8.0.0 |
7859
- | `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 |
7860
- | `skillsFromDir(dir, { viaToolName })` | drop it — same reason | deprecated 8.7.0 |
7861
- | `WindowRefusalReason` member `'summary-not-smaller'` | `'replacement-not-smaller'` | renamed 8.14.0 |
7862
- | type `FoldRefusal` | `WindowRefusal` | renamed 7.17 |
7863
- | type `FoldRefusalReason` | `WindowRefusalReason` | renamed 7.17 |
7864
- | `WindowStrategy` exported from `agentfootprint/memory` | `MemoryWindowStrategy` | renamed 7.27.1 |
7865
- | `CompactionRecord.foldedStageIds` | `WindowRecord.removedStageIds` | family name published 7.17 |
7866
- | `CompactionRecord.foldedMessageCount` | `WindowRecord.removedMessageCount` | family name published 7.17 |
7867
- | `ContextBudgetPressurePayload.capTokens` | `cap`, read with `unit` | renamed 8.14.0 |
7868
- | `ContextBudgetPressurePayload.projectedTokens` | `projected`, read with `unit` | renamed 8.14.0 |
7869
- | `BudgetPressureRecord.capTokens` | `cap`, read with `unit` | renamed 8.14.0 |
7870
- | `BudgetPressureRecord.projectedTokens` | `projected`, read with `unit` | renamed 8.14.0 |
7871
-
7872
- Three of those are worth a sentence each, because the *reason* is the migration:
8056
+ | removed | replacement | since |
8057
+ | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- | -------------------------- |
8058
+ | `AgentBuilder.recorder(rec)` | `AgentBuilder.watch(rec)` — same list, same order, same attachment, and variadic | deprecated 8.0.0 |
8059
+ | `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 |
8060
+ | `skillsFromDir(dir, { viaToolName })` | drop it — same reason | deprecated 8.7.0 |
8061
+ | `WindowRefusalReason` member `'summary-not-smaller'` | `'replacement-not-smaller'` | renamed 8.14.0 |
8062
+ | type `FoldRefusal` | `WindowRefusal` | renamed 7.17 |
8063
+ | type `FoldRefusalReason` | `WindowRefusalReason` | renamed 7.17 |
8064
+ | `WindowStrategy` exported from `agentfootprint/memory` | `MemoryWindowStrategy` | renamed 7.27.1 |
8065
+ | `CompactionRecord.foldedStageIds` | `WindowRecord.removedStageIds` | family name published 7.17 |
8066
+ | `CompactionRecord.foldedMessageCount` | `WindowRecord.removedMessageCount` | family name published 7.17 |
8067
+ | `ContextBudgetPressurePayload.capTokens` | `cap`, read with `unit` | renamed 8.14.0 |
8068
+ | `ContextBudgetPressurePayload.projectedTokens` | `projected`, read with `unit` | renamed 8.14.0 |
8069
+ | `BudgetPressureRecord.capTokens` | `cap`, read with `unit` | renamed 8.14.0 |
8070
+ | `BudgetPressureRecord.projectedTokens` | `projected`, read with `unit` | renamed 8.14.0 |
8071
+
8072
+ Three of those are worth a sentence each, because the _reason_ is the migration:
7873
8073
 
7874
8074
  - **`viaToolName` named a door that was never built.** The evaluator activates
7875
8075
  an `llm-activated` skill by matching `ctx.activatedInjectionIds`, only
@@ -7908,7 +8108,7 @@ replacement and says when the signpost comes down.
7908
8108
  Deleting the type member alone would have been a silent DOWNGRADE for the two
7909
8109
  `viaToolName` cases: an object literal gets an excess-property error, but an
7910
8110
  options bag arriving through a variable does not, and the value would then be
7911
- *ignored* where 8.7.0 refused it. So the field is read at run time exactly once
8111
+ _ignored_ where 8.7.0 refused it. So the field is read at run time exactly once
7912
8112
  more, to say it is gone.
7913
8113
 
7914
8114
  ### Two things deliberately kept
@@ -7923,7 +8123,7 @@ more, to say it is gone.
7923
8123
  `CircuitState`, which is declared in both breaker files as
7924
8124
  `'closed' | 'open' | 'half-open'` — two declarations, one type, pinned as such.
7925
8125
  - **`buildRunSteps(events)` survives, still `@deprecated`.** Its deprecation is
7926
- a *preference*, not a migration: live consumers should attach
8126
+ a _preference_, not a migration: live consumers should attach
7927
8127
  `runStepRecorder()` and read `getSteps()` (O(N), the house pattern) instead of
7928
8128
  re-walking an event log (O(N²) across repeated calls). But the shim is the only
7929
8129
  way to build steps from a saved event list — replay, post-hoc analysis, tests —
@@ -8000,7 +8200,7 @@ The fix is one coherent rule across the structural splitters:
8000
8200
  alone. The heading is now glued to the first body paragraph; it can never
8001
8201
  again be a chunk by itself.
8002
8202
  - **The family was inspected, and two members are exempt by design.**
8003
- `fixedWithOverlap` cuts uniformly sized chunks *by request* — imposing a
8203
+ `fixedWithOverlap` cuts uniformly sized chunks _by request_ — imposing a
8004
8204
  250-char floor on `fixedWithOverlap({ chars: 120 })` would repeal the
8005
8205
  caller's own choice, and its only runt (the file tail) has always folded
8006
8206
  backward. `wholeDocument` is one chunk per document by definition.
@@ -8054,7 +8254,7 @@ any immutable or serverless runtime has this problem):
8054
8254
  - **`exportCorpus(store, identity?)`** (`agentfootprint/rag`) — every entry
8055
8255
  of a corpus namespace as one plain-JSON `CorpusBundle`:
8056
8256
  `{ entries: [{ id, text, vector, metadata }], embedder: { id, dimensions },
8057
- namespace }`. Plain JSON on purpose — the runtime that needs this is
8257
+ namespace }`. Plain JSON on purpose — the runtime that needs this is
8058
8258
  exactly the runtime that cannot open a database file. It refuses an empty
8059
8259
  namespace (naming the identity-mismatch cause), entries with no vector or
8060
8260
  no passage (a bundle never ships an unservable or uncitable entry), and a
@@ -8216,7 +8416,7 @@ it, before a byte is embedded. `InMemoryStore` and `sqliteVectorStore` declare
8216
8416
  **Behaviour change:** building a corpus into `AgentCoreStore` or `RedisStore` is
8217
8417
  refused where it used to run. Neither could ever serve those vectors back —
8218
8418
  `RedisStore` has no `search()` at all, so the same mistake already failed one
8219
- layer later, when `defineRAG` refused the store *after* the whole index had been
8419
+ layer later, when `defineRAG` refused the store _after_ the whole index had been
8220
8420
  embedded and billed. The refusal moved to the call that starts the spending.
8221
8421
 
8222
8422
  **Absence is not a `false`.** A store that declares nothing behaves exactly as it
@@ -8236,7 +8436,7 @@ it wrong.
8236
8436
  ```ts
8237
8437
  import { bedrockEmbedder } from 'agentfootprint/providers';
8238
8438
 
8239
- const embedder = bedrockEmbedder({ region: 'us-east-1' }); // 1024-d
8439
+ const embedder = bedrockEmbedder({ region: 'us-east-1' }); // 1024-d
8240
8440
  const small = bedrockEmbedder({ region: 'us-east-1', dimensions: 512 });
8241
8441
  ```
8242
8442
 
@@ -8409,7 +8609,7 @@ the run stops re-asking and names the rule — in the `outputAttempts` row
8409
8609
  (`brokenBy`), in the event, in `outputContractUnmet()`, and in the warning.
8410
8610
  Re-asking cannot fix a rule: a deterministic one breaks the next answer
8411
8611
  identically, so the retries buy a repeat of the same ending. An answer that was
8412
- *already* bad still spends its retries — the stop applies only when the
8612
+ _already_ bad still spends its retries — the stop applies only when the
8413
8613
  middleware is the cause.
8414
8614
 
8415
8615
  ### `.outputFallback()` says which door reaches its tiers
@@ -8496,8 +8696,8 @@ flowchartAsTool({
8496
8696
  name: 'weather_advice',
8497
8697
  description: 'Decide whether to bike tomorrow.',
8498
8698
  flowchart: adviceChart,
8499
- keepRecord: true, // ← off by default
8500
- keepRecordLimit: 20, // ← bounded LRU window (this is the default)
8699
+ keepRecord: true, // ← off by default
8700
+ keepRecordLimit: 20, // ← bounded LRU window (this is the default)
8501
8701
  redact: { keys: ['apiKey'] },
8502
8702
  });
8503
8703
  ```
@@ -8588,8 +8788,8 @@ artifacts by hand: `innerRunStore`, `innerRunsOf`, `mergeInnerRuns`,
8588
8788
  ### The demo
8589
8789
 
8590
8790
  `examples/features/50-through-the-tool-boundary.ts` — a weather-advice agent
8591
- whose ONE tool is a 4-stage footprintjs chart. Turn 1: *"Should I bike to work
8592
- in Chicago tomorrow?"* Turn 2: *"Why did you say it'll rain?"* — answered
8791
+ whose ONE tool is a 4-stage footprintjs chart. Turn 1: _"Should I bike to work
8792
+ in Chicago tomorrow?"_ Turn 2: _"Why did you say it'll rain?"_ — answered
8593
8793
  through visible `find_in_trace` → `inspect_tool_call` → `inspect_tool_run`
8594
8794
  calls that cite the inner stage (`validate-forecast#1`), the exact field
8595
8795
  (`rainChancePct = 82`) and the rule that consumed it. The chart's own stage
@@ -8610,8 +8810,8 @@ evidence existed, and the model had no way to reach it.
8610
8810
  ### `find_in_trace(query)` — free text in, step ids out
8611
8811
 
8612
8812
  Every other trace tool needs a name you already have: a step id, a state key, a
8613
- variable. But a follow-up question arrives in the user's words — *"why did you say
8614
- order 7712 was out of warranty?"* — and the model's only options were to guess a
8813
+ variable. But a follow-up question arrives in the user's words — _"why did you say
8814
+ order 7712 was out of warranty?"_ — and the model's only options were to guess a
8615
8815
  state key or read the whole narrative.
8616
8816
 
8617
8817
  This searches stage names and descriptions, state keys, every committed value and
@@ -8748,7 +8948,7 @@ await callTraceTool(tools, 'find_in_trace', { query: 'order 7712' });
8748
8948
 
8749
8949
  Pure: no engine, no agent, no I/O. Exported from `agentfootprint/observe` (and
8750
8950
  `agentfootprint/debug`). Honest about the two things a serialized run cannot carry
8751
- back — `controlDeps` is a lookup *function* and does not serialize (slices say
8951
+ back — `controlDeps` is a lookup _function_ and does not serialize (slices say
8752
8952
  `⚠ control edges unavailable`, the marker that already existed), and the narrative
8753
8953
  survives only if a narrative recorder was attached, since `recordRun` deliberately
8754
8954
  attaches none. Two teaching refusals name `recordRun` as the producer: a bundle with
@@ -8801,11 +9001,11 @@ cursor half and left the rule half standing, and the rule half is the bug: an en
8801
9001
  routed to `T`, `S`'s rule still matched and `S` and `T` were both active. Measured on
8802
9002
  a two-skill support graph:
8803
9003
 
8804
- | iteration | active | tool menu | skill bodies |
8805
- |---|---|---|---|
8806
- | 1 | `triage` | `read_skill`, `lookup_order` | triage |
9004
+ | iteration | active | tool menu | skill bodies |
9005
+ | --------------- | ------------------ | -------------------------------------------- | --------------- |
9006
+ | 1 | `triage` | `read_skill`, `lookup_order` | triage |
8807
9007
  | 2 — the handoff | `triage`, `refund` | `read_skill`, `lookup_order`, `issue_refund` | triage + refund |
8808
- | 3 | `triage`, `refund` | same | triage + refund |
9008
+ | 3 | `triage`, `refund` | same | triage + refund |
8809
9009
 
8810
9010
  Note iteration 3. This was never a one-iteration blip: with the cursor parked on
8811
9011
  `refund`, `triage`'s rule kept matching, so it came back and stayed. The overlap was
@@ -8813,8 +9013,8 @@ the steady state.
8813
9013
 
8814
9014
  **A conditional entry is now active exactly while the cursor is on it** — the same
8815
9015
  compiled expression a route target and an exclusive entry already used. One law for a
8816
- flat graph: *a skill is active iff the cursor is on it, or it declared itself
8817
- unconditional.* `when` chooses where a turn STARTS.
9016
+ flat graph: _a skill is active iff the cursor is on it, or it declared itself
9017
+ unconditional._ `when` chooses where a turn STARTS.
8818
9018
 
8819
9019
  This finishes 8.3.0 rather than reverting it. Both failures 8.3.0 named — a declared
8820
9020
  step INTO an entry skill, and a `read_skill` pick onto one — are carried by the cursor
@@ -8852,7 +9052,7 @@ superseded pick.
8852
9052
 
8853
9053
  `multi-entry-fanout` fired whenever a graph declared two or more entries, including
8854
9054
  when every one of them carried a `when` — a deterministic rule-router, which is a
8855
- taught shape. Worse, the advice it gave was *"give the extras a `when`"*, to entries
9055
+ taught shape. Worse, the advice it gave was _"give the extras a `when`"_, to entries
8856
9056
  that already had one. It computed which entries were unconditional and then used that
8857
9057
  only to soften the middle of the sentence.
8858
9058
 
@@ -8871,7 +9071,7 @@ why the check over-fired. Rewritten.
8871
9071
  A suppression the run cannot name is a silent drop. When a conditional entry's rule
8872
9072
  matched and the cursor law kept it off the wire, the entry's id is now reported on the
8873
9073
  per-iteration evaluation event, beside the `cursorMove` that says where the graph went
8874
- instead. Together they answer *"why isn't my entry loading?"* without anyone
9074
+ instead. Together they answer _"why isn't my entry loading?"_ without anyone
8875
9075
  re-running a predicate to guess.
8876
9076
 
8877
9077
  Omitted when nothing was suppressed, and for every non-skill-graph run — so an
@@ -8917,8 +9117,8 @@ build, and one changes a string on the wire. All eight are below.
8917
9117
  correct branch:
8918
9118
 
8919
9119
  - **same provider family** — it billed your MAIN model for every fold. The
8920
- refusal three lines above it in the same file promised *"the library will not
8921
- quietly bill your main model for compaction"*, and then did.
9120
+ refusal three lines above it in the same file promised _"the library will not
9121
+ quietly bill your main model for compaction"_, and then did.
8922
9122
  - **different provider** — it sent your agent's model id to a vendor that has
8923
9123
  never heard of it, so the fold died mid-run, on a paid run, in a file whose
8924
9124
  own header promises "everything fails at `.build()`, never mid-run".
@@ -8946,7 +9146,7 @@ Readings are now stamped with the iteration whose call produced them and expire
8946
9146
  one boundary later. An expired reading is `undefined`, which every strategy
8947
9147
  already treats as "do not act" — so a window strategy **stands down** instead of
8948
9148
  deciding on a number nobody took, and says so once on the console rather than
8949
- going quiet. "Counted, never guessed" has to mean counted *recently*.
9149
+ going quiet. "Counted, never guessed" has to mean counted _recently_.
8950
9150
 
8951
9151
  An agent whose provider reports usage reliably is unaffected.
8952
9152
 
@@ -8991,10 +9191,10 @@ comparison of two string lengths will not.
8991
9191
  Two emitters share this event name, this `slot: 'messages'` value, and — until
8992
9192
  now — one indistinguishable payload:
8993
9193
 
8994
- | emitter | counts | `unit` |
8995
- |---|---|---|
8996
- | the three context slots (`contextBudget`, **on by default**) | `String.length` | `'chars'` |
8997
- | a window strategy (`.window()` / `.compaction()`) | provider-reported input tokens | `'tokens'` |
9194
+ | emitter | counts | `unit` |
9195
+ | ------------------------------------------------------------ | ------------------------------ | ---------- |
9196
+ | the three context slots (`contextBudget`, **on by default**) | `String.length` | `'chars'` |
9197
+ | a window strategy (`.window()` / `.compaction()`) | provider-reported input tokens | `'tokens'` |
8998
9198
 
8999
9199
  So one subscriber routinely received both, and `cap 200, projected 258` could
9000
9200
  mean 258 characters or 258 tokens — a roughly 4× difference in the same field,
@@ -9018,8 +9218,8 @@ oversight — please do not "fix" them.
9018
9218
  ### `costBudget` can now stop the run
9019
9219
 
9020
9220
  `costBudget` was warn-only, while `commentaryTemplates.ts` narrated
9021
- *"{{appName}} hit a cost limit and stopped."* and `docs/monitor/deployment.mdx`
9022
- claimed the agent *"halts when the per-run USD budget is hit"*. It did neither.
9221
+ _"{{appName}} hit a cost limit and stopped."_ and `docs/monitor/deployment.mdx`
9222
+ claimed the agent _"halts when the per-run USD budget is hit"_. It did neither.
9023
9223
  `docs/monitor/observability.mdx`, on the same site, correctly said the library
9024
9224
  never auto-aborts.
9025
9225
 
@@ -9128,7 +9328,7 @@ version would make an older deployment refuse a session it can serve.
9128
9328
  **Governance never silently drops — and never silently invents.** Eight ways a
9129
9329
  rule you configured could decide nothing, and you could only find out by reading
9130
9330
  a quiet run. Seven are now refused at build time with a message that names the
9131
- fix; one was a rule that ran everywhere except the one path where a *person* had
9331
+ fix; one was a rule that ran everywhere except the one path where a _person_ had
9132
9332
  just typed the value.
9133
9333
 
9134
9334
  Two of these change what a run DOES. Both are called out below.
@@ -9200,7 +9400,7 @@ It now raises `DecisionRequiredError` (`code: 'ERR_DECISION_REQUIRED'`) at the
9200
9400
  API boundary. **Nothing executes and the checkpoint is unchanged**, so the same
9201
9401
  one can be answered properly and resumed again. The error names the gate
9202
9402
  (`gate: 'checkIn' | 'ask'`), the tool, the middleware that asked, and `received`
9203
- — the *shape* that arrived, never its contents, because a resume payload is
9403
+ — the _shape_ that arrived, never its contents, because a resume payload is
9204
9404
  caller data and an error message ends up in logs.
9205
9405
 
9206
9406
  Discriminated by the **pause**, never by the input, via the new
@@ -9282,9 +9482,9 @@ never stops your strategy. It now also carries two methods:
9282
9482
  ```ts
9283
9483
  const telemetry = agent.enable.observability({ strategy: cloudwatch });
9284
9484
 
9285
- await telemetry.flush(); // drain: driver queue first, then the buffer
9286
- telemetry(); // detach (unchanged)
9287
- telemetry.stop(); // release — timers, clients, buffers
9485
+ await telemetry.flush(); // drain: driver queue first, then the buffer
9486
+ telemetry(); // detach (unchanged)
9487
+ telemetry.stop(); // release — timers, clients, buffers
9288
9488
  ```
9289
9489
 
9290
9490
  `flush()` enforces the ORDER, which is the part no consumer could write from
@@ -9407,7 +9607,7 @@ of trying again. A drain that cannot finish must return, never retry forever.
9407
9607
 
9408
9608
  With `enable.observability({ detach })`, each export is scheduled onto a
9409
9609
  footprintjs detach driver. Scheduling happened inside a promise continuation,
9410
- so the detach handle reached footprintjs's registry a microtask *after* the
9610
+ so the detach handle reached footprintjs's registry a microtask _after_ the
9411
9611
  event was dispatched. `flushAllDetached()` drains until that registry is empty
9412
9612
  — and it was still empty when it looked.
9413
9613
 
@@ -9430,7 +9630,7 @@ the package either way.
9430
9630
  reach today (the declared type is still `Unsubscribe`); it exists so the
9431
9631
  no-subscription case is not the one path that breaks when that type widens.
9432
9632
  - Removed a dead loop condition in the CloudWatch drain (`lastFlushPromise !==
9433
- Promise.resolve()` compares against a freshly minted promise and is always
9633
+ Promise.resolve()` compares against a freshly minted promise and is always
9434
9634
  true).
9435
9635
 
9436
9636
  ## [8.11.0] - 2026-08-07
@@ -9467,7 +9667,7 @@ delivery — just an empty log group.
9467
9667
  The docstring for `logStreamName` had been promising `"Created on first put if
9468
9668
  it doesn't exist"` since the adapter shipped. It was never true. Worse, the
9469
9669
  convention the docs themselves recommended — `` `${HOSTNAME}/${Date.now()}` ``
9470
- — produces a name that *cannot* pre-exist, so following the documentation
9670
+ — produces a name that _cannot_ pre-exist, so following the documentation
9471
9671
  guaranteed the bug on every deploy. The only configuration that worked was the
9472
9672
  undocumented one.
9473
9673
 
@@ -9483,7 +9683,7 @@ instead of implying otherwise.
9483
9683
 
9484
9684
  The missing stream was one delivery failure. It turned out **every** delivery
9485
9685
  failure was silent: an IAM denial, a throttle, a rejected batch. Each adapter
9486
- installed its console fallback lazily *inside* its own `_onError` method — so
9686
+ installed its console fallback lazily _inside_ its own `_onError` method — so
9487
9687
  the delivery path, which read the hook rather than calling the method, found
9488
9688
  `undefined` and dropped the error on the floor. `cloudwatch`, `xray` and `otel`
9489
9689
  all had it.
@@ -9510,7 +9710,7 @@ fail. It is replaced by six that assert unconditionally.
9510
9710
 
9511
9711
  ### A knob the warning told you to turn, that did not exist
9512
9712
 
9513
- An over-budget context slot warned: *"Raise `budgetCap` on the slot config."*
9713
+ An over-budget context slot warned: _"Raise `budgetCap` on the slot config."_
9514
9714
  `budgetCap` was reachable from no public door. `buildMessagesSlot()` was called
9515
9715
  with no arguments at all four of its call sites, so its 10000-character cap was
9516
9716
  unreachable by construction. A warning you cannot act on is worse than no
@@ -9530,15 +9730,15 @@ budget is a signal, not a limiter. `LLMCallOptions` takes the same option
9530
9730
 
9531
9731
  A 429 is a **pre-execution rejection**: the rate limiter refused the request at
9532
9732
  the edge and the server never ran the tool, so a retry cannot double-execute
9533
- anything. That is exactly what is *not* true of a 500 or a timeout, where the
9733
+ anything. That is exactly what is _not_ true of a 500 or a timeout, where the
9534
9734
  call may have half-run and a retry could charge a card twice.
9535
9735
 
9536
9736
  That asymmetry is the entire license for this feature, so the policy is 429 and
9537
9737
  nothing else — pinned by a property test that walks twelve other statuses and a
9538
9738
  thrown transport error and asserts a single attempt for each. Managed gateways
9539
9739
  rate-limit per principal by design; without this, a designed and self-clearing
9540
- condition reached the model as a thrown tool error it reads as *"this tool is
9541
- broken"*, whereupon it apologises, picks another tool, or invents an answer.
9740
+ condition reached the model as a thrown tool error it reads as _"this tool is
9741
+ broken"_, whereupon it apologises, picks another tool, or invents an answer.
9542
9742
 
9543
9743
  It lives at the `fetch` seam because that is the only place `Retry-After` still
9544
9744
  exists — the MCP SDK reads the response, throws `StreamableHTTPError(status,
@@ -9565,18 +9765,22 @@ Per-attempt visibility is the `onRetry` callback — the contract `withRetry` an
9565
9765
  are consumer-called, the docs now say so, and a batching exporter loses its
9566
9766
  final batch and leaks its timer if you skip them:
9567
9767
  ```ts
9568
- process.on('SIGTERM', async () => { await telemetry.flush(); telemetry.stop(); stop(); });
9768
+ process.on('SIGTERM', async () => {
9769
+ await telemetry.flush();
9770
+ telemetry.stop();
9771
+ stop();
9772
+ });
9569
9773
  ```
9570
9774
  Wiring them into the framework lifecycle would change `run()` timing and
9571
9775
  misbehave for a strategy shared across two `enable` calls, so it is a design
9572
9776
  question on the ledger rather than a silent default.
9573
9777
  - **A Skill's tools are visible from iteration 1.** `DefineSkillOptions.tools`
9574
- said they were *"added to the tools slot once activated"*. They are added to
9778
+ said they were _"added to the tools slot once activated"_. They are added to
9575
9779
  the registry at build time; activation adds the Skill's **body**, not its
9576
9780
  tools. Gating is opt-in via `autoActivate: 'currentSkill'` (which
9577
9781
  `skillGraph().tree()` sets for you on every leaf) — and the docs said
9578
9782
  otherwise in twelve places, including a `process_refund` example claiming a
9579
- tool was *"locked away"*. That example now sets `autoActivate` and the prose
9783
+ tool was _"locked away"_. That example now sets `autoActivate` and the prose
9580
9784
  no longer implies a security boundary the default does not provide.
9581
9785
  - **`autoActivate` stopped calling itself a forward-compat marker** awaiting
9582
9786
  "v2.5 runtime wiring" — that wiring shipped in 2.5.0, six majors ago.
@@ -9586,7 +9790,7 @@ Per-attempt visibility is the `onRetry` callback — the contract `withRetry` an
9586
9790
  anything, and a lower tier is not a safer one: `'minimal'` still ships
9587
9791
  `agent.turn_start` (`userPrompt`), `agent.turn_end` (`finalContent`) and
9588
9792
  `agent.iteration_end` (the whole conversation `history[]`) — measured, it
9589
- carries user content in a *higher* share of its events than `'standard'` does.
9793
+ carries user content in a _higher_ share of its events than `'standard'` does.
9590
9794
  The docstring says this plainly now, points at `auditExport()` (bounded by
9591
9795
  default) and `otelObservability()` (omits `userPrompt`), and warns that
9592
9796
  `redactContent` does **not** apply to this channel — it operates on the
@@ -9680,12 +9884,12 @@ exists.
9680
9884
  `textLoader`, `markdownLoader` and `htmlLoader` need no dependency. `pdfLoader`
9681
9885
  needs one, and it was picked by measuring rather than by reputation:
9682
9886
 
9683
- | package | installed | packages | verdict |
9684
- |---|---|---|---|
9685
- | **`unpdf`** | **2.5 MB** | **1** | chosen — zero transitive deps, per-page text |
9686
- | `pdf-parse@2` | 86 MB | 3 | a native binary (`@napi-rs/canvas`), to read text |
9687
- | `pdf-parse@1` | 34 MB | 4 | unmaintained since 2018 |
9688
- | `pdfjs-dist@6` | 62 MB | 2 | 25× the size for the same engine |
9887
+ | package | installed | packages | verdict |
9888
+ | -------------- | ---------- | -------- | ------------------------------------------------- |
9889
+ | **`unpdf`** | **2.5 MB** | **1** | chosen — zero transitive deps, per-page text |
9890
+ | `pdf-parse@2` | 86 MB | 3 | a native binary (`@napi-rs/canvas`), to read text |
9891
+ | `pdf-parse@1` | 34 MB | 4 | unmaintained since 2018 |
9892
+ | `pdfjs-dist@6` | 62 MB | 2 | 25× the size for the same engine |
9689
9893
 
9690
9894
  It is an optional peer, lazily loaded, refusing with an install line when a PDF
9691
9895
  is actually met. Per-page text is why a PDF citation can name a page you can
@@ -9789,7 +9993,6 @@ the page its 150-character run-up borrowed from.
9789
9993
  - **`unpdf`** as a new optional peer. Only `pdfLoader` touches it, only when a
9790
9994
  PDF is actually read.
9791
9995
 
9792
-
9793
9996
  ## [8.9.0] - 2026-08-06
9794
9997
 
9795
9998
  **The durable index.** 8.8.0 made retrieval tell the truth about what it read.
@@ -9815,8 +10018,8 @@ await indexDocuments(store, embedder, docs, { embedderId: embedder.id });
9815
10018
  Embedding cost is not one number, and the split is the whole argument for a
9816
10019
  file. **Index time** embeds the corpus: once, scaling with how much you store.
9817
10020
  **Query time** embeds the user's question: per retrieval, scaling with traffic.
9818
- A 10,000-chunk corpus is 10,000 embeddings *once* and one per question
9819
- thereafter — with a `Map` it is 10,000 embeddings *per restart*.
10021
+ A 10,000-chunk corpus is 10,000 embeddings _once_ and one per question
10022
+ thereafter — with a `Map` it is 10,000 embeddings _per restart_.
9820
10023
 
9821
10024
  `agentfootprint.embedding.generated` has carried an `inputKind: 'document' |
9822
10025
  'query'` field since 2.x and nothing ever emitted it, so any dashboard built
@@ -9839,13 +10042,13 @@ top-K or it does not answer.
9839
10042
 
9840
10043
  Measured against this implementation on Node 22.16, Apple silicon:
9841
10044
 
9842
- | corpus | query | resident matrix | file | first search (hydration) |
9843
- |---|---|---|---|---|
9844
- | 10,000 × 384-d | 6 ms | 15 MB | 21 MB | 45 ms |
9845
- | 50,000 × 384-d | 31 ms | 77 MB | 105 MB | 251 ms |
9846
- | 100,000 × 384-d | 65 ms | 154 MB | 211 MB | 939 ms |
9847
- | 10,000 × 1536-d | 16 ms | 61 MB | 83 MB | 122 ms |
9848
- | 50,000 × 1536-d | 89 ms | 307 MB | 413 MB | **5.7 s** |
10045
+ | corpus | query | resident matrix | file | first search (hydration) |
10046
+ | --------------- | ----- | --------------- | ------ | ------------------------ |
10047
+ | 10,000 × 384-d | 6 ms | 15 MB | 21 MB | 45 ms |
10048
+ | 50,000 × 384-d | 31 ms | 77 MB | 105 MB | 251 ms |
10049
+ | 100,000 × 384-d | 65 ms | 154 MB | 211 MB | 939 ms |
10050
+ | 10,000 × 1536-d | 16 ms | 61 MB | 83 MB | 122 ms |
10051
+ | 50,000 × 1536-d | 89 ms | 307 MB | 413 MB | **5.7 s** |
9849
10052
 
9850
10053
  **The documented ceiling is 50,000 chunks** — under 100 ms per query at every
9851
10054
  embedder this library ships, under ~300 MB resident. It degrades linearly to
@@ -9928,13 +10131,12 @@ safe to answer with "no matches".**
9928
10131
  - The BLOB on disk keeps the **original** vector, so `get`/`list` round-trip
9929
10132
  exactly what was written; normalisation happens once, into the resident
9930
10133
  matrix, so search is a dot product without changing what is stored.
9931
- - Found while writing the schema-identity check: it originally ran *after* the
10134
+ - Found while writing the schema-identity check: it originally ran _after_ the
9932
10135
  indexes were created, so a foreign `af_vectors` table failed on a missing
9933
10136
  column and was reported as `'cannot-open'` — the right refusal for the wrong
9934
10137
  reason, telling the reader to check file permissions when the real problem was
9935
10138
  that the file belonged to something else. It runs before them now.
9936
10139
 
9937
-
9938
10140
  ## [8.8.0] - 2026-08-06
9939
10141
 
9940
10142
  **Retrieval tells the truth.** A retrieval computed a cosine score for every candidate
@@ -9969,12 +10171,14 @@ the two are registered separately, each with its own store:
9969
10171
  ```ts
9970
10172
  const agent = Agent.create({ provider })
9971
10173
  .rag(defineRAG({ id: 'product-docs', store: corpusStore, embedder }))
9972
- .memory(defineMemory({
9973
- id: 'chat',
9974
- type: MEMORY_TYPES.EPISODIC,
9975
- strategy: { kind: MEMORY_STRATEGIES.WINDOW, size: 10 },
9976
- store: conversationStore,
9977
- }))
10174
+ .memory(
10175
+ defineMemory({
10176
+ id: 'chat',
10177
+ type: MEMORY_TYPES.EPISODIC,
10178
+ strategy: { kind: MEMORY_STRATEGIES.WINDOW, size: 10 },
10179
+ store: conversationStore,
10180
+ }),
10181
+ )
9978
10182
  .build();
9979
10183
  ```
9980
10184
 
@@ -9996,7 +10200,7 @@ namespace it reads from — defaulting to the same `'_global'` the indexer write
9996
10200
  index with no options and retrieve with no options and the documents are found. Pass it
9997
10201
  explicitly for a per-tenant corpus, on both sides.
9998
10202
 
9999
- And a namespace that holds nothing is now *reported* rather than answered around:
10203
+ And a namespace that holds nothing is now _reported_ rather than answered around:
10000
10204
  `corpusEmpty: true` on the retrieval event, plus a once-per-process warning naming the
10001
10205
  namespace it searched and the usual cause.
10002
10206
 
@@ -10015,7 +10219,7 @@ namespace it searched and the usual cause.
10015
10219
  are admitted**: `search` returns score-descending, so either the whole pool clears the
10016
10220
  floor (admitted = first `k`, as before) or some entry fails it (every later entry fails
10017
10221
  too, so the pool already holds every entry that clears it). `rejectWindow` only controls
10018
- how many near-misses can be *shown*.
10222
+ how many near-misses can be _shown_.
10019
10223
  - **`agentfootprint.memory.retrieved`** (new, 72 typed events) — one per retrieval,
10020
10224
  carrying every candidate. `candidates: undefined` means the store ranked server-side
10021
10225
  and returned nothing comparable; it never means there were none.
@@ -10056,7 +10260,7 @@ recency ordering the best-scoring chunk can land last.
10056
10260
  ### Chunks the model can cite
10057
10261
 
10058
10262
  A retrieved page of a PDF rendered as `<memory role="unknown" turn="0">` under the header
10059
- *"Relevant context from prior conversations"* — three claims that were not true of a
10263
+ _"Relevant context from prior conversations"_ — three claims that were not true of a
10060
10264
  document, and no way to cite it. `defineRAG` renders a corpus as what it is:
10061
10265
 
10062
10266
  ```text
@@ -10108,13 +10312,12 @@ nothing to configure.
10108
10312
  `core/`; a second implementation is how two recordings of the same bytes end up
10109
10313
  disagreeing about their id.
10110
10314
 
10111
-
10112
10315
  ## [8.7.0] - 2026-08-06
10113
10316
 
10114
10317
  **The check-up stops being quiet, and a dead option stops pretending.** 8.4.0 stopped a
10115
10318
  skill graph from throwing away what the author declared; 8.5.0 stopped it telling the
10116
10319
  model things that were not so. This one is about the configurations the library
10117
- *watched you build and said nothing about* — an entry menu with no way to choose from
10320
+ _watched you build and said nothing about_ — an entry menu with no way to choose from
10118
10321
  it, a transition the cursor can never take, a tool name two sources claim, a scoped
10119
10322
  tool provider that returns nothing forever. Nine findings, one shape: the library knew,
10120
10323
  and did not say.
@@ -10122,7 +10325,7 @@ and did not say.
10122
10325
  ### An entry menu with no way to choose from it
10123
10326
 
10124
10327
  Declare two entries and no `.entryBy()` / `.entryByRead()`, and both of them load on
10125
- every call. An entry's compiled trigger is cursor-*independent* — no `when` compiles to
10328
+ every call. An entry's compiled trigger is cursor-_independent_ — no `when` compiles to
10126
10329
  `{ kind: 'always' }` — while exactly ONE of them can be the cursor: the first whose
10127
10330
  `when` passes. So the extras pay for their body and their tools on every iteration and
10128
10331
  route nothing, which is the opposite of what a skill graph is for.
@@ -10168,7 +10371,7 @@ all versus only bare edges incoming.
10168
10371
 
10169
10372
  ### `unreachable-skill` is told per trigger kind
10170
10373
 
10171
- The sentence *"it can only be reached by the model via read_skill"* is true for an
10374
+ The sentence _"it can only be reached by the model via read_skill"_ is true for an
10172
10375
  `llm-activated` trigger and for no other kind — `Agent.openSkillIds()` admits an open
10173
10376
  pick only for that one. But `deriveTrigger` returns null for an unwired skill, so a
10174
10377
  skill that arrived carrying a hand-authored `rule` trigger **kept it**, and the warning
@@ -10238,7 +10441,7 @@ const graphScoped = (id: string, tools: Tool[]): ToolProvider => ({
10238
10441
  - **`ToolDispatchContext.activeSkillIds`** — the real active set for this iteration.
10239
10442
  Optional, so a provider written before 8.7.0 sees `undefined` and behaves as it did.
10240
10443
  - **`agentfootprint.tools.shadowed`** (71 typed events now) — `{ toolName, iteration,
10241
- schemaFrom, schemaFromId?, dispatchTo, dispatchToId? }`. Names only: never args, never
10444
+ schemaFrom, schemaFromId?, dispatchTo, dispatchToId? }`. Names only: never args, never
10242
10445
  results, never a description body.
10243
10446
  - **`skillScopedToolsTarget` / `SKILL_SCOPED_TOOLS_ID_PREFIX`** — the provider-id
10244
10447
  convention, readable by anyone composing providers.
@@ -10256,7 +10459,7 @@ const graphScoped = (id: string, tools: Tool[]): ToolProvider => ({
10256
10459
  object-literal form since 8.4.0. **Behavior change.** A fluent graph with an
10257
10460
  error-level problem — `no-entry` or `unknown-skill`, i.e. a graph that cannot start a
10258
10461
  turn at all — built in silence outside dev mode and surfaced as a run that entered no
10259
- skill. What still builds: every graph whose check-up has no *error* (warnings never
10462
+ skill. What still builds: every graph whose check-up has no _error_ (warnings never
10260
10463
  throw, however many); every call passing `check: 'warn'` explicitly, which still never
10261
10464
  throws, so the mode keeps its name and its meaning; `check: 'off'` skips entirely.
10262
10465
  Only code that was already shipping a graph the library could not start is affected.
@@ -10265,7 +10468,7 @@ const graphScoped = (id: string, tools: Tool[]): ToolProvider => ({
10265
10468
  activates the skill — and no such tool has ever been built. The evaluator activates an
10266
10469
  `llm-activated` skill by matching `ctx.activatedInjectionIds`, which only `read_skill`
10267
10470
  writes, and it has never read the field. A skill declaring `viaToolName:
10268
- 'open_playbook'` activated through `read_skill` exactly like every other skill, so the
10471
+ 'open_playbook'` activated through `read_skill` exactly like every other skill, so the
10269
10472
  declaration described a door that does not exist. Nothing that worked stops working;
10270
10473
  a silent no-op becomes a named one, at `Agent.injection()` — the one funnel `.skill()`,
10271
10474
  `.skills()`, `.skillGraph()`, `skillsFromDir()` and a hand-built Injection all pass
@@ -10338,7 +10541,7 @@ it raises `CredentialConsentRequiredError`.
10338
10541
  back `authorization-required`.
10339
10542
  - **`CredentialConsentRequiredError`** (`ERR_CREDENTIAL_CONSENT_REQUIRED`, from
10340
10543
  `agentfootprint/identity`) — carries `service`, `sessionId`, `authorizationUrl`,
10341
- `tool` and `iteration`. The error *message* deliberately omits the URL, because
10544
+ `tool` and `iteration`. The error _message_ deliberately omits the URL, because
10342
10545
  a message is the one string that reliably reaches a log line.
10343
10546
  - **`pauseData.authorization`** — `{ service, authorizationUrl, sessionId }` on a
10344
10547
  consent pause, surfaced by `standingAgent` as `PendingAsk.pauseData`. The
@@ -10366,7 +10569,7 @@ it raises `CredentialConsentRequiredError`.
10366
10569
  This is the mirror image of a guarantee the library kept carefully everywhere
10367
10570
  else. `agentfootprint.credential.authorization_required` was designed to carry
10368
10571
  `{ service, sessionId }` and never the URL; OTel and X-Ray record the tool
10369
- result's *type* and never its value; the audit bundle's default `bounded` mode
10572
+ result's _type_ and never its value; the audit bundle's default `bounded` mode
10370
10573
  maps `tool_end.result` to `[type: string]`. Every observer channel was
10371
10574
  disciplined. The one channel nobody thought of as an observer — the
10372
10575
  conversation — was not, and it feeds all the others.
@@ -10506,7 +10709,7 @@ that gave the middleware-ask outcome union no `result` arm.
10506
10709
  ## [8.5.0] - 2026-08-06
10507
10710
 
10508
10711
  **`read_skill` tells the whole truth.** 8.4.0 stopped a skill graph from throwing
10509
- away what the author declared. This one stops it from telling the *model* things
10712
+ away what the author declared. This one stops it from telling the _model_ things
10510
10713
  that were not so. Five findings, all of the same shape: the library said a thing had
10511
10714
  happened, or offered a thing it would refuse, or recorded a cause that was not the
10512
10715
  cause. One is a build-time refusal, one is a gate refusal, three are fixes.
@@ -10515,8 +10718,8 @@ cause. One is a build-time refusal, one is a gate refusal, three are fixes.
10515
10718
 
10516
10719
  A `tree()` routes by predicate on every iteration. It has no cursor, so `read_skill`
10517
10720
  has nothing to move. But `graph.reachableSkills()` reported **all the leaves**, so
10518
- the gate accepted a leaf pick and `read_skill` answered *"Skill 'x' activated for the
10519
- next iteration"* — and nothing happened. A leaf compiles to a `rule` trigger; a
10721
+ the gate accepted a leaf pick and `read_skill` answered _"Skill 'x' activated for the
10722
+ next iteration"_ — and nothing happened. A leaf compiles to a `rule` trigger; a
10520
10723
  `read_skill` call writes only `activatedInjectionIds`; no `rule` trigger reads that.
10521
10724
  The leaf never activated, the tree re-decided by predicate, and the run then emitted
10522
10725
  `agentfootprint.skill.reroute_superseded` naming a winner that **did not exist** —
@@ -10542,7 +10745,7 @@ though the tool accepted the name. Answer with the skill the tree routed to, or
10542
10745
  **Behavior change:** `graph.reachableSkills()` now returns `[]` for a decision
10543
10746
  `tree()`, from every cursor. Its contract is "what `read_skill` may jump to", and
10544
10747
  all-leaves was the lie; use `graph.skills` to enumerate leaves, which is what it was
10545
- always for. `read_skill` is not dead under a tree — anything registered *beside* the
10748
+ always for. `read_skill` is not dead under a tree — anything registered _beside_ the
10546
10749
  graph (`.skill(x)`, `.skills(reg)`, `.selfExplain()`) is **open** and still admitted
10547
10750
  from anywhere, because those really do activate by `read_skill`. Two docstrings that
10548
10751
  promised "read_skill stays a full escape hatch there" are corrected.
@@ -10568,7 +10771,7 @@ Not reachable from here (read_skill for these will be refused):
10568
10771
  ```
10569
10772
 
10570
10773
  **The enum stays the full catalog, deliberately.** `toolArgValidation` defaults to
10571
- `'enforce'` and runs *before* the gate; an off-enum id is rejected with a generic
10774
+ `'enforce'` and runs _before_ the gate; an off-enum id is rejected with a generic
10572
10775
  schema error and never reaches it. Narrowing the enum would therefore have retired
10573
10776
  the gate's teaching refusal, the `agentfootprint.skill.rejected` event,
10574
10777
  `routeRecorder`'s rejection hops and the rejected-cap governor's only input — four
@@ -10602,7 +10805,7 @@ Use 'both' (system prompt AND tool result) or 'system-prompt'.
10602
10805
  ```
10603
10806
 
10604
10807
  Refusal rather than a quiet fall back to the system slot: the author wrote
10605
- `'tool-only'` to keep the body *out* of the system prompt, and silently putting it
10808
+ `'tool-only'` to keep the body _out_ of the system prompt, and silently putting it
10606
10809
  back would honour the activation while breaking the declaration — a different lie,
10607
10810
  not a fix. `'both'` already means "deliver it either way".
10608
10811
 
@@ -10630,7 +10833,7 @@ drift:
10630
10833
  - `routeRecorder()` reads it, and a `'model-pick'` hop carries **no** `edgeLabel`.
10631
10834
 
10632
10835
  This settles the one case no observer could reconstruct: an edge and a same-turn pick
10633
- naming the *same* skill resolves to `'route'` (`D1 > D2`), and only the resolver
10836
+ naming the _same_ skill resolves to `'route'` (`D1 > D2`), and only the resolver
10634
10837
  knows. Without `cursorMove` (an older graph, an older recording) the previous
10635
10838
  inference still stands.
10636
10839
 
@@ -10665,11 +10868,11 @@ refusal on the agent, and the gate one is a fix, not a refusal.
10665
10868
  cannot move the graph somewhere the graph doesn't go. That set is about the CURSOR,
10666
10869
  but it was being used as the whole catalog, so three shapes were dead:
10667
10870
 
10668
- | you wrote | before | now |
10669
- |---|---|---|
10670
- | `.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 |
10671
- | `.skillGraph(g).skill(x)` / `.skills(reg)` | `x` was listed in `read_skill`'s own menu and refused every time; its body was unreachable | activates |
10672
- | `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 |
10871
+ | you wrote | before | now |
10872
+ | ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
10873
+ | `.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 |
10874
+ | `.skillGraph(g).skill(x)` / `.skills(reg)` | `x` was listed in `read_skill`'s own menu and refused every time; its body was unreachable | activates |
10875
+ | `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 |
10673
10876
 
10674
10877
  A skill is **open** when its trigger is `llm-activated` (the trigger `read_skill`
10675
10878
  actually activates — a rule-gated injection is still refused, because admitting it
@@ -10794,7 +10997,7 @@ dropped pick is reported rather than swallowed — see the new event below.
10794
10997
  ### `agentfootprint.skill.reroute_superseded` (new typed event — 70 total)
10795
10998
 
10796
10999
  Fires in exactly one case: a `read_skill` the gate accepted did not end up active
10797
- because a declared edge won the same turn (the model emitted a domain tool *and*
11000
+ because a declared edge won the same turn (the model emitted a domain tool _and_
10798
11001
  `read_skill` in one message). Payload: `{ volunteeredId, wonId, fromSkillId,
10799
11002
  iteration }`. It is derived from the real active set, not from which clause won,
10800
11003
  so it cannot fire for a pick that did take effect.
@@ -10838,7 +11041,7 @@ never engages any of this.
10838
11041
 
10839
11042
  **Durable compaction.** An agent that has been up for a week folds week one
10840
11043
  into a summary. Then it gets deployed over. It comes back, is handed the same
10841
- conversation — and now it can still tell you what week one was about, *and*
11044
+ conversation — and now it can still tell you what week one was about, _and_
10842
11045
  show you week one, word for word.
10843
11046
 
10844
11047
  The window half of that already worked: a summary is an ordinary message, so
@@ -10909,14 +11112,14 @@ surprise.
10909
11112
 
10910
11113
  ### New exports
10911
11114
 
10912
- | export | what it is |
10913
- | --- | --- |
10914
- | `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". |
10915
- | `foldedMessages(conversation)` | Every retained message from every span, oldest fold first. |
10916
- | `FoldedSpan` · `CompactionRetention` · `FoldedConversation` | The types. |
11115
+ | export | what it is |
11116
+ | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
11117
+ | `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". |
11118
+ | `foldedMessages(conversation)` | Every retained message from every span, oldest fold first. |
11119
+ | `FoldedSpan` · `CompactionRetention` · `FoldedConversation` | The types. |
10917
11120
 
10918
11121
  The fingerprint is also what makes the join **forgery-proof**: `isCompactedSummary`
10919
- answers "this *looks* like a frame", which is all a prefix check can see, and a
11122
+ answers "this _looks_ like a frame", which is all a prefix check can see, and a
10920
11123
  model that copies the frame's opening words passes it. `foldedSpanFor` answers
10921
11124
  the stronger question — different content, different fingerprint, no match.
10922
11125
 
@@ -10977,14 +11180,14 @@ openai({ baseURL: 'http://localhost:11434/v1', apiKey: 'ollama' });
10977
11180
 
10978
11181
  Two things go wrong with a local runtime, and each has a one-command answer. Both
10979
11182
  now raise a typed `OllamaUnavailableError` (discriminated by `reason`) whose
10980
- message *is* the instruction — never a raw `ECONNREFUSED`, never a bare `404`,
11183
+ message _is_ the instruction — never a raw `ECONNREFUSED`, never a bare `404`,
10981
11184
  and never a hang: a deadline bounds the wait for the daemon to answer (not
10982
11185
  generation, so a slow model is untouched).
10983
11186
 
10984
11187
  - **Daemon not running** — names the address it tried, `ollama serve`, the
10985
11188
  install link, and how to point somewhere else.
10986
11189
  - **Model not pulled** — names `ollama pull <model>`, and asks `/api/tags` so it
10987
- can also list what this machine *does* have.
11190
+ can also list what this machine _does_ have.
10988
11191
 
10989
11192
  ### Also in this release
10990
11193
 
@@ -10993,7 +11196,7 @@ generation, so a slow model is untouched).
10993
11196
  compaction and cost budgets work against a local model.
10994
11197
  - **Thinking blocks from local reasoning models.** `ollama('deepseek-r1', { think: true })`
10995
11198
  asks Ollama to lift reasoning out of the answer; the new `ollamaThinkingHandler`
10996
- auto-wires by provider name and normalizes it. When a model was *not* asked and
11199
+ auto-wires by provider name and normalizes it. When a model was _not_ asked and
10997
11200
  writes `<think>…</think>` into the answer instead, the library **recognizes the
10998
11201
  shape and surfaces the blocks, but does not edit the answer** — silently
10999
11202
  rewriting model output is a change of meaning, and that belongs to the
@@ -11039,32 +11242,32 @@ The doors are now named for the job:
11039
11242
 
11040
11243
  **Every old import path still works, unchanged, for all of 8.x.** They are
11041
11244
  marked `@deprecated` so your editor points at the new door; nothing is logged,
11042
- nothing breaks, and each one re-exports the *same symbols* the door carries —
11245
+ nothing breaks, and each one re-exports the _same symbols_ the door carries —
11043
11246
  not copies. `test/api-conformance/door-aliases.test.ts` drives the TypeScript
11044
11247
  checker over the shipped `.d.ts` files to prove it, name by name, so the
11045
11248
  aliases cannot drift. They are removed in 9.0.0.
11046
11249
 
11047
11250
  ### Migration
11048
11251
 
11049
- | you were importing from | import from |
11050
- |---|---|
11051
- | `agentfootprint/llm-providers` | `agentfootprint/providers` |
11052
- | `agentfootprint/embedders` | `agentfootprint/providers` |
11053
- | `agentfootprint/tool-providers` | `agentfootprint/providers` |
11054
- | `agentfootprint/thinking` | `agentfootprint/providers` |
11055
- | `agentfootprint/memory-providers` | `agentfootprint/memory` |
11056
- | `agentfootprint/observability-providers` | `agentfootprint/observe` |
11057
- | `agentfootprint/strategies` | `agentfootprint/observe` |
11058
- | `agentfootprint/stream` | `agentfootprint/observe` |
11059
- | `agentfootprint/status` | `agentfootprint/observe` |
11060
- | `agentfootprint/locales` | `agentfootprint/observe` |
11061
- | `agentfootprint/debug` | `agentfootprint/observe` |
11062
- | `agentfootprint/debug/finders` | `agentfootprint/observe` |
11063
- | `agentfootprint/observability/contextError/finders` | `agentfootprint/observe` |
11064
- | `agentfootprint/reliability` | `agentfootprint/resilience` |
11065
- | `agentfootprint/hosting-providers` | `agentfootprint/hosting` |
11066
- | `agentfootprint/injection-engine` | `agentfootprint/context` |
11067
- | `agentfootprint/identity` | `agentfootprint/security` |
11252
+ | you were importing from | import from |
11253
+ | --------------------------------------------------- | --------------------------- |
11254
+ | `agentfootprint/llm-providers` | `agentfootprint/providers` |
11255
+ | `agentfootprint/embedders` | `agentfootprint/providers` |
11256
+ | `agentfootprint/tool-providers` | `agentfootprint/providers` |
11257
+ | `agentfootprint/thinking` | `agentfootprint/providers` |
11258
+ | `agentfootprint/memory-providers` | `agentfootprint/memory` |
11259
+ | `agentfootprint/observability-providers` | `agentfootprint/observe` |
11260
+ | `agentfootprint/strategies` | `agentfootprint/observe` |
11261
+ | `agentfootprint/stream` | `agentfootprint/observe` |
11262
+ | `agentfootprint/status` | `agentfootprint/observe` |
11263
+ | `agentfootprint/locales` | `agentfootprint/observe` |
11264
+ | `agentfootprint/debug` | `agentfootprint/observe` |
11265
+ | `agentfootprint/debug/finders` | `agentfootprint/observe` |
11266
+ | `agentfootprint/observability/contextError/finders` | `agentfootprint/observe` |
11267
+ | `agentfootprint/reliability` | `agentfootprint/resilience` |
11268
+ | `agentfootprint/hosting-providers` | `agentfootprint/hosting` |
11269
+ | `agentfootprint/injection-engine` | `agentfootprint/context` |
11270
+ | `agentfootprint/identity` | `agentfootprint/security` |
11068
11271
 
11069
11272
  `agentfootprint`, `agentfootprint/memory`, `agentfootprint/observe`,
11070
11273
  `agentfootprint/security`, `agentfootprint/hosting` and
@@ -11078,7 +11281,7 @@ aliases cannot drift. They are removed in 9.0.0.
11078
11281
  executing those registrations and carrying them in every bundle. Side-effectful
11079
11282
  code stays behind its own plainly-named door.
11080
11283
  - **`agentfootprint/events`** is not folded into `/observe`. It is the typed
11081
- wire vocabulary observers *read*, not a tool for watching — and concretely,
11284
+ wire vocabulary observers _read_, not a tool for watching — and concretely,
11082
11285
  its `ContextSource` (the injection-flavour union: `'rag' | 'skill' | …`) is a
11083
11286
  completely different type from the `ContextSource` `/observe` already carries
11084
11287
  (the context-bisect record). Two incompatible shapes cannot share a door.
@@ -11123,7 +11326,7 @@ runtime door, and it still returns the `Unsubscribe` you own.
11123
11326
  `CombinedRecorder` keeps its export too.
11124
11327
 
11125
11328
  There is deliberately **no** `WATCH_MOMENTS`. `.act()`'s keys are a closed,
11126
- compiler-pinned list because a rule has to be *told* where it may speak; an
11329
+ compiler-pinned list because a rule has to be _told_ where it may speak; an
11127
11330
  observer attends the whole stream, and a list we published would be a
11128
11331
  vocabulary we then had to keep true against every event ever added.
11129
11332
 
@@ -11159,7 +11362,7 @@ vocabulary we then had to keep true against every event ever added.
11159
11362
  ## [7.28.0] - 2026-08-05
11160
11363
 
11161
11364
  A paused agent is a promise you made to a person. Until this release the library
11162
- handed you that promise as JSON and wished you luck: *store it anywhere.*
11365
+ handed you that promise as JSON and wished you luck: _store it anywhere._
11163
11366
  Anywhere was the whole of the offer.
11164
11367
 
11165
11368
  `sqliteSessions({ file })` is the first battery included — the same
@@ -11182,7 +11385,7 @@ and the next step up was "bring a Redis" — a service to run, secure, back up a
11182
11385
  pay for, to keep a few kilobytes of chat. Everyone in between wrote the same
11183
11386
  little file store themselves and each one re-decided what a half-written file
11184
11387
  means. A pause had no home at all: a question outstanding is the one piece of
11185
- agent state that *must* outlive the process, because the answer arrives on human
11388
+ agent state that _must_ outlive the process, because the answer arrives on human
11186
11389
  time — after lunch, after the deploy, tomorrow. Both land in one table here,
11187
11390
  because `CheckpointEnvelope` was already a union of the two and a session store
11188
11391
  has no business caring which half it is holding.
@@ -11192,8 +11395,7 @@ process (or a few) on ONE machine, writing ONE file. It survives anything that
11192
11395
  ends the process and leaves the disk alone. It is **not** a distributed store:
11193
11396
  two machines do not share a session by both opening a file over a network
11194
11397
  filesystem. WAL gives many readers plus **one writer at a time**, and that is
11195
- the ceiling — a second writer waits for the lock up to `busyTimeoutMs` (default
11196
- 5000) and then fails loudly rather than queueing forever. When you outgrow it,
11398
+ 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,
11197
11399
  one argument to `standingAgent` changes and nothing above it moves.
11198
11400
 
11199
11401
  **A refusal where a fallback would have been easier.** `node:sqlite` ships with
@@ -11209,7 +11411,7 @@ user.
11209
11411
  **"Unreadable is not absent", one level up.** The envelope law already said an
11210
11412
  unreadable stored conversation and an absent one are different facts, and only
11211
11413
  one is safe to answer with a fresh start. A file store can break that promise
11212
- higher up — point it at a log file and a careless adapter opens it as an *empty*
11414
+ higher up — point it at a log file and a careless adapter opens it as an _empty_
11213
11415
  store. So the file is checked at construction and refused with
11214
11416
  `UnreadableSessionFileError`, whose `problem` field is the fact to branch on:
11215
11417
  `'cannot-open'`, `'not-our-schema'` (somebody else's table of that name), or
@@ -11220,7 +11422,7 @@ session that was never written hydrates as `undefined`.
11220
11422
  well as fields inside the JSON, so during an incident `sqlite3` answers "which
11221
11423
  sessions are waiting on a person, and since when?" with no JSON parser and
11222
11424
  without this library. `journalMode` on the returned store reports what the file
11223
- *actually got* rather than what was asked for — a silent downgrade from WAL on a
11425
+ _actually got_ rather than what was asked for — a silent downgrade from WAL on a
11224
11426
  network filesystem is the kind of thing only ever discovered under load.
11225
11427
 
11226
11428
  Added, all on the existing `agentfootprint/hosting` door — no new subpath:
@@ -11252,7 +11454,7 @@ the machine took, and the machine is shared: the suite runs beside a build, a
11252
11454
  coverage pass, and two other vitest workers. Identical code takes three to five
11253
11455
  times longer under that load with nothing about the code having changed. So the
11254
11456
  assertion cannot tell "we got slower" from "the box was busy", and it fires
11255
- *exactly* when CI is busiest. Five of them had already been logged as flakes —
11457
+ _exactly_ when CI is busiest. Five of them had already been logged as flakes —
11256
11458
  `xray` P6, `withCircuitBreaker` P6, `locales/messages` Block D,
11257
11459
  `consumer-domain-events`, `SkillRegistryOptions` — always under concurrent
11258
11460
  build load, always passing in isolation. The failure mode of a guard nobody
@@ -11296,7 +11498,7 @@ still flaked when actually run under the reproduction condition. Three things
11296
11498
  had to be added, each because the proof run said so:
11297
11499
 
11298
11500
  - **Repeat until the sample is worth timing.** Below a scheduler quantum, one
11299
- preemption *is* the measurement: a 0.2ms operation that gets descheduled
11501
+ preemption _is_ the measurement: a 0.2ms operation that gets descheduled
11300
11502
  reads as a hundred times its real cost, while the 100ms operation beside it
11301
11503
  absorbs the same theft as a rounding error — and load stops cancelling. Each
11302
11504
  operation is now repeated inside one sample until the sample clears 20ms, and
@@ -11328,8 +11530,8 @@ load, which is what makes them worth asserting.
11328
11530
  **No perf claim was deleted. The form changed; the meaning stayed** — and in
11329
11531
  four places the meaning got sharper, because writing the claim down properly
11330
11532
  exposed what it had actually been asserting. Three sites keep a millisecond
11331
- ceiling on purpose and say so at the site: they are stated against a *configured
11332
- delay* (a mock's own thinking band, a slow branch's own timeout, a strategy's
11533
+ ceiling on purpose and say so at the site: they are stated against a _configured
11534
+ delay_ (a mock's own thinking band, a slow branch's own timeout, a strategy's
11333
11535
  own per-event block) rather than against a guess about the machine, because
11334
11536
  "did not sleep longer than it was told to" has no cheaper form.
11335
11537
 
@@ -11485,8 +11687,8 @@ each of those is now the failure of the thing that caused it. A 400, a 500, one
11485
11687
  refused upgrade, one ended conversation. Never the process.
11486
11688
 
11487
11689
  One of them was not even a throw. `serveOne`'s promise is held in a Set and
11488
- voided at the call site, so anything that escaped it was an *unhandled
11489
- rejection* — which on node's defaults is the same dead container reached by a
11690
+ voided at the call site, so anything that escaped it was an _unhandled
11691
+ rejection_ — which on node's defaults is the same dead container reached by a
11490
11692
  different road. It is total by construction now, and says so.
11491
11693
 
11492
11694
  **The audit's other finding is that the conversation door was already safe, and
@@ -11503,17 +11705,16 @@ serve. It is a great deal of ceremony when all you wanted was a `/debug/trace`
11503
11705
  beside the agent on the one port the container was given.
11504
11706
 
11505
11707
  ```ts
11506
- nodeHost({ port: 8080, onUnhandled: (req, res) => myRouter(req, res) })
11708
+ nodeHost({ port: 8080, onUnhandled: (req, res) => myRouter(req, res) });
11507
11709
  ```
11508
11710
 
11509
11711
  Same single port, opposite direction: the host binds the socket as it always
11510
- did, and every path it does not own is handed to your code **instead of** its
11511
- 404. The host still never answers for your application — with this hook it no
11712
+ 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
11512
11713
  longer has to 404 for it either.
11513
11714
 
11514
11715
  What it never receives is the interesting half. The paths the host owns —
11515
- `invokePath`, `healthPath`, `conversationPath` — never reach it, *including a
11516
- wrong method on one of them*, because a hook that could claim `POST /invoke`
11716
+ `invokePath`, `healthPath`, `conversationPath` — never reach it, _including a
11717
+ wrong method on one of them_, because a hook that could claim `POST /invoke`
11517
11718
  would be a second door wearing the first one's name. And it is refused at
11518
11719
  construction beside `{ server }`, by name: there, unmatched paths already fall
11519
11720
  through to your own `'request'` listeners, so a second way to answer them would
@@ -11560,7 +11761,7 @@ hosts rather than by care.
11560
11761
  chunks are coerced back to bytes, and no bytes are lost doing it: `setEncoding`
11561
11762
  decodes through a `StringDecoder`, which holds a partial multi-byte sequence
11562
11763
  across a chunk boundary rather than splitting it. Pinned by writing a body in
11563
- two TCP writes with the split placed *inside* a four-byte character and
11764
+ two TCP writes with the split placed _inside_ a four-byte character and
11564
11765
  asserting it round-trips. Reachable only through `{ server }` — the mode built
11565
11766
  for co-listeners — and reproduced there, with a real second listener on a real
11566
11767
  shared socket rather than a stubbed request.
@@ -11649,7 +11850,7 @@ avoid admitting the cost would have been an accounting trick.
11649
11850
  **The corrective message is an authored frame with the validator's error as
11650
11851
  DATA.** The library's own words come first and say that what follows is a
11651
11852
  report about the answer rather than an instruction; the error is quoted
11652
- verbatim; and *nothing authored follows it*, so there is no trailing sentence
11853
+ verbatim; and _nothing authored follows it_, so there is no trailing sentence
11653
11854
  for injected text to pre-empt. A schema whose error message reads "IGNORE ALL
11654
11855
  PREVIOUS INSTRUCTIONS" produces a message that still says, first and in the
11655
11856
  library's voice, what it is. This is exactly the compaction frame's rule
@@ -11728,7 +11929,7 @@ bytes — pinned against 7.25 by test, not by care.
11728
11929
 
11729
11930
  - **`agentfootprint.agent.output_schema_retry`** — one per failed attempt,
11730
11931
  carrying `{ attempt, retriesRemaining, iteration, stage, error, path?,
11731
- correctiveMessageHash }`. 69 typed events across 20 domains. It sits in the
11932
+ correctiveMessageHash }`. 69 typed events across 20 domains. It sits in the
11732
11933
  `agent` domain beside `output_schema_validation_failed`, its in-stage
11733
11934
  sibling; a new domain for one event that has a family home would have been
11734
11935
  taxonomy for its own sake.
@@ -11789,8 +11990,9 @@ side can call. So this release ships a second port beside the first.
11789
11990
  ```ts
11790
11991
  const host = nodeHost({ port: 8080 });
11791
11992
 
11792
- await standingAgent({ agent, sessions, host }); // POST /invoke
11793
- await host.serveConversations((conversation) => { // WS /conversation
11993
+ await standingAgent({ agent, sessions, host }); // POST /invoke
11994
+ await host.serveConversations((conversation) => {
11995
+ // WS /conversation
11794
11996
  conversation.onFrame((frame) => conversation.send(answer(frame)));
11795
11997
  conversation.onClose(({ by, reason }) => log(by, reason));
11796
11998
  });
@@ -11959,11 +12161,11 @@ So this release does two things, and the second is the reason for the first.
11959
12161
  ```ts
11960
12162
  Agent.create({ provider, model })
11961
12163
  .act({
11962
- input: [scrubSSNs], // the message, before the run commits it
11963
- beforeTool: [refundCeiling, fourEyes], // every call, before it is dispatched
11964
- afterTool: [stripPII], // every result, before the model reads it
11965
- window: slidingWindow({ keepRecentTurns: 12 }), // what the live window keeps
11966
- output: [noCodenames], // the answer, before the caller gets it
12164
+ input: [scrubSSNs], // the message, before the run commits it
12165
+ beforeTool: [refundCeiling, fourEyes], // every call, before it is dispatched
12166
+ afterTool: [stripPII], // every result, before the model reads it
12167
+ window: slidingWindow({ keepRecentTurns: 12 }), // what the live window keeps
12168
+ output: [noCodenames], // the answer, before the caller gets it
11967
12169
  })
11968
12170
  .build();
11969
12171
  ```
@@ -11977,8 +12179,8 @@ wire, same rows in the ledger.
11977
12179
 
11978
12180
  The canonical path is preserved by **demoting the doors, not deleting them**.
11979
12181
  `.toolMiddleware()`, `.messageMiddleware()`, `.window()` and `.compaction()`
11980
- are unchanged and stay open, and they are now documented under *Composing
11981
- incrementally* — because adding one rule to an agent somebody else built is a
12182
+ are unchanged and stay open, and they are now documented under _Composing
12183
+ incrementally_ — because adding one rule to an agent somebody else built is a
11982
12184
  real job, and a bundle that must be written all at once cannot do it. That
11983
12185
  division is the one-sentence answer to "which spelling": **`.act()` for an
11984
12186
  agent you own, a door for a piece you are adding to somebody else's.** A second
@@ -12227,8 +12429,8 @@ the honest pick is the one whose bytes come back unchanged.
12227
12429
  **The law, which is the part that outlives this vendor.** In the words of the
12228
12430
  field report that bought it:
12229
12431
 
12230
- > *An unreadable stored conversation and an absent one are different facts, and
12231
- > only one of them is safe to answer with a fresh start.*
12432
+ > _An unreadable stored conversation and an absent one are different facts, and
12433
+ > only one of them is safe to answer with a fresh start._
12232
12434
 
12233
12435
  A session nobody has used is absent, and answering it fresh is right. A session
12234
12436
  whose bytes are present and unreadable is not, and answering THAT fresh is
@@ -12246,7 +12448,7 @@ through to the fresh-start path.
12246
12448
  meant reading how this repo writes an event blob, and `AgentCoreStore` — the
12247
12449
  `MemoryStore` adapter — wrote them identically: `payload: [{ blob: entry }]`,
12248
12450
  read back as objects only. Same service, same mangling, same silence, different
12249
- loss: an entry that decodes to nothing was *skipped*, so `list()` came back one
12451
+ loss: an entry that decodes to nothing was _skipped_, so `list()` came back one
12250
12452
  memory short and `get()` came back `null`. Memory that silently stays empty is
12251
12453
  indistinguishable from memory that works, until somebody notices the assistant
12252
12454
  has forgotten a customer's address. Shipping the cure for one organ while the
@@ -12289,8 +12491,8 @@ and are still described that way.
12289
12491
  refused loudly rather than decoded to `undefined`.
12290
12492
 
12291
12493
  - **`hydrate` no longer answers "no session" for a session that HAS one.** The
12292
- adapter's decode step now distinguishes *no blob at all* (an absence, which
12293
- hydrates as `undefined`) from *a blob it cannot read* (which travels on to the
12494
+ adapter's decode step now distinguishes _no blob at all_ (an absence, which
12495
+ hydrates as `undefined`) from _a blob it cannot read_ (which travels on to the
12294
12496
  shared reading law and is refused by name). Both file and event modes pass the
12295
12497
  session id into `checkEnvelope`, so a refusal names the conversation.
12296
12498
 
@@ -12439,7 +12641,7 @@ than by an opinion.** See below.
12439
12641
  satisfy the container contract this repo documents for a managed agent
12440
12642
  runtime, and is not trying to.** It serves MCP — statelessly (it neither
12441
12643
  issues nor demands a session id, so replicas are interchangeable), on the path
12442
- and port you choose — and it answers *neither* of the contract's two routes:
12644
+ and port you choose — and it answers _neither_ of the contract's two routes:
12443
12645
  `GET /ping` and `POST /invocations` are 404s from it. Two protocols, two
12444
12646
  paths, two adapters: serve the container contract with
12445
12647
  `agentCoreRuntimeHost` and MCP with `mcpServe`. The one thing that is NOT
@@ -12454,7 +12656,7 @@ The refusal becomes acceptance.
12454
12656
  Three releases ago the messages slot was a lie: content declared for it was
12455
12657
  recorded as injected, counted in the slot composition, routed by the engine, and
12456
12658
  never sent. 7.19.1 refused the declaration by name rather than deliver it badly,
12457
- and said why in the same breath — the wire has no system role *inside* the
12659
+ and said why in the same breath — the wire has no system role _inside_ the
12458
12660
  message list on the Anthropic family (system is a separate top-level field)
12459
12661
  while the OpenAI family carries it, so wiring the slot straight through would
12460
12662
  have replaced one uniform gap with a **provider-dependent** one that nothing in
@@ -12479,7 +12681,7 @@ refused when the run starts, naming the provider and the roles it does. The role
12479
12681
  is never rewritten to one that fits — changing who appears to speak is a meaning
12480
12682
  change the app must make, not the library. **Position**: a delivered message goes
12481
12683
  at the end of the window, and if its role would repeat the turn already there, it
12482
- is *deferred* to the next boundary with a sentence on
12684
+ is _deferred_ to the next boundary with a sentence on
12483
12685
  `messagesDelivery.deferred`, never dropped and never reordered, and never
12484
12686
  inserted between a tool call and its result.
12485
12687
 
@@ -12535,7 +12737,7 @@ end. An honest limitation stated loudly beats a clever one hidden.
12535
12737
  ### Fixed
12536
12738
 
12537
12739
  - **The cache marker for `field: 'messages'` pointed at the wrong message.** It
12538
- counted entries in a per-slot list of *injections* and handed that count to
12740
+ counted entries in a per-slot list of _injections_ and handed that count to
12539
12741
  providers who read it as a position in `request.messages` — two index spaces
12540
12742
  under one name. It was unreachable while nothing could target the slot, and
12541
12743
  delivery makes it reachable, so it is recomputed against the actual wire array
@@ -12623,7 +12825,6 @@ end. An honest limitation stated loudly beats a clever one hidden.
12623
12825
  request's tool array. It is out of this release's scope, and it is now the only
12624
12826
  one of the pair left.
12625
12827
 
12626
-
12627
12828
  ## [7.20.0] - 2026-08-03
12628
12829
 
12629
12830
  Three small honesty fixes. No new machinery, no delivery change, no wire bytes
@@ -12733,7 +12934,7 @@ message-sequence rule; it is queued as a feature, with this gap as its evidence.
12733
12934
  / `.instruction` / `.fact` all pass through — refuses a hand-built `Injection`
12734
12935
  carrying `inject.messages`, so the refusal cannot be walked around. The
12735
12936
  message names the limitation and the working alternatives: `slot:
12736
- 'system-prompt'` (the default, delivered by every provider), a tool's return
12937
+ 'system-prompt'` (the default, delivered by every provider), a tool's return
12737
12938
  value (a tool result IS a recent message, at the recency the option was
12738
12939
  reaching for), and the text passed to `agent.run({ message })`.
12739
12940
 
@@ -12941,7 +13142,7 @@ Example: `examples/deploy/durable-sessions.ts`.
12941
13142
  ## [7.18.0] - 2026-08-03
12942
13143
 
12943
13144
  Every agent framework lets you wrap a tool call. Most of them let the wrapper
12944
- *answer* — return a canned string, a cached value, a "simulated" result — and
13145
+ _answer_ — return a canned string, a cached value, a "simulated" result — and
12945
13146
  the moment one does, the trace is fiction. The model was told a tool ran.
12946
13147
  Nothing ran.
12947
13148
 
@@ -13000,7 +13201,7 @@ taken afterwards: the trace would show text nobody ever sent.
13000
13201
 
13001
13202
  - **`ask` suspends on the SHIPPED pause machinery.** `isAskPause(outcome)`
13002
13203
  narrows a paused run and `outcome.ask` carries `{ question, detail?,
13003
- middleware }`. Resume with `checkInApproved` / `checkInDeclined` — the same
13204
+ middleware }`. Resume with `checkInApproved` / `checkInDeclined` — the same
13004
13205
  human-answer vocabulary check-ins use, deliberately, because a person
13005
13206
  approving is a person approving and one word for one thing beats a synonym.
13006
13207
  A malformed resume DECLINES, so a governed call can never execute because a
@@ -13137,7 +13338,7 @@ measured lifetime. Removing is not forgetting.
13137
13338
  token budget, and reporting a `capTokens` nobody configured would be exactly
13138
13339
  the invented number this family refuses.
13139
13340
 
13140
- `keepRecentTurns` is required and has no default. It *is* the policy.
13341
+ `keepRecentTurns` is required and has no default. It _is_ the policy.
13141
13342
 
13142
13343
  - **`tokenBudget({ thresholdTokens, keepRecentTurns? })` — counted, then
13143
13344
  dropped.** Reads the input tokens the adapter reported for the last call and
@@ -13185,7 +13386,7 @@ measured lifetime. Removing is not forgetting.
13185
13386
  It appears only at the head (a removal in the middle leaves the opening turn
13186
13387
  in place, so there is nothing to fix and a spliced `user` message is its own
13187
13388
  risk); it never accumulates (the next drop absorbs it); and if it would not
13188
- be *smaller* than the span it replaces, the whole drop is abandoned under
13389
+ be _smaller_ than the span it replaces, the whole drop is abandoned under
13189
13390
  `summary-not-smaller`, whose meaning generalizes to "the replacement came
13190
13391
  back no smaller than the span" rather than growing the closed reason union.
13191
13392
 
@@ -13279,7 +13480,7 @@ arrived yet.
13279
13480
  Example: `examples/context-engineering/11-compaction.ts`.
13280
13481
 
13281
13482
  - **`CompactionRecord` on `scope.compactions` — the fold's half of the law.**
13282
- One record per over-budget visit, *including the visits that folded nothing*,
13483
+ One record per over-budget visit, _including the visits that folded nothing_,
13283
13484
  which are the interesting ones. It carries `foldedStageIds` (real
13284
13485
  `runtimeStageId`s, resolvable in the commit log), `foldedMessageCount`,
13285
13486
  `measuredTokens` vs `thresholdTokens`, exact `windowCharsBefore` /
@@ -13303,7 +13504,7 @@ arrived yet.
13303
13504
  - **`COMPACTED_FRAME_PREFIX` / `isCompactedSummary(msg)`** — the authored frame
13304
13505
  is a library constant and the summarizer's text is appended after it as data.
13305
13506
  A summarizer returning `IGNORE ALL PREVIOUS INSTRUCTIONS` still arrives
13306
- *inside* a message that says, first and in the library's own words, that what
13507
+ _inside_ a message that says, first and in the library's own words, that what
13307
13508
  follows is a summary written by a model and not the conversation. A test pins
13308
13509
  exactly that, with a hostile summarizer. The boundary points both ways: the
13309
13510
  folded transcript reaches the summarizer between markers the authored
@@ -13333,7 +13534,7 @@ arrived yet.
13333
13534
  - **With `.compaction()` configured, the compaction stage becomes the ReAct
13334
13535
  loop target** (`compact`), mounted immediately before the previous one. The
13335
13536
  loop is branch-sourced, so anything ahead of the target runs once and is
13336
- never seen again — and being the target puts the fold *before* the injection
13537
+ never seen again — and being the target puts the fold _before_ the injection
13337
13538
  engine and the three context slots, which is the point: the triggers, the
13338
13539
  slots and the wire then all see one window, and no part of the run reasons
13339
13540
  over a past the model was not shown. Without `.compaction()` the loop target
@@ -13350,9 +13551,9 @@ arrived yet.
13350
13551
  ## [7.15.0] - 2026-08-02
13351
13552
 
13352
13553
  7.14.0 shipped two hosting ports that name no cloud, plus a conformance suite,
13353
- and made a promise: *a cloud adapter is vendor paths and a header mapping on a
13554
+ and made a promise: _a cloud adapter is vendor paths and a header mapping on a
13354
13555
  port that already worked; if writing one needs a change to a port, the port was
13355
- wrong.* A promise like that is worth nothing until somebody writes the adapter.
13556
+ wrong._ A promise like that is worth nothing until somebody writes the adapter.
13356
13557
 
13357
13558
  This release writes it. `agentCoreRuntimeHost` is a real cloud runtime's
13358
13559
  container contract — different paths, different body fields, the conversation id
@@ -13366,8 +13567,8 @@ policy store behind the existing permission port, per-request credential vending
13366
13567
  for Gateway tools, and the memory adapter's `search()` finally wired.
13367
13568
 
13368
13569
  Three seams did have to move, and none of them was a port. They are listed under
13369
- "Changed" rather than buried, because *where an adapter needs more than paths
13370
- and headers* is the interesting result of an exercise like this — and two of the
13570
+ "Changed" rather than buried, because _where an adapter needs more than paths
13571
+ and headers_ is the interesting result of an exercise like this — and two of the
13371
13572
  three turned out not to be about this vendor at all.
13372
13573
 
13373
13574
  ### Added
@@ -13399,7 +13600,7 @@ three turned out not to be about this vendor at all.
13399
13600
  Example: `examples/deploy/agentcore-runtime.ts`.
13400
13601
 
13401
13602
  - **`httpHost({ name, wire, invokePath, healthPath, port?, hostname?,
13402
- capabilities? })` — the HTTP work, parameterised by the JSON dialect it
13603
+ capabilities? })` — the HTTP work, parameterised by the JSON dialect it
13403
13604
  speaks.** Draining on close, aborting when the caller hangs up, failing a
13404
13605
  handler that throws, failing a handler that answers nothing, mapping refusal
13405
13606
  codes to status codes, and choosing between one JSON body and Server-Sent
@@ -13414,7 +13615,7 @@ three turned out not to be about this vendor at all.
13414
13615
  matching and gets it subtly wrong in exactly one deployment.
13415
13616
 
13416
13617
  - **`agentCorePolicy({ policyStoreId, region?, onUnavailable?, onWarning?,
13417
- principalFor?, name?, cacheSize? })` — an AgentCore policy store behind the
13618
+ principalFor?, name?, cacheSize? })` — an AgentCore policy store behind the
13418
13619
  existing `PermissionChecker` port** (`agentfootprint/security`). Every
13419
13620
  attempted tool call becomes one evaluation.
13420
13621
 
@@ -13462,7 +13663,7 @@ three turned out not to be about this vendor at all.
13462
13663
 
13463
13664
  Results are marked `metadata.source: 'agentcore-memory-record'`, because
13464
13665
  `search` reads a genuinely different population than `list`: the records
13465
- AgentCore's extraction strategies *derived from* your events, whose ids belong
13666
+ AgentCore's extraction strategies _derived from_ your events, whose ids belong
13466
13667
  to AgentCore, so `store.get(result.entry.id)` will not find them.
13467
13668
 
13468
13669
  There is **no `stream()`**. AgentCore Memory has no streaming data-plane
@@ -13579,7 +13780,7 @@ and if writing one ever needs a change to a port, the port was wrong.
13579
13780
  - **`standingAgent({ agent, sessions, host, onConcurrentInvoke? })` — the
13580
13781
  composer.** Per request: wake and hydrate the session, resume that
13581
13782
  conversation or start a fresh one, persist what the run left behind, reply.
13582
- Persist happens *before* the answer goes out, so a queued next turn can never
13783
+ Persist happens _before_ the answer goes out, so a queued next turn can never
13583
13784
  read state older than the answer already given.
13584
13785
 
13585
13786
  It restates the `resumeOnError` tool re-execution caveat **verbatim** in its
@@ -13593,7 +13794,7 @@ and if writing one ever needs a change to a port, the port was wrong.
13593
13794
  afterwards belongs to whichever started last, so one session's envelope can end
13594
13795
  up holding another session's conversation with nothing in the recording to say
13595
13796
  so. `ConcurrentInvokePolicy` is the separate question of a second turn of the
13596
- *same* conversation: `'reject'` (default) refuses with a `ConcurrentRunError`
13797
+ _same_ conversation: `'reject'` (default) refuses with a `ConcurrentRunError`
13597
13798
  naming the active run (`409`), `'enqueue'` queues it FIFO behind the run whose
13598
13799
  state it will then read. A request for a **different** session is never
13599
13800
  refused — it waits its turn.
@@ -13634,7 +13835,7 @@ person something, `standingAgent` answers with a `PauseNotCarriedError` and
13634
13835
  writes **nothing** — the session keeps exactly the conversation it had before the
13635
13836
  request. Over HTTP that is a `409`, not a `500`, because the agent did not break
13636
13837
  and every dashboard that sees a 500 will conclude otherwise. `'conversation-v1'`
13637
- stores a conversation; a paused run is a conversation *plus* an engine
13838
+ stores a conversation; a paused run is a conversation _plus_ an engine
13638
13839
  checkpoint, and storing half of it would be worse than storing none. Carrying a
13639
13840
  pause would be a NEW format name in the same envelope — which is precisely what
13640
13841
  the version in the format is for.
@@ -13649,7 +13850,7 @@ host can send any string there, including someone else's.
13649
13850
 
13650
13851
  **The conformance suite is the deliverable, not the tests for it.** One handler
13651
13852
  constant, served by `nodeHost` and by a minimal in-process host that declares
13652
- *no* capabilities so the buffering path is exercised rather than assumed, with a
13853
+ _no_ capabilities so the buffering path is exercised rather than assumed, with a
13653
13854
  final pair of cases invoking both and comparing directly. A future adapter —
13654
13855
  including a cloud one — is measured against that file.
13655
13856
 
@@ -13677,7 +13878,7 @@ other way, and a value committed where run-level values already commit.
13677
13878
  and the body still arrives only after it does. What changes is who can edit a
13678
13879
  playbook, and whether changing the refund policy shows up as a reviewable diff.
13679
13880
 
13680
- A skill body is *instructions to a model*, so where it came from is a security
13881
+ A skill body is _instructions to a model_, so where it came from is a security
13681
13882
  property rather than a convenience — content fetched at run time is content
13682
13883
  someone else can change after you reviewed it. The loader therefore accepts a
13683
13884
  local directory and nothing else: a URL is **refused by name**, not fetched.
@@ -13748,7 +13949,7 @@ other way, and a value committed where run-level values already commit.
13748
13949
 
13749
13950
  - **`McpClientOptions.signal` cancels a hung MCP tool call again — it never
13750
13951
  did.** The signal was being sent as part of the `tools/call` request
13751
- *params*, where an `AbortSignal` JSON-serializes to `{}`: the server received
13952
+ _params_, where an `AbortSignal` JSON-serializes to `{}`: the server received
13752
13953
  a meaningless field and the caller received no cancellation. The SDK takes
13753
13954
  per-request options in a separate trailing argument, which is where the signal
13754
13955
  now goes; it is threaded to `connect()` and `listTools()` for the same reason,
@@ -13787,7 +13988,7 @@ synchronous, and making it async to accommodate a resolver would shift every
13787
13988
  agent's timing for a feature most agents do not use. A resolver that needs I/O
13788
13989
  can do it before `run()` and close over the result.
13789
13990
 
13790
- Every debugging session starts at a *variable* — "where did that instruction come
13991
+ Every debugging session starts at a _variable_ — "where did that instruction come
13791
13992
  from?", "which loop wrote the history it answered from?" — and both halves of the
13792
13993
  answer already existed, in vocabularies that did not meet. footprintjs 9.13 records
13793
13994
  a variable's whole life in commit indices and runtimeStageIds; the localizer thinks
@@ -13798,7 +13999,7 @@ Joining them turned out to buy something bigger than a nicer read-out. The backw
13798
13999
  walk narrows each loop with embedding similarity — a proxy that points at a
13799
14000
  neighbourhood and cannot separate a planted instruction from an innocent same-topic
13800
14001
  sibling. But where the recording carries per-write provenance, one part of that guess
13801
- is unnecessary: the commit log *says* which write produced the value this loop read.
14002
+ is unnecessary: the commit log _says_ which write produced the value this loop read.
13802
14003
  So the walk stops guessing exactly there — and keeps saying so everywhere else.
13803
14004
 
13804
14005
  ### Added
@@ -13826,7 +14027,7 @@ So the walk stops guessing exactly there — and keeps saying so everywhere else
13826
14027
  same hops, same order, same verdicts (pinned by a deep-equal test).
13827
14028
 
13828
14029
  The proxy still picks WHO; dataflow picks WHERE. A stage-level edge never becomes
13829
- an exact hop, and a recorded edge outranks the *inferred* proximate-tool hop —
14030
+ an exact hop, and a recorded edge outranks the _inferred_ proximate-tool hop —
13830
14031
  better evidence wins, and the hop record says which kind it used.
13831
14032
 
13832
14033
  - **`AgentOptions.writeProvenance`** (`'off'` default, `'reads-prefix'` to enable) —
@@ -13843,7 +14044,7 @@ So the walk stops guessing exactly there — and keeps saying so everywhere else
13843
14044
  `coverage: 'exact'` requires **positive** evidence — at least one recorded per-write
13844
14045
  edge — not merely the absence of a conservative one. A key nothing ever reads back
13845
14046
  (the agent's `lastToolResult`: written by tool-calls, never read by `call-llm`) has
13846
- an empty edge set, so "no conservative edges" is *vacuously* true; scoring that as
14047
+ an empty edge set, so "no conservative edges" is _vacuously_ true; scoring that as
13847
14048
  exact would hand the walk its most confident hop on its least-evidenced key. Absence
13848
14049
  of dataflow is `'unknown'`, never exactness.
13849
14050
 
@@ -13867,7 +14068,7 @@ it with measurements instead of re-deriving why it was strict.
13867
14068
 
13868
14069
  ## [7.11.0] - 2026-08-02
13869
14070
 
13870
- A pipeline whose steps form a *shape* rather than a line — one step feeding two
14071
+ A pipeline whose steps form a _shape_ rather than a line — one step feeding two
13871
14072
  independent lookups, a third waiting for both — had no home here. You could nest
13872
14073
  a `Parallel` inside a `Sequence`, but then you were scheduling it by hand, and
13873
14074
  the values did not survive the trip.
@@ -13939,7 +14140,7 @@ switches keep compiling. Same reasoning as `workflow()` in 7.10.0.
13939
14140
  ## [7.10.0] - 2026-08-02
13940
14141
 
13941
14142
  Two routing-shaped gaps closed. Both were things the docs told you to hand-roll,
13942
- and both were fiddly in the same way: the wiring is easy to get *nearly* right,
14143
+ and both were fiddly in the same way: the wiring is easy to get _nearly_ right,
13943
14144
  and nearly right fails quietly — at run time, several steps away from the
13944
14145
  mistake.
13945
14146
 
@@ -13948,7 +14149,7 @@ mistake.
13948
14149
  - **`llmRouter` — the classic Swarm decision, packaged.** `swarm()` asks for a
13949
14150
  `route()` that is sync and pure, and it means it: the `Conditional` evaluates
13950
14151
  it once per branch predicate and the loop's exit guard evaluates it again
13951
- after every turn. So the LLM decision has to happen *somewhere else*, before
14152
+ after every turn. So the LLM decision has to happen _somewhere else_, before
13952
14153
  the message reaches `route` — and that placement is the part everyone
13953
14154
  re-invented, along with the prompt, the parsing, and a second copy of the
13954
14155
  agent roster that drifts from the first.
@@ -13962,8 +14163,8 @@ mistake.
13962
14163
  with a stale decision.
13963
14164
 
13964
14165
  The decision is validated JSON — `RoutingDecision` = `{ agentId?, message,
13965
- reason? }`. No `agentId` means "done", and the swarm halts through its own
13966
- halt sentinel. An id that isn't in the roster is kept verbatim, *not* quietly
14166
+ reason? }`. No `agentId` means "done", and the swarm halts through its own
14167
+ halt sentinel. An id that isn't in the roster is kept verbatim, _not_ quietly
13967
14168
  swapped for a plausible one: `swarm()`'s existing done/fallback law then ends
13968
14169
  the run, so a hallucinated agent shows up as a halt instead of a wrong answer.
13969
14170
  Unusable output throws `RoutingDecisionError` with the model's raw text
@@ -13998,7 +14199,7 @@ mistake.
13998
14199
  feeds the next step's `{ message }`, the house convention every LLM runner
13999
14200
  speaks; anything else must match exactly) — a chain that doesn't line up is a
14000
14201
  compile error, pinned by `@ts-expect-error` fixtures under `npm run
14001
- test:types`. At run time, values are handed over **unchanged**: objects stay
14202
+ test:types`. At run time, values are handed over **unchanged**: objects stay
14002
14203
  objects. `workflow(draft, edit)` over two `LLMCall`s reads exactly as it
14003
14204
  always did.
14004
14205
 
@@ -14028,7 +14229,7 @@ mistake.
14028
14229
 
14029
14230
  **If you pass `dimensions` today, your vectors change length.** They were the
14030
14231
  model's native length all along; now they are the length you asked for. Anything
14031
- you have already embedded and stored was written at the *old* length, so a store
14232
+ you have already embedded and stored was written at the _old_ length, so a store
14032
14233
  built with `openaiEmbedder({ dimensions: 256 })` on 7.8 holds 1536-long vectors
14033
14234
  and will not match new 256-long queries. **Re-embed, or drop `dimensions` to
14034
14235
  keep the old lengths.**
@@ -14046,7 +14247,7 @@ Also changed, in the same spirit of "`.dimensions` must not lie":
14046
14247
  1536 for everything, so `text-embedding-3-large` under-reported by half.
14047
14248
  - An **unknown model with no `dimensions` is now a construction-time error**
14048
14249
  instead of a silent 1536. This is the breaking edge: `openaiEmbedder({
14049
- baseURL, model: 'nomic-embed-text' })` against a gateway, an Ollama server or
14250
+ baseURL, model: 'nomic-embed-text' })` against a gateway, an Ollama server or
14050
14251
  an Azure deployment name now throws until you state the length. That
14051
14252
  population is exactly the one that was being lied to. One option fixes it:
14052
14253
  `{ dimensions: 768 }`.
@@ -14062,7 +14263,7 @@ Also changed, in the same spirit of "`.dimensions` must not lie":
14062
14263
  - **`localEmbedder({ backend })` / `staticEmbedder({ backend })` — pass an
14063
14264
  already-imported module, and the on-device embedders work in a browser.** To
14064
14265
  keep the heavy peer deps optional, both factories import them through a
14065
- *variable* specifier — which no bundler can see through. The bare name
14266
+ _variable_ specifier — which no bundler can see through. The bare name
14066
14267
  survived a production build and reached the browser unresolved:
14067
14268
  `TypeError: Failed to resolve module specifier '@huggingface/transformers'`.
14068
14269
  The capability was there all along; only the packaging blocked it. Now the
@@ -14099,7 +14300,7 @@ Also changed, in the same spirit of "`.dimensions` must not lie":
14099
14300
 
14100
14301
  - **Docs-truth check — an ongoing, honest answer to "do the docs describe what
14101
14302
  the code actually does?"** `npm run docs:truth` (new CI job `docs-truth`)
14102
- answers three *separate* questions for every capability the package exposes,
14303
+ answers three _separate_ questions for every capability the package exposes,
14103
14304
  because their combinations are different bugs: DECLARED (in the published
14104
14305
  surface), DOCUMENTED (described in prose on the site), EXERCISED (a real run
14105
14306
  produces it). Declared/documented/never-exercised is the shape a dead or
@@ -14113,7 +14314,7 @@ Also changed, in the same spirit of "`.dimensions` must not lie":
14113
14314
  therefore cannot see a single `agentfootprint/<subpath>` symbol. The
14114
14315
  surface is reported per subpath, since root-barrel-vs-subpath is itself a
14115
14316
  known source of user confusion. Events come from `ALL_EVENT_TYPES`.
14116
- - The DOCUMENTED column counts *only* prose on the 63 hand-written pages
14317
+ - The DOCUMENTED column counts _only_ prose on the 63 hand-written pages
14117
14318
  under `docs-next/content/docs`. Both TypeDoc trees
14118
14319
  (`docs-next/content/docs/api/`, `docs/api-reference/`) are excluded — they
14119
14320
  are generated from source, so every symbol appears in them by construction