@ssheleg/agent-stack 0.23.2 → 0.24.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ## 0.24.0 — the agent stack closes its audit findings
2
+
3
+ Sherlock external-v3 (24 findings), each carrying its own executable regression
4
+ under `test/audit_regressions/`.
5
+
6
+ CI now MEASURES the token budget with a real tokenizer instead of estimating it
7
+ from character count — the pinned auditor had been issuing a token verdict from a
8
+ chars/3.9 estimate, which is the defect that script's own doctrine names.
9
+
1
10
  ## v0.23.2 — a path that resolves only from the neighbour's directory, and the description with no room left
2
11
 
3
12
  Family audit 2026-09-06 (wave AUDIT-WAVE-0906) brought three findings for this member.
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@ssheleg/agent-stack",
3
- "version": "0.23.2",
3
+ "version": "0.24.0",
4
4
  "scripts": {
5
- "test": "python3 test/validate.py && python3 test/plant_guard_test.py && node test/installer_test.js"
5
+ "test": "python3 test/validate.py && python3 test/plant_guard_test.py && node test/installer_test.js && npm run test:audit",
6
+ "test:audit": "for t in test/audit_regressions/*.py; do python3 \"$t\" || exit 1; done"
6
7
  },
7
- "description": "Production patterns for AI agent orchestrators tool-calling loops, multi-stage pipelines with checkpoints, LLM provider routing with fallback, four-layer memory with confidence decay plus the wallet side of reselling LLM access. This package is the installer CLI.",
8
+ "description": "Production patterns for AI agent orchestrators \u2014 tool-calling loops, multi-stage pipelines with checkpoints, LLM provider routing with fallback, four-layer memory with confidence decay \u2014 plus the wallet side of reselling LLM access. This package is the installer CLI.",
8
9
  "bin": {
9
10
  "agent-stack": "bin/agent-stack.js"
10
11
  },
@@ -3,7 +3,7 @@
3
3
  "name": "agent-stack",
4
4
  "displayName": "Agent Stack",
5
5
  "description": "Four skills: agent-orchestrator — tool-calling loops, pipelines with checkpoints, provider routing with fallback, memory architecture, plus the wallet side of reselling LLM access; agent-evals — run/trace/thread evals, LLM judges, and fixtures grown from production; agent-interop — MCP servers and clients, A2A agent cards, the MCP Registry, and gateways; agent-harness — system prompts, tool shaping, workflow-vs-agent, and auditing an agent system.",
6
- "version": "0.23.2",
6
+ "version": "0.24.0",
7
7
  "author": {
8
8
  "name": "ssheleg",
9
9
  "url": "https://x.com/sshlg93"
@@ -76,7 +76,7 @@ Assert on three axes at once, with three different mechanisms:
76
76
 
77
77
  | Axis | Assert | With |
78
78
  |---|---|---|
79
- | Trajectory | what the run **must not** do, and what it must have touched — never the order | set/subset matchers, forbidden-call lists |
79
+ | Trajectory | what the run **must not** do, what it must have touched, and the mandatory **happens-before** edges — never the full exact order | set/subset matchers, forbidden-call lists, partial-order (a before b) |
80
80
  | Final response | quality, tone, policy compliance | rubric or judge |
81
81
  | **State change** | the memory row exists, the file was written, the artifact is there | direct inspection of the side effect |
82
82
 
@@ -152,19 +152,26 @@ assertions, tool-call correctness — all deterministic, all faster and cheaper
152
152
  call. Send to a judge only what cannot be decided by code.
153
153
 
154
154
  **Read the trajectory; do not match it.** An agent that reaches a correct answer through
155
- three wrong tool calls is a latent outage — and asserting the *sequence* to catch that is
156
- measurably the wrong instrument. Anthropic names the instinct and rejects its strict form:
157
- exact tool-order assertions are *"too rigid and results in overly brittle tests, as agents
158
- regularly find valid approaches that eval designers didn't anticipate"*, and the worked
155
+ three wrong tool calls is a latent outage — and asserting the *exact sequence* to catch
156
+ that is measurably the wrong instrument. Anthropic rejects its strict form: exact
157
+ tool-order assertions are *"too rigid and results in overly brittle tests, as agents
158
+ regularly find valid approaches that eval designers didn't anticipate"* the worked
159
159
  case is an agent that solved a τ²-bench booking task through a policy loophole, failing
160
160
  the eval as written while serving the user better. Grade **what was produced and what
161
- changed**, and let the path vary.
161
+ changed**, and let the incidental path vary.
162
162
 
163
- The opposite edge is measured too, so this is not "grade the final answer": a grader blind
164
- to the trajectory misses **44% of safety violations and 13% of robustness failures**,
163
+ The opposite edge is measured too: a grader blind to the trajectory misses **44% of safety violations and 13% of robustness failures**,
165
164
  because a policy breach on the way to a correct result leaves no trace in the outcome. Use
166
165
  the trajectory for the claims the outcome cannot carry — a forbidden call, a missing
167
- confirmation, a secret read — as a **set and a forbidden list**, never as an order.
166
+ confirmation, a secret read — as a **set and a forbidden list**.
167
+
168
+ What is forbidden is the redundant **exact global sequence**, not order as such.
169
+ A few **happens-before** edges are semantically mandatory: authorization
170
+ precedes its effect, a fresh read precedes the write depending on it, a
171
+ transaction commits before what publishes it. Assert those as a **partial
172
+ order** (a before b), never a total one — reordering two independent reads must
173
+ pass, reordering confirm/charge or acquire/write must fail — and keep the
174
+ negative example (a confirm-after-charge trace) beside the rubric.
168
175
 
169
176
  **Calibrate the judge before trusting it.** Collect human labels on the same traces,
170
177
  measure agreement, iterate the judge prompt until agreement is high — *then* let it score
@@ -253,10 +260,16 @@ with no production in it is imagination.** The requirement itself gets its id an
253
260
  definition of done from `task-pipeline`'s REQ spine — what this pack owns is the
254
261
  observable's *form*, not the register it hangs on.
255
262
 
256
- **The first release has no production, so its offline gate is observables only** (§3). That
257
- is not the corpus rule suspended for a special case: the corpus is empty because nothing has
258
- run yet, and it fills from the first real traces. Inventing *inputs* to fill it sooner would
259
- still be imagination.
263
+ **The first release has no production so it runs against a SEED corpus, and
264
+ observable-only is not release-ready.** A criterion with no input proves
265
+ no capability, so a greenfield feature seeds a curated/synthetic/manual corpus with at least a **happy**, an **adversarial** and a
266
+ **failure/retry** trial. Each seed input carries its **provenance**
267
+ (`curated`/`synthetic`/`manual`) and is SUPPLEMENTED by production traces,
268
+ never declared full coverage. The release gate requires EXECUTED trials;
269
+ observables with nothing run against them are `specification-ready`, not
270
+ `release-ready`. A corrupted fixture, input or runner is a `TEST_ERROR`,
271
+ never a behaviour pass/fail; cases are isolated, so B's result never depends
272
+ on whether A ran.
260
273
 
261
274
  **Never author the suite up front** — the *corpus*, that is: the inputs. Every production
262
275
  failure and every thumbs-down becomes a fixture:
@@ -300,7 +313,11 @@ None of the above runs without these, and they are the part people skip:
300
313
  of `human` | `llm_judge` | `code_check`. A score with no source cannot be calibrated,
301
314
  audited, or trusted differently from its neighbours.
302
315
  - **Whole prompts, not just messages** — instructions, tool schemas and context as they
303
- were sent. A fixture cannot be replayed from a summary.
316
+ were sent. A fixture cannot be replayed from a summary. And a CANDIDATE's
317
+ version, output and score are their own records beside the old trace — a
318
+ regrade of the stored output is labelled regrade, never "the candidate
319
+ passed": only executing the candidate against the frozen fixture (a real,
320
+ stochastic call, costed in the receipt) can say the decision changed.
304
321
  - **State snapshots at turn boundaries**, so a thread test can assert what carried.
305
322
 
306
323
  ---
@@ -10,7 +10,7 @@ three places it will not carry what this skill requires.
10
10
  ## Contents
11
11
 
12
12
  - [Read this before encoding any of it](#read-this-before-encoding-any-of-it)
13
- - [Span names are formulas, and the operation list is closed](#span-names-are-formulas-and-the-operation-list-is-closed)
13
+ - [Span names are formulas, and the operation set is well-known but extensible](#span-names-are-formulas-and-the-operation-set-is-well-known-but-extensible)
14
14
  - [The evaluation event, missing the field §7 requires](#the-evaluation-event-missing-the-field-7-requires)
15
15
  - [Content: three tiers, and a hook that runs when nothing else does](#content-three-tiers-and-a-hook-that-runs-when-nothing-else-does)
16
16
  - [Tokens are eleven numbers and money is none of them](#tokens-are-eleven-numbers-and-money-is-none-of-them)
@@ -31,11 +31,14 @@ inference span the only attributes marked `Stable` are the ones borrowed from co
31
31
  attribute is `Development`.**
32
32
 
33
33
  So: adopt it, because a moving standard beats a private vocabulary that will never be read
34
- by anyone else's tooling — and **pin the version you adopted and expect to migrate**. Treat
34
+ by anyone else's tooling — and **pin the version you adopted and expect to migrate**. When
35
+ you encode any field from this file, record the **semconv schema revision and the commit SHA
36
+ you read it at, plus the observation date** beside your instrumentation — a `gen_ai.*` field
37
+ quoted with no revision is a field with no expiry, and this whole spec is `Development`. Treat
35
38
  any code that branches on a `gen_ai.*` attribute as code with an expiry date, and re-read
36
39
  the spec before quoting a field name from this file.
37
40
 
38
- ## Span names are formulas, and the operation list is closed
41
+ ## Span names are formulas, and the operation set is well-known but extensible
39
42
 
40
43
  Span names are computed, not free text:
41
44
 
@@ -48,10 +51,15 @@ Span names are computed, not free text:
48
51
  | agent creation | `create_agent {gen_ai.agent.name}` | — |
49
52
  | MCP | `{mcp.method.name} {target}`, target being the tool or prompt name | — |
50
53
 
51
- `gen_ai.operation.name` is a **closed 17-value enum** — `chat`, `text_completion`,
52
- `generate_content`, `embeddings`, `retrieval`, `fetch_response`, `execute_tool`,
53
- `create_agent`, `invoke_agent`, `plan` and the rest. A value outside it is not an extension,
54
- it is a name a backend cannot group by.
54
+ `gen_ai.operation.name` is a **well-known SET, not a closed enum** — the semconv
55
+ lists `chat`, `text_completion`, `generate_content`, `embeddings`, `retrieval`,
56
+ `fetch_response`, `execute_tool`, `create_agent`, `invoke_agent`, `plan` and the
57
+ rest, and a well-known value is preferred WHERE ONE FITS. But a provider
58
+ operation with no matching well-known value is allowed to carry a custom value:
59
+ it is not silently dropped, and an unknown value is stored RAW so a later
60
+ schema revision can recognise it. A backend groups the well-known values and
61
+ keeps the raw ones addressable — losing them is the failure, not carrying
62
+ them.
55
63
 
56
64
  **Only two attributes are Required on an inference span.** Everything else that matters —
57
65
  the model that actually answered, token counts, finish reasons — is Recommended or
@@ -127,17 +135,43 @@ leaks through the one field it never inspected. Decide naming and redaction toge
127
135
  standardises tokens and never money, so cost is always a join against a price table living
128
136
  outside the trace — and that join is where the number goes wrong.
129
137
 
130
- Because usage is not one number. It is eleven: `gen_ai.usage.input_tokens`, `output_tokens`,
131
- `reasoning.output_tokens`, `cache_read.input_tokens`, `cache_write.input_tokens`, and
132
- per-modality `text.*` / `image.*` / `audio.*` splits including
133
- `image.cache_read.input_tokens`.
134
-
135
- **A cost computed from `input_tokens + output_tokens` alone is wrong in both directions.** It
136
- bills cache reads at full price — they are the cheap ones — and it misses reasoning tokens and
137
- cache writes entirely. `../agent-orchestrator/references/kv-cache.md` is the other half of
138
+ Because usage is not one number, and the counters are of TWO kinds — TOTALS and
139
+ DISJOINT BILLING BUCKETS, and confusing them double-counts. `input_tokens` and
140
+ `output_tokens` are the totals; `reasoning.output_tokens` is a SUBSET of
141
+ `output_tokens` (not an addition to it), and `cache_read.input_tokens` /
142
+ `cache_write.input_tokens` are subsets of `input_tokens`. The per-modality
143
+ `text.*` / `image.*` / `audio.*` splits (including `image.cache_read.input_tokens`)
144
+ partition those same totals by modality — they are not extra tokens either.
145
+
146
+ **A cost computed from `input_tokens + output_tokens` alone is wrong** because it
147
+ prices the cached portion of the input at the full input rate — cache reads are
148
+ the cheap ones. The fix is NOT to add reasoning or modality counters back onto
149
+ the totals (that bills them twice): it is to **SUBTRACT the cached portion from
150
+ the total and apply the provider's cache-read (and cache-write) rate to it**,
151
+ pricing the remaining full-rate input and the output at their own rates. `../agent-orchestrator/references/kv-cache.md` is the other half of
138
152
  this: the cache read is the case worth getting right, because at scale it is most of the
139
153
  traffic.
140
154
 
155
+ **The worked receipt — a disjoint partition that reconciles.** Say `input_tokens
156
+ = 1000` with `cache_read.input_tokens = 800`, and `output_tokens = 200` with
157
+ `reasoning.output_tokens = 120`, at rates `$3 / $0.30 / $15` per 1k for
158
+ full-input / cache-read / output:
159
+
160
+ | Bucket | Tokens | Rate /1k | Cost |
161
+ |---|---|---|---|
162
+ | input, full-rate = `input − cache_read` | 200 | $3.00 | $0.60 |
163
+ | `cache_read` (a subset of input) | 800 | $0.30 | $0.24 |
164
+ | output (reasoning is a SUBSET, not added) | 200 | $15.00 | $3.00 |
165
+ | **total** | | | **$3.84** |
166
+
167
+ Two invariants a receipt MUST satisfy, and an independent example checks: the
168
+ priced token buckets **sum back to the totals** — 200 + 800 = 1000 input, and
169
+ output is 200 (reasoning's 120 is inside it, never a fourth line) — so no token
170
+ is charged twice; and the naive `input + output` at the input/output rates
171
+ ($3.00 + $3.00 = $6.00) OVER-charges by pricing the 800 cached tokens at $3
172
+ instead of $0.30. The receipt reconciles with the totals; the naive number does
173
+ not.
174
+
141
175
  And `gen_ai.client.token.usage` carries a hard **MUST NOT report** when the counts are not
142
176
  obtainable. A zero is a claim; absence is the honest value. That is the same rule
143
177
  `agent-harness/references/audit.md` states for cost attribution — *missing attribution beats
@@ -171,7 +205,14 @@ which:
171
205
  |---|---|---|---|
172
206
  | **Durable execution** (Temporal) | nothing — recorded results are replayed and only the failed step retries | free, deterministic | can I resume without redoing 20 web searches |
173
207
  | **Trace playground** (Phoenix) | the model call, against the live provider, with an edited prompt | a real call | would a different prompt have done better |
174
- | **Fixture replay** (this skill, §2 single-step) | an assertion over a stored run | free | did the decision at this point change |
175
-
176
- They are not interchangeable, and a runbook that says *"replay the run"* has not said what it
177
- means. Name the sense.
208
+ | **Regrade of a stored output** (this skill, §2 single-step) | nothing — a new assertion over the OLD output | free, deterministic | does the OLD output satisfy the changed rubric — **explicitly NOT a check of the candidate** |
209
+ | **Candidate execution over a frozen fixture** | the CANDIDATE (new prompt / model / tool schema) against frozen inputs | a real model call, stochastic — say so in the receipt | did the candidate's decision change |
210
+
211
+ They are not interchangeable, and a runbook that says *"replay the run"* has not said what
212
+ it means. Name the sense. The one this table exists to un-blur: **an old output does not
213
+ change when the candidate changes** — a regrade re-marks yesterday's homework under a new
214
+ rubric, and calling it "did the decision change" claims a candidate check that never ran.
215
+ The test with teeth is the fourth row: mutate the candidate to a knowingly wrong tool, and
216
+ the gate's result MUST change; a gate a candidate mutation cannot move is a regrade wearing
217
+ the wrong label. The candidate's version, its output and its score are stored as their own
218
+ records, never overwriting the old trace they are compared against.
@@ -38,9 +38,32 @@ The 95% band is roughly `±1.96 · SE`. Computed, not quoted:
38
38
 
39
39
  ```python
40
40
  import math
41
- def band(p, n): return 1.96 * math.sqrt(p * (1 - p) / n) * 100 # percentage points
41
+
42
+ def band(p, n):
43
+ """Wald approximation, percentage points. Valid only for moderate n with p
44
+ away from the boundary (rule of thumb: n*p >= 10 and n*(1-p) >= 10). At
45
+ p=0 or p=1 it returns ZERO width — which is exactly wrong: a run that has
46
+ never failed is not a run with no uncertainty."""
47
+ return 1.96 * math.sqrt(p * (1 - p) / n) * 100
48
+
49
+ def wilson(p, n, z=1.96):
50
+ """Wilson score interval — the DEFAULT for a proportion. Nonzero width at
51
+ the boundary, honest at small n; use exact (Clopper–Pearson) when n is
52
+ tiny and the decision is expensive. n == 0 is total uncertainty (0, 1),
53
+ never a zero-width claim."""
54
+ if n == 0:
55
+ return (0.0, 1.0)
56
+ denom = 1 + z * z / n
57
+ centre = (p + z * z / (2 * n)) / denom
58
+ half = z * math.sqrt(p * (1 - p) / n + z * z / (4 * n * n)) / denom
59
+ return (max(0.0, centre - half), min(1.0, centre + half))
42
60
  ```
43
61
 
62
+ The table above is Wald and inherits its limits; **a zero or tiny sample is not
63
+ a universal bound.** `wilson(0.0, 5)` spans up to ≈43% — five clean runs still
64
+ leave nearly half the range open — where Wald would print ±0.0 and read as
65
+ certainty.
66
+
44
67
  **So "the new one gets 73% where the old one got 70%, on a hundred cases" is not a
45
68
  result.** It is a number inside its own noise. The error shrinks as `1/√n`, which is the
46
69
  whole practical consequence: **the fix for a 2–3 pp expected gain is more tasks, not more
@@ -49,9 +72,12 @@ argument.** Quadrupling the set halves the band.
49
72
  A corollary worth stating because leaderboards invite the opposite: **differences below
50
73
  about 3 pp deserve scepticism until both configurations are documented and matched.**
51
74
 
52
- > The formula assumes independent cases. A benchmark whose tasks share a fixture, an
53
- > environment or a generator violates that, and the true band is wider than this. Wider,
54
- > never narrower so the table is a floor on your uncertainty, not a ceiling.
75
+ > The formula assumes independent (iid) cases. A benchmark whose tasks share a
76
+ > fixture, an environment or a generator violates that, and under the usual
77
+ > assumptionPOSITIVE intra-cluster correlation, which is what shared
78
+ > fixtures produce — the true band is wider than this, so treat the table as a
79
+ > floor. That is an assumption, not a theorem: engineered negative dependence
80
+ > can narrow a band, it just never happens by accident in a shared fixture.
55
81
 
56
82
  ## pass@k and pass^k are different questions
57
83
 
@@ -78,6 +104,13 @@ If a failed attempt leaves a charge, a message or a mutated row behind, `pass@k`
78
104
  available to you as a metric — you cannot pick the best of five refunds. Sample in a
79
105
  sandbox or a rollback-capable environment, and count **every** failure.
80
106
 
107
+ **Both are computed over TASK-LEVEL trials.** k repeated trials of one task
108
+ estimate that task's own p_i; the benchmark number is the mean over TASKS of
109
+ the per-task pass@k (or pass^k). Pooling repeated trials of one task into the
110
+ denominator as if each were a new task inflates n with copies of the same
111
+ difficulty — the trials are not independent tasks, and counting them as tasks
112
+ is how a small suite pretends to be a large one.
113
+
81
114
  **A report that gives k without saying which k it means is unreadable.** *k independent
82
115
  samples of one task* and *k consecutive tasks on one live pipeline* are different claims.
83
116
 
@@ -103,9 +136,12 @@ Two consequences, and they cut in opposite directions:
103
136
 
104
137
  - **You cannot compute `pass^k` from `pass^1`.** Exponentiating a headline rate gives a
105
138
  number far below the truth. Measure `pass^k` directly, at the k you care about.
106
- - **Anthropic's `0.75³ ≈ 42%` is a worst case, not a forecast.** It is the right shape for
107
- an argument *consistency is a much harder bar* and the wrong number to put in a
108
- release gate.
139
+ - **Anthropic's `0.75³ ≈ 42%` is the INDEPENDENCE BASELINE, not a bound.** Real
140
+ curves usually sit above it because successes cluster by task (positive
141
+ dependence), but that is an empirical pattern, not a guarantee — engineered
142
+ negative dependence can fall below it. The right shape for an argument —
143
+ *consistency is a much harder bar* — and the wrong number to put in a release
144
+ gate either way.
109
145
 
110
146
  The other half of independence is the harness, not the task: Anthropic requires each trial
111
147
  start from a clean environment, because *"unnecessary shared state between runs (leftover
@@ -115,8 +151,13 @@ metric mean anything.
115
151
 
116
152
  ## Pairing: same tasks, same seeds, per-task deltas
117
153
 
118
- **Never subtract two independent averages.** Run both configurations over the *same* task
119
- list with the *same* fixed seeds, record a per-task win/loss/tie, and test the deltas.
154
+ **Prefer pairing and never subtract two averages WITHOUT an interval.** Run
155
+ both configurations over the *same* task list with the *same* fixed seeds,
156
+ record a per-task win/loss/tie, and test the deltas: pairing cancels the
157
+ per-task difficulty variance and needs far fewer runs. An UNPAIRED comparison
158
+ of two independent averages is still legitimate when pairing is impossible —
159
+ it just pays for it with the wider two-sample band, and the sin is quoting the
160
+ subtraction bare, as if the band were zero.
120
161
 
121
162
  ```
122
163
  for task in tasks: # identical list
@@ -138,6 +179,34 @@ for task in tasks: # identical list
138
179
  **Ship on three conditions, not one:** the difference exceeds the noise band, it survives
139
180
  the paired analysis, and it reproduces on a rerun.
140
181
 
182
+ ## The design decides the method — and the receipt names both
183
+
184
+ Paired, clustered and unpaired are three DIFFERENT corpus structures, and a
185
+ test method borrowed from the wrong one produces confident nonsense. The
186
+ result's receipt names the design AND the method, and they must match:
187
+
188
+ | Corpus structure | Matching method | Mismatch that looks fine and is not |
189
+ |---|---|---|
190
+ | **paired** — same tasks, same seeds, per-task deltas | McNemar (binary) or a paired bootstrap over the DELTAS | running McNemar on two independent runs pairs rows that share nothing |
191
+ | **clustered** — k dependent repeats per task | a cluster bootstrap that resamples TASKS (each task carries its repeats along) | bootstrapping TRIALS treats dependent repeats as iid and shrinks the band by ~√k for free |
192
+ | **unpaired** — two independent samples | the two-sample (Welch) SE, wider band | quoting the paired-sized band for an unpaired design |
193
+
194
+ **Dependent repeats are never claimed iid**, and a small sample never buys
195
+ imaginary certainty — the Wilson bounds above are the floor either way.
196
+
197
+ ## Splits are spent once — case ids and groupings are FIXED
198
+
199
+ The corpus's case IDs and their groupings (which task belongs to which cluster,
200
+ which split) are frozen before any run and never regrouped to taste.
201
+
202
+ - **Validation** MAY be used for tuning — that is what it is for.
203
+ - **The final holdout is spent ONCE**, on the version already chosen. It is
204
+ never used to pick between versions; a holdout consulted per candidate is a
205
+ second validation set wearing a blindfold.
206
+ - **A reused validation example can never be relabelled an "unseen final
207
+ test"** — the receipt says which split every number came from, and "unseen"
208
+ is a property of the RUN HISTORY, not of the label somebody wrote today.
209
+
141
210
  ## The harness is a variable, so pin it
142
211
 
143
212
  The container spec is part of the measurement. On Terminal-Bench 2.0 the gap between the
@@ -33,13 +33,16 @@ forwards and backwards, which is why they live together here.
33
33
 
34
34
  ---
35
35
 
36
- ## Rule zero — most agent bugs are prompt bugs wearing a stack trace
36
+ ## Rule zero — check the prompt first (a diagnostic heuristic, with exceptions)
37
37
 
38
- The instinct when an agent misbehaves is to change the code. The measured reality, in every
39
- source this skill was built from, is that the largest behavioural changes come from the
40
- text: **"the biggest performance improvements often come from clearly explaining tool usage
41
- in the system prompt"**, and **"even small refinements to tool descriptions can yield
42
- dramatic improvements."**
38
+ The instinct when an agent misbehaves is to change the code. The sources this skill was
39
+ built from pull the other way: **"the biggest performance improvements often come from
40
+ clearly explaining tool usage in the system prompt"**, and **"even small refinements to
41
+ tool descriptions can yield dramatic improvements."** That is vendor guidance about where
42
+ leverage OFTEN lives, not a measured share of defects — so it orders the DIAGNOSIS, never
43
+ the verdict. **The exceptions are the findings a prompt cannot touch:** a deterministic
44
+ race, a hardcoded secret, a timeout wired to the wrong operation — source-level invariant
45
+ violations are code bugs, provable by reading, and no rewording treats them.
43
46
 
44
47
  Before adding a retry, a router, or a sub-agent, check in this order:
45
48
 
@@ -97,11 +100,15 @@ A **static** graph has every node and edge decided up front; a **dynamic** one g
97
100
  nodes read their own output and decide what comes next.
98
101
 
99
102
  **Static first, always** — go dynamic only after the static version hits a wall you can
100
- name, because dynamic is more powerful and much harder to control. And one row of that
101
- decision is hard rather than preferential: **a run that has to be auditable is static.**
102
- A dynamic graph's executed shape is not the shape anybody drew, so *"here is the design"*
103
- and *"here is what happened"* stop being the same document, and every claim about the run
104
- becomes unfalsifiable from outside.
103
+ name, because dynamic is more powerful and much harder to control. But **auditability is
104
+ NOT the same axis as static structure** that conflates the plan drawn beforehand with
105
+ the execution graph saved afterward. A run is auditable when its EXECUTION RECORD is
106
+ complete: every node, edge and event that actually ran, the policy version in force, and
107
+ deterministic bounds (budget / depth / node caps) with provenance. A static graph is the
108
+ PREFERENCE because its executed shape usually matches the drawn one; a dynamic graph is
109
+ auditable too when it keeps that record within those caps. What is never evidence is a
110
+ design DIAGRAM on its own — *"here is what I planned"* is not *"here is what happened"*,
111
+ in either mode.
105
112
 
106
113
  The six-row table, the rest of the model — the fake-edge test, the diamond, the checker
107
114
  node before a convergence — and what a host actually executes when it fans out are one
@@ -145,8 +152,12 @@ The long version is `references/audit.md`. The shape:
145
152
  Monday. This is the same rule `agent-evals` applies to eval rubrics and
146
153
  `seo-aeo-audit` to sites.
147
154
 
148
- **The finding that ends most audits early:** the system has no evals. Everything downstream
149
- is then unfalsifiableincluding this audit. Say so first, and make it the first item.
155
+ **The finding most audits surface first:** the system has no evals. That is a finding
156
+ about UNKNOWN RELIABILITYevery *behavioural estimate* downstream is unfalsifiable,
157
+ including this audit's. It does NOT dissolve what is provable at the source: a
158
+ demonstrable double charge, a hardcoded secret, a deterministic race keep their own
159
+ findings and their own priority, set by the concrete harm — a general "no evals" never
160
+ masks a specific proven harm.
150
161
 
151
162
  ---
152
163
 
@@ -189,7 +200,7 @@ prompt.
189
200
  ## Checklist — a harness worth shipping
190
201
 
191
202
  - [ ] Workflow-versus-agent decided deliberately, and the simpler option was actually tried
192
- - [ ] Static-versus-dynamic decided too, and a run that must be auditable is static
203
+ - [ ] Static-versus-dynamic decided too static preferred for predictability; a run that must be auditable keeps a complete execution record (not merely a static shape)
193
204
  - [ ] System prompt at the **right altitude** — heuristics, not hardcoded branches, not vague hope
194
205
  - [ ] Every status, category and enum the agent must produce is **enumerated in the prompt**
195
206
  - [ ] Today's date, and any other volatile context, injected rather than assumed
@@ -88,10 +88,21 @@ Walk them in order. Later tracks assume earlier ones.
88
88
  - Is tool output treated as **untrusted input**?
89
89
  - Can an audit row prove a control was applied — does it carry the **policy version**?
90
90
  - Is there a deterministic limit anywhere consequential, or only probabilistic content checks?
91
+ - **The lethal trifecta is a specific EXFILTRATION pattern, not a full threat
92
+ model.** Private data + untrusted content + external comms in one session is
93
+ the exfiltration triangle — but a session MISSING one leg is not thereby
94
+ "safe". Audit **capabilities and effects SEPARATELY**: untrusted content +
95
+ a write capability, with no private data at all, is an unrelated destructive
96
+ effect (injected content corrupts state or takes a damaging action) and is
97
+ its OWN finding. "Only two of the three, therefore a PASS" is the mistake —
98
+ removing a trifecta leg removes THAT exfiltration path, not every risk.
91
99
 
92
100
  ### 7 — Evidence
93
101
 
94
- - **Are there evals?** If not, this is finding number one and everything else is unfalsifiable.
102
+ - **Are there evals?** If not, that is finding number one about unknown reliability: every
103
+ *behavioural estimate* in this audit is then unfalsifiable. Findings proven at the source
104
+ (an invariant read off the code, a deterministic reproduction) stand on their own and are
105
+ prioritized by their concrete harm, not discounted under the general finding.
95
106
  - Do they judge the **trajectory**, or only the final answer?
96
107
  - Has any production failure become a permanent fixture?
97
108
  - Is a judge calibrated against human labels, or trusted because it is a judge?
@@ -110,6 +121,13 @@ Every finding carries one, and the tier is part of the finding:
110
121
  **Never present judgement as measured.** A finding whose tier is honest survives the meeting
111
122
  where it is challenged; one that is inflated loses the whole report.
112
123
 
124
+ Orthogonal to the tier, name the PROOF CLASS, because it decides what "no evals" does to
125
+ the finding: a **source-level invariant proof** (the race, the hardcoded secret, the
126
+ miswired timeout — read off the code) and a **deterministic reproduction** (a script that
127
+ shows the double charge every run) survive a system with no evals untouched; only a
128
+ **behavioural estimate** ("the agent usually recovers") needs an eval suite to be
129
+ falsifiable — and inherits the no-evals finding until one exists.
130
+
113
131
  ## Priority — four axes, and no scalar
114
132
 
115
133
  `P = blast × confidence / effort` used to sit here, and it contradicted the two sections
@@ -172,6 +190,10 @@ a finding whose tier is `judgement` says so there rather than being quietly disc
172
190
  - **Grading instead of planning.** A score ends the conversation the audit was meant to
173
191
  start — including a score assembled from honest axes. Publish the axes; do not multiply
174
192
  them.
175
- - **Confusing "no evals" with "not measured yet."** It is the root finding; put it first,
176
- because every other conclusion inherits it.
193
+ - **Confusing "no evals" with "not measured yet."** It is the root finding for every
194
+ behavioural estimate, which inherits it. A source-level proof or deterministic
195
+ reproduction does NOT inherit it — burying a demonstrable double charge under a general
196
+ "everything is unfalsifiable" is how the one finding with a victim gets deprioritized.
197
+ - **Treating a broken unit invariant with a prompt change.** Rule zero orders the
198
+ diagnosis; it does not convert a code bug into a wording bug.
177
199
  - **Reading a silent scanner as a clean system.** It is silent about what it can see.
@@ -124,7 +124,12 @@ the mistake cannot be made**, rather than documenting the mistake.
124
124
  - An `enum` instead of a free-text field with a list of valid values in the description.
125
125
  - One tool that does the two-step correctly instead of two tools that must be ordered.
126
126
  - A required `confirm: true` on a destructive action, so a partially-formed call fails
127
- closed.
127
+ closed. **But `confirm: true` is a SYNTAX GUARD, not user approval** — the MODEL can
128
+ set the boolean itself, so it proves only that the call is complete, never that a human
129
+ agreed. Real user approval is a **verifiable grant from a trusted control plane, bound to
130
+ the principal, action, exact arguments and an expiry** (or an already-existing user
131
+ authorization); a stale grant does NOT authorize changed arguments, and a client-supplied
132
+ boolean creates no authorization at all.
128
133
 
129
134
  ## Annotations, and the risk one tool cannot show you
130
135
 
@@ -154,8 +159,12 @@ Three capabilities that are individually ordinary and jointly an exfiltration pa
154
159
  2. exposure to **untrusted content**,
155
160
  3. the ability to **communicate externally**.
156
161
 
157
- Any two are safe. All three in one session mean untrusted content can instruct the agent to
158
- read private data and send it out, and no prompt-level instruction reliably prevents it.
162
+ All three in one session mean untrusted content can instruct the agent to read private data
163
+ and send it out, and no prompt-level instruction reliably prevents it. But the trifecta
164
+ names a SUFFICIENT configuration for one SPECIFIC risk — private-data EXFILTRATION — not a
165
+ complete security model: **"any two are safe" over-claims.** Untrusted content plus a write
166
+ capability, with no access to private data at all, still lets injected content corrupt state
167
+ or take a damaging action; drop any leg and you have removed THAT triangle, not every risk.
159
168
 
160
169
  **The reason it belongs here rather than in a permission check:** the trifecta is a property
161
170
  of *the tool set assembled in a session*, so **per-tool analysis cannot see it**. Every tool
@@ -71,10 +71,29 @@ tasks, while MCP is more about agents using capabilities."* Real systems run bot
71
71
  server whose internals speak MCP — and that is the recommended architecture, not a
72
72
  compromise.
73
73
 
74
- **The tell that you picked wrong:** if you find yourself inventing a task lifecycle, a
75
- progress channel and a resumable handle on top of `tools/call`, you wanted A2A. If you find
76
- yourself publishing an agent card for something that is one HTTP call with a JSON schema,
77
- you wanted MCP.
74
+ **The dispatch criterion is WHAT the other side is, not how long it runs.** MCP =
75
+ a CAPABILITY / tool you control the shape of; A2A = an AUTONOMOUS PEER whose
76
+ outcome you delegate and whose insides you cannot see. **Duration is a SECOND
77
+ question, and it is about a Tasks CAPABILITY, not a protocol.** A long-running
78
+ FIXED job you own — a ten-minute export, a batch transform — is MCP with the
79
+ **Tasks** extension (a durable handle: poll, supply input mid-flight, retrieve
80
+ later; see `references/mcp.md`), NOT A2A. So decide by the routing set, and
81
+ never by the word *long-running*:
82
+
83
+ - long-running FIXED export → **MCP Tasks, when the client/SDK negotiates that
84
+ extension**;
85
+ - autonomous outsourced negotiation → **A2A** (you delegate the outcome, not
86
+ the steps);
87
+ - **Tasks unsupported** by the reached client/SDK → an explicit fallback
88
+ (chunk the job, a job id the caller polls with a plain `tools/call`, or a
89
+ webhook) — reaching for A2A because Tasks is absent is picking a protocol to
90
+ dodge a missing extension.
91
+
92
+ Check the client's ACTUALLY-negotiated extensions before building on Tasks;
93
+ inventing a task lifecycle on top of `tools/call` when Tasks IS available is
94
+ re-implementing the extension, and reaching for A2A when Tasks is merely
95
+ unsupported is the mis-route this audit closes. Publishing an agent card for
96
+ something that is one HTTP call with a JSON schema is still the MCP direction.
78
97
 
79
98
  ---
80
99
 
@@ -13,11 +13,47 @@ repeat it.
13
13
 
14
14
  ## Contents
15
15
 
16
+ - Every example names its SDK — distribution, import, tested version, lifecycle
16
17
  - Mounting into an existing web app
17
18
  - Auth middleware and a health endpoint
18
19
  - Client configuration
19
20
  - Debugging a client that will not connect
20
21
 
22
+ ## Every example names its SDK — distribution, import, tested version, lifecycle
23
+
24
+ `FastMCP` exists in TWO distributions, and they are not the same package: the
25
+ official `mcp` SDK (`from mcp.server.fastmcp import FastMCP`) and the
26
+ standalone `fastmcp` package, whose 2.x renamed the server class and moved the
27
+ transport args out of the constructor. A snippet that names only the class
28
+ names neither. Every executable example in this file is written against **the
29
+ official `mcp` SDK, pinned**:
30
+
31
+ ```text
32
+ # requirements.txt — the pin IS the example's identity
33
+ mcp==1.12.3 # the version these snippets were verified against (2026-08-13);
34
+ starlette==0.47.* # re-pin only together with a re-run of the acceptance below
35
+ uvicorn==0.35.*
36
+ ```
37
+
38
+ ```python
39
+ from mcp.server.fastmcp import FastMCP # official SDK — NOT `from fastmcp import FastMCP`
40
+ ```
41
+
42
+ - **Current path (v2/current spec, pinned SDK above):** the snippets below,
43
+ as written.
44
+ - **v1 / standalone-`fastmcp` migration:** explicitly **out of scope** here.
45
+ Supporting it means its OWN fixture verified against its own pin — renaming
46
+ imports and hoping is how the constructor-args move ships a 404.
47
+ - **Lifecycle, in the verified order:** create the server → register tools →
48
+ build the ASGI app → register `/health` on the OUTER app → wrap with the
49
+ auth middleware → mount → serve. The health route registers BEFORE the auth
50
+ wrap, or the probe that says "up" needs a credential to say it.
51
+ - **Proof is a localhost protocol call, never a string in markdown.** The
52
+ acceptance boots the pinned example in a clean env and asserts:
53
+ `GET /health` → 200 · unauthenticated `/mcp` → 401 · then `initialize`,
54
+ `tools/list` and one `tools/call` succeed against `http://127.0.0.1`. A
55
+ snippet nobody booted is a hope with syntax highlighting.
56
+
21
57
  ## Mounting into an existing web app
22
58
 
23
59
  The common production shape: you already run a FastAPI/Starlette app, and the MCP server
@@ -219,7 +219,11 @@ Opt-in, negotiated, and worth checking before inventing an equivalent:
219
219
 
220
220
  - **Tasks** — a durable handle for long-running requests: poll for status, supply input
221
221
  mid-flight, retrieve the result later. This is the answer to "my tool takes ten minutes",
222
- and it exists so you do not hold a connection open or invent a job table.
222
+ and it exists so you do not hold a connection open or invent a job table. **Duration is a
223
+ TASKS-capability question, never a reason to switch to A2A**: a long-running FIXED job you
224
+ own is MCP-with-Tasks. But Tasks is an OPT-IN, NEGOTIATED extension — check the client/SDK
225
+ actually supports it before building on it; where it is unsupported, the fallback is a
226
+ chunked job or a caller-polled job id over plain `tools/call`, not a protocol change.
223
227
  - **MCP Apps** — interactive UI rendered inline in the conversation.
224
228
  - **Skills over MCP** — structured instruction sets discovered and consumed through MCP,
225
229
  which is how a server ships Agent Skills rather than only tools.
@@ -6,7 +6,7 @@ description: >-
6
6
  loops, pipelines with human checkpoints, provider routing with fallback/retry, memory
7
7
  architecture, retrieval and decay, context budgets, sub-agent coordination, error hierarchies; the
8
8
  work as a graph — parallel layers, fake edges, a checker before convergence; for resale:
9
- tiered wallets, one markup boundary, two-phase commit across database and provider API,
9
+ tiered wallets, one markup boundary, the saga across database and provider API,
10
10
  spend-delta polling, budget and loop guards, per-tenant keys. Triggers - "agent system",
11
11
  "orchestrator", "tool calling", "sub-agent", "LLM router", "fallback chain", "human in the
12
12
  loop", "memory layer", "LLM billing", "token wallet", "агентная система", "оркестратор",
@@ -282,10 +282,13 @@ comes from, and what this host actually executes are in
282
282
 
283
283
  Four rules, and these are the ones that change code:
284
284
 
285
- - **Label every edge with what crosses it. No payload, no edge.** Run the fake-edge test
286
- over any chain you inherited: write the steps as boxes, ask of each arrow whether data
287
- from A actually enters B, and delete the arrows that only encode the order somebody
288
- typed. Two or three per workflow is the normal yield.
285
+ - **Type every edge: data, control, authorization or resource.** Run the fake-edge
286
+ test over any chain you inherited and delete an arrow only when it carries NONE
287
+ of the four: no payload, no causal constraint, no permission, no shared resource.
288
+ backup→migration, approval→charge and lease→edit carry no bytes and are real;
289
+ the arrows that only encode the order somebody typed are the two or three per
290
+ workflow the test normally yields. Before a fan-out, compare read/write sets —
291
+ read-only branches parallelise, two writers of one thing were a resource edge.
289
292
  - **`depends_on` is a claim, so execute by layer.** §5's executor walked `plan.stages` in
290
293
  list order beside a model that declared its dependencies — which serialises a plan that
291
294
  went to the trouble of saying it need not be. Kahn the graph; a cycle fails the plan
@@ -294,9 +297,11 @@ Four rules, and these are the ones that change code:
294
297
  returns a hallucination, and the synthesis node cannot tell: it combines all three and
295
298
  answers confidently. The checker decides *usable / not usable* and nothing else, and
296
299
  the convergence depends on **the checker**, never directly on a branch.
297
- - **Static unless you can name what forces dynamic.** A graph that picks its own next
298
- nodes cannot be audited afterwards, because the shape that ran is not the shape anyone
299
- drew. Where a run has to be explainable, that settles it.
300
+ - **Static unless you can name what forces dynamic** for predictability, not
301
+ auditability. A run is auditable when it SAVES its execution record (nodes/edges/events
302
+ that ran, the policy version, deterministic budget/depth/node caps, provenance); a
303
+ dynamic graph that keeps that record is auditable too. A design diagram alone is never
304
+ evidence: the shape drawn is not the shape that ran.
300
305
 
301
306
  ---
302
307
 
@@ -120,24 +120,43 @@ already have and ask which of its waits are real.
120
120
 
121
121
  ## 3. The fake-edge test
122
122
 
123
- Five minutes, no tooling, and it is the highest-yield thing in this file.
123
+ Five minutes, no tooling, and it is the highest-yield thing in this file
124
+ and its question is **typed**, because "does data cross?" alone deletes real
125
+ constraints (AS-04). An edge is one of four kinds:
126
+
127
+ | Kind | What crosses | Example that MUST survive |
128
+ |---|---|---|
129
+ | **data** | A's output enters B | findings → draft |
130
+ | **control** | ordering only — a causal constraint with no bytes | backup → migration |
131
+ | **authorization** | a decision that permits B | approval → charge |
132
+ | **resource** | A and B touch one thing that tolerates one writer | two writes to one ledger |
124
133
 
125
134
  1. Write every step as a box.
126
135
  2. Draw an arrow between each pair of consecutive steps.
127
- 3. For each arrow ask: **does data from A actually enter B?** — not *"does B come after
128
- A"*.
129
- 4. Yes → keep it, and **write the payload on the arrow**.
130
- 5. Nodelete it. That wait was free to give away and you were paying for it.
131
- 6. Everything with no incoming arrow starts immediately.
136
+ 3. For each arrow ask, in order: does data from A enter B? does B's SAFETY
137
+ depend on A having finished (control)? does A PERMIT B (authorization)?
138
+ do A and B contend for one resource?
139
+ 4. Any yes keep it, **type it, and write the rationale on the arrow**
140
+ the payload for a data edge, the constraint for the other three.
141
+ 5. No to all four → delete it. That wait encoded the order somebody typed.
142
+ 6. Everything with no incoming arrow starts immediately — and **before any
143
+ fan-out, compare the branches' side-effect footprints and read/write
144
+ sets**: two read-only reviews genuinely parallelise; two writers of one
145
+ file were a resource edge nobody drew.
132
146
  7. Everything with no outgoing arrow is a final output.
133
147
 
134
- The tell that the test is being done honestly is step 4: if the payload cell is empty,
135
- the edge is fake, and the person drawing it now has to say so out loud rather than
136
- leaving the arrow in place because it looked orderly.
148
+ The tell that the test is being done honestly is step 4: an arrow with no
149
+ type and no rationale is fake, and the person drawing it now has to say so
150
+ out loud rather than leaving it in place because it looked orderly. The
151
+ inverse tell is step 5 done lazily: backup→migration, approval→charge and
152
+ lease→edit all carry NO payload, and deleting them for that is how a
153
+ migration runs against nothing — an empty payload cell justifies deletion
154
+ only when there is also no causal, permissive or resource constraint.
137
155
 
138
- **Expect two or three fake edges in any workflow you have not run this against.** The
139
- classic is *"review file A, then review file B"*: it reads as a sequence, and the review
140
- of B never once looks at what A returned.
156
+ **Expect two or three fake edges in any workflow you have not run this
157
+ against.** The classic is *"review file A, then review file B"*: it reads as
158
+ a sequence, the review of B never looks at what A returned, and both are
159
+ read-only — no data, no control, no authorization, no resource.
141
160
 
142
161
  ## 4. The diamond
143
162
 
@@ -263,13 +282,20 @@ grows: a node finishes, looks at what it found, and decides what should come nex
263
282
  | **static** | **always first** — switch only after the static version hits a wall you can name |
264
283
  | dynamic | the scope of the work depends on what is discovered along the way |
265
284
  | dynamic | a node must choose its successors from its own output |
266
- | **never dynamic** | **you will need to audit exactly what ran and why** |
267
-
268
- The last row is a hard rule in this pack, not a preference. A dynamic graph's executed
269
- shape is not the shape anybody drew, so *"here is the graph"* and *"here is what
270
- happened"* stop being the same document and every claim about the run becomes
271
- unfalsifiable from the outside. That is the same failure `agent-evals` names when a
272
- system has no durable trace.
285
+ | prefer static | **you will need to audit exactly what ran and why** — but see below |
286
+
287
+ The last row is a PREFERENCE, not a hard ban and the earlier draft got this wrong by
288
+ equating auditability with static structure, which conflates the plan drawn beforehand
289
+ with the execution graph saved afterward. **Auditability is a property of the RECORD, not
290
+ of the shape:** a run is auditable when its execution record is complete (every node,
291
+ edge and event that ran), the policy version is captured, and the run stayed inside
292
+ deterministic bounds — a **budget, a depth cap and a node cap**, each with provenance. A
293
+ static graph is preferred because its executed shape usually matches the drawn one and it
294
+ is predictable; a DYNAMIC graph under those caps, saving that record, passes the same
295
+ reconstruction audit — the SAME reconstruction that detects a deleted event or edge in
296
+ either mode. What never passes, static or dynamic, is a design DIAGRAM on its own:
297
+ *"here is the graph"* is not *"here is what happened"*. That is the same failure
298
+ `agent-evals` names when a system has no durable trace.
273
299
 
274
300
  **Most workflows that feel like they need a dynamic graph need a better static one.**
275
301
  Dynamic is more powerful and much harder to control; it is the second reach, never the
@@ -364,7 +390,12 @@ session:
364
390
  ## Workflow defaults
365
391
 
366
392
  - A node with no declared dependency starts immediately; do not serialise by habit.
367
- - Every declared dependency names the data it carries. No payload named ⇒ delete the edge.
393
+ - Every declared dependency carries a type (data/control/authorization/resource)
394
+ and a rationale. Delete an edge only when it has none of the four — an empty
395
+ payload alone never justifies deletion: backup→migration carries no bytes and
396
+ is real.
397
+ - Before a fan-out, compare the branches' side-effect footprints and read/write
398
+ sets; writers of one resource serialise, read-only branches run in parallel.
368
399
  - A checker sits between any parallel layer and the node that consumes it, and the
369
400
  consumer depends on the checker rather than on the layer.
370
401
  - A checker flags; it never silently passes an incomplete output.
@@ -1,7 +1,7 @@
1
1
  # Reselling LLM access — metering, wallets and guardrails
2
2
 
3
3
  **Load this when** the product resells LLM access: tiered wallets and the single
4
- boundary where markup applies, two-phase commit across a database and a provider API
4
+ boundary where markup applies, the saga across a database and a provider API
5
5
  with compensating transactions, advisory locking, optimistic concurrency for reclaims,
6
6
  spend-delta polling and its three cases, budget / loop / auto-pause guardrails,
7
7
  per-tenant key lifecycle and healing, the refund waterfall, and model-routing
@@ -18,7 +18,8 @@ the patterns hold for any upstream that issues per-tenant keys with limits.
18
18
  ## Contents
19
19
 
20
20
  - [The tiered wallet](#the-tiered-wallet)
21
- - [Two-phase commit across a DB and an external API](#two-phase-commit-across-a-db-and-an-external-api)
21
+ - [The saga across a DB and an external API](#the-saga-across-a-db-and-an-external-api)
22
+ - [Reconciling an unknown](#reconciling-an-unknown)
22
23
  - [Serializing concurrent transfers](#serializing-concurrent-transfers)
23
24
  - [Optimistic concurrency for reclaims](#optimistic-concurrency-for-reclaims)
24
25
  - [Discovering spend you do not control](#discovering-spend-you-do-not-control)
@@ -67,29 +68,76 @@ the constant changes.
67
68
 
68
69
  ---
69
70
 
70
- ## Two-phase commit across a DB and an external API
71
+ ## The saga across a DB and an external API
71
72
 
72
- You have a database you can roll back and an HTTP API you cannot. Order matters,
73
- and so does what you do when step 2 fails.
73
+ You have a database you can roll back and an HTTP API you cannot. That pair is
74
+ a **saga** local transactions stitched together by compensations — and it is
75
+ not two-phase commit: 2PC needs a coordinator both participants obey, and the
76
+ provider's API never agreed to prepare/commit. Naming it 2PC is how the next
77
+ defect ships, because 2PC has no *unknown* outcome, and an HTTP call to a
78
+ system you do not control has one all the time.
74
79
 
75
- **DB first, API second, compensate on failure:**
80
+ Every operation that touches the provider carries an **`operation_id`**, minted
81
+ inside the DB transaction, and a state that moves
82
+ `pending → applied | unknown | compensated`:
76
83
 
77
84
  1. Acquire the lock (below).
78
85
  2. Read fresh balances **inside** the transaction — not before it.
79
86
  3. Compute the transfer and apply the markup once.
80
- 4. Zero the source tier, increment the destination, write an audit row.
87
+ 4. Zero the source tier, increment the destination, write the intent row
88
+ `operation_id`, state `pending` — an outbox entry, not a log line.
81
89
  5. Commit.
82
- 6. Call the provider to raise the key limit.
83
- 7. **On API failure: a compensating transaction restores every DB value and
84
- writes a `compensation` audit row.**
90
+ 6. Call the provider to raise the key limit, idempotently where the API allows
91
+ (send the `operation_id` as the idempotency key).
92
+ 7. **On an outcome that proves the call did not apply** — a 4xx validation
93
+ refusal, a "no such key" — a compensating transaction restores every DB
94
+ value, writes a `compensation` audit row, and marks the operation
95
+ `compensated`.
96
+ 8. **On an AMBIGUOUS outcome — a timeout, a connection reset after send, a
97
+ 5xx — the operation is marked `unknown` and is NOT compensated.** The
98
+ provider may have applied the change: compensating on a guess restores a
99
+ ledger the key no longer matches, and the money drifts in the direction you
100
+ cannot see. `unknown` resolves only by **reconciliation** — read the
101
+ provider's actual state (the key's real limit), then mark `applied` or
102
+ compensate on evidence. Until it resolves, the operation blocks retries of
103
+ itself: a retry of an `unknown` is how one top-up applies twice.
85
104
 
86
105
  The alternative — API first, DB second — leaves money on the key that your
87
106
  ledger does not know about, and no amount of retrying finds it again. The
88
- compensating transaction is not optional politeness; it is the only thing that
89
- makes step 6 recoverable.
90
-
91
- Log both the intent and the compensation. An audit trail that records only
92
- successes cannot answer "where did the $35 go" six weeks later.
107
+ compensating transaction makes the *known* failure recoverable; the `unknown`
108
+ state is what keeps the ambiguous one honest.
109
+
110
+ Log the intent, the outcome and the compensation, keyed by `operation_id`. An
111
+ audit trail that records only successes cannot answer "where did the $35 go"
112
+ six weeks later — and one that cannot say "we do not know yet" answers it
113
+ wrongly.
114
+
115
+ ## Reconciling an unknown
116
+
117
+ Three rules, and every one exists because a late HTTP response is a message
118
+ from the past:
119
+
120
+ - **Ask by the operation's own idempotency key.** Reconciliation queries the
121
+ provider for what happened to THIS `operation_id` — never "read the limit
122
+ and guess whose change it reflects". Ambient state is the sum of every
123
+ operation that ever landed; only the key isolates yours.
124
+ - **The tenant's ledger carries a revision, and every resolve is a CAS.** A
125
+ reconcile or compensation writes only if the revision it read is still
126
+ current; a late or concurrent response that lost the race aborts and
127
+ re-reads, it never blind-writes. Without this, the response to operation A —
128
+ arriving after operation B moved the same tenant's ledger — "restores"
129
+ values B already superseded, and the compensation itself becomes the
130
+ corruption.
131
+ - **Compensate only your own confirmed operation.** A compensation names its
132
+ `operation_id`, reverses exactly that operation's delta, and runs only after
133
+ reconciliation confirmed THAT operation did not apply. A response for A is
134
+ never grounds to touch B's rows — however tempting the arithmetic looks.
135
+
136
+ **Repeated reconciliation is idempotent.** `unknown → applied` and
137
+ `unknown → compensated` are one-way edges: resolving an already-resolved
138
+ operation reads its state and stops — zero new writes, zero new audit rows. A
139
+ reconciler that runs twice (and it will: cron plus a manual "Sync now" is the
140
+ normal case, not the weird one) must find nothing left to do the second time.
93
141
 
94
142
  ---
95
143
 
@@ -141,16 +189,30 @@ discover spend by **polling a cumulative counter and taking the delta**:
141
189
  delta = currentUsage - lastRecordedUsage
142
190
  ```
143
191
 
144
- Three cases, and only the first is obvious:
145
-
146
- - `lastRecordedUsage == 0 && currentUsage > 0` → **seed the baseline, record
147
- nothing.** Recording it charges the tenant for everything spent before you
148
- started watching.
149
- - `currentUsage > lastRecordedUsage` record `delta`, then immediately enforce
150
- budgets (below).
151
- - `currentUsage < lastRecordedUsage` → the key was recreated. **Resync the
152
- baseline, record nothing.** A negative delta treated as spend credits money
153
- that was never returned.
192
+ **Zero is a value, not an absence.** The baseline row carries three fields
193
+ BESIDE the sum — `baseline_initialized`, `observed_at`, and
194
+ `provider_key_generation` (the key's id or created-at, whatever the provider
195
+ lets you read) because `lastRecordedUsage == 0` has two meanings that cost
196
+ money to conflate: "never watched" and "watched from zero". Testing the sum
197
+ for zero eats the first REAL spend of every key you watched from birth,
198
+ silently, as "seeding".
199
+
200
+ Four cases, decided by the flags, never by the sum:
201
+
202
+ - `!baseline_initialized` → **seed the baseline, record nothing**, set
203
+ `baseline_initialized`, stamp `observed_at` and the generation. Recording
204
+ here charges the tenant for everything spent before you started watching.
205
+ - initialized, `currentUsage > lastRecordedUsage` → record `delta` — including
206
+ the very first delta of a key whose baseline is a genuine 0 — then
207
+ immediately enforce budgets (below).
208
+ - initialized, `currentUsage < lastRecordedUsage`, **generation changed** →
209
+ the key really was recreated: resync the baseline to the new generation,
210
+ record nothing. The new key's next increase is recorded normally.
211
+ - initialized, `currentUsage < lastRecordedUsage`, **same generation** →
212
+ **ANOMALY.** Do not resync, do not record, do not guess "recreated" — a
213
+ counter that went backwards on the same key is the provider disagreeing
214
+ with your ledger, and reconciliation (above) owns it. A guessed resync here
215
+ quietly forgives the difference forever.
154
216
 
155
217
  Sync your stored limit from the provider's authoritative value on the same pass —
156
218
  under the lock, with a re-read, so the sync does not clobber a transfer that
@@ -235,10 +235,12 @@ The progression, and both ends are wrong:
235
235
  access control**.
236
236
 
237
237
  **What this pack already has, and what it is not.** `agent-sync` gives leases, race-free id
238
- reservation and a run journal: it decides *who may write this file right now*. That is
239
- coordination, and it is not shared memory it says nothing about what an agent should be
240
- allowed to *read*, or whose experiential memory is trustworthy enough to act on. An agent
241
- system that needs both needs both.
238
+ reservation and a run journal: it decides *who may write this file right now* precisely,
239
+ via a per-file **resource claim**, because a task lease authorizes the task and not the file
240
+ (SY-04), and its guarantee is a real cross-machine compare-and-swap only under the git lease
241
+ backend, advisory otherwise. That is coordination, and it is not shared memory — it says
242
+ nothing about what an agent should be allowed to *read*, or whose experiential memory is
243
+ trustworthy enough to act on. An agent system that needs both needs both.
242
244
 
243
245
  **The design rule:** make shared writes **attributed and scoped**. An entry carries who
244
246
  wrote it and under what role, and a reader may weigh it accordingly. Unattributed shared
@@ -18,7 +18,8 @@ long-term behaviour is a consequence of those frequencies, not of separate boxes
18
18
  - [3. Evolution — consolidation](#3-evolution--consolidation)
19
19
  - [4. Evolution — updating, and the stability–plasticity dilemma](#4-evolution--updating-and-the-stabilityplasticity-dilemma)
20
20
  - [5. Evolution — forgetting](#5-evolution--forgetting)
21
- - [6. What this pack already implements](#6-what-this-pack-already-implements)
21
+ - [6. The temporal evidence lifecycle](#6-the-temporal-evidence-lifecycle)
22
+ - [7. What this pack already implements](#7-what-this-pack-already-implements)
22
23
 
23
24
  ## 1. Formation — five ways to turn experience into an entry
24
25
 
@@ -134,7 +135,38 @@ constraint, many memory systems avoid directly deleting certain memories."*
134
135
  Deletion remains a **correctness and privacy** operation — a person asking to be forgotten
135
136
  is not a capacity decision, and `memory-architecture.md` §8 covers it.
136
137
 
137
- ## 6. What this pack already implements
138
+ ## 6. The temporal evidence lifecycle
139
+
140
+ One record's timeline, and the two events that are NOT on it:
141
+
142
+ ```
143
+ born ──▶ corroborated ──▶ superseded ──▶ (restored)
144
+ │ (independent (reversible: un-supersede puts it back;
145
+ │ provenance is_active=False, nothing was deleted
146
+ │ only) superseded_by)
147
+ └──▶ expired (volatile fact past its freshness window —
148
+ out of default retrieval WHATEVER its confidence,
149
+ verified included)
150
+ ```
151
+
152
+ - **Retrieval is not a lifecycle event.** Reading a record — or the agent
153
+ restating it in its own words — moves nothing: not confidence, not
154
+ freshness, not activity. A self-generated repeat scored as confirmation is
155
+ the compounding error `patterns.md` → Confidence Management refuses; only
156
+ evidence with independent provenance corroborates.
157
+ - **Supersession is an annotation, and it is reversible.** A new dated fact
158
+ sets `is_active=False` + `superseded_by` on the old one and KEEPS it: the
159
+ chain is walkable from either end, an explicit query still reaches the old
160
+ value, and restoring it (the correction turned out wrong) is clearing two
161
+ fields, not resurrecting a deleted row. The dated history is the audit
162
+ trail of what the system believed when.
163
+ - **`verified` is about confidence, never about time.** A volatile fact —
164
+ a quota, a price, a rate limit — carries a freshness window from its
165
+ `validity` field, and past that window it leaves default retrieval even at
166
+ confidence 1.0. Verification exempts a record from confidence DECAY;
167
+ nothing exempts a fact about the present from the present.
168
+
169
+ ## 7. What this pack already implements
138
170
 
139
171
  Stated so this file is read as an extension and not as a replacement:
140
172
 
@@ -146,9 +178,9 @@ Stated so this file is read as an extension and not as a replacement:
146
178
  | Forgetting, time-based | `patterns.md` → Confidence Management |
147
179
  | Global integration, cross-scope | `patterns.md` → Cross-Resource Learning Transfer |
148
180
  | **Frequency-based forgetting** | **nowhere — and the long-tail trap above is why that is a deliberate omission rather than a gap to close carelessly** |
149
- | **Temporal annotation instead of deletion** | **nowhere** Conflict Resolution currently resolves rather than annotates |
181
+ | **Temporal annotation instead of deletion** | `patterns.md` Conflict Resolution supersession sets `is_active=False` + `superseded_by` and the old record stays in history, reversible and reachable by explicit query |
150
182
  | **Dual-phase updating** | **nowhere** — the pack updates inline |
151
183
 
152
- The last three are named as absent rather than quietly added: each is a real change to a
184
+ The remaining two are named as absent rather than quietly added: each is a real change to a
153
185
  mechanism that is in production, and this file's job is to say what the options are, not to
154
186
  change `patterns.md` from a survey.
@@ -364,26 +364,36 @@ class LearningAnalyzer:
364
364
 
365
365
  ## Confidence Management
366
366
 
367
+ Confidence measures **corroboration, not match frequency**: it moves only on
368
+ evidence with independent provenance — another session, another agent, an
369
+ observed outcome. Retrieving a record, or the agent restating it in its own
370
+ words, bumps nothing: a self-generated repeat scored as confirmation is how an
371
+ early mistake compounds into a "high-confidence" one.
372
+
367
373
  ```
368
374
  LEARNING CONFIDENCE:
369
- Initial: 0.6
370
- Confirmed: +0.1 (cap 1.0)
371
- Applied: tracked (times_applied counter)
372
- Contradicted: -0.3
373
- Stale (30d): -0.02/month
374
- Deactivated: below 0.2
375
+ Initial: 0.6
376
+ Corroborated: +0.1 (cap 1.0) — independent provenance only;
377
+ retrieval and self-repetition move nothing
378
+ Applied: tracked (times_applied counter — a usage stat, not evidence)
379
+ Contradicted: handled by the contradiction gate below, never a bare -0.3
380
+ on a keyword match
381
+ Stale (30d): -0.02/month
382
+ Deactivated: below 0.2 — demoted from default retrieval, kept in history
375
383
 
376
384
  SESSION NOTE CONFIDENCE:
377
- Initial: 0.7
378
- Confirmed: +0.1 (cap 1.0)
379
- Verified: +0.15 (exempt from decay)
380
- Stale (60d): -0.1 per cycle
381
- Floor: 0.1
385
+ Initial: 0.7
386
+ Corroborated: +0.1 (cap 1.0), same provenance rule
387
+ Verified: +0.15 exempt from confidence DECAY, not from validity:
388
+ a volatile fact past its freshness window leaves default
389
+ retrieval whatever its confidence says
390
+ Stale (60d): -0.1 per cycle
391
+ Floor: 0.1
382
392
 
383
393
  INSIGHT CONFIDENCE:
384
394
  Initial: 0.5
385
395
  Resurfaced: +0.05
386
- Confirmed: +0.15
396
+ Corroborated: +0.15
387
397
  Dismissed: -0.2
388
398
  Stale (30d): -0.05 per cycle
389
399
  Expired: below 0.15
@@ -393,53 +403,83 @@ INSIGHT CONFIDENCE:
393
403
 
394
404
  ## Fuzzy Deduplication Pattern
395
405
 
396
- Used across all memory layers:
406
+ **Similarity proposes; the contradiction gate disposes.** Lexical similarity
407
+ is CANDIDATE RETRIEVAL only — it has no side effects. The measured
408
+ counterexample that fixed this rule: *"Always allow external sharing of
409
+ customer data"* and *"Never allow external sharing of customer data"* score
410
+ `SequenceMatcher` similarity **0.8791** — far above any threshold — and the
411
+ old on-match behaviour (bump confidence, keep the longer text, reactivate)
412
+ would have REINFORCED the stale instruction with the user's own correction,
413
+ and kept "Always" because it is one word longer.
397
414
 
398
415
  ```python
399
416
  from difflib import SequenceMatcher
400
417
 
401
418
  THRESHOLD = 0.75 # learnings/notes; 0.80 for insights
402
419
 
403
- async def find_similar(session, connection_id, category, subject, text):
420
+ async def find_candidates(session, connection_id, category, subject, text):
421
+ """Returns candidates for the contradiction gate. Nothing else happens
422
+ here: no confidence bump, no text replacement, no reactivation."""
404
423
  candidates = await load_existing(session, connection_id, category, subject)
405
424
  text_lower = text.strip().lower()
406
- best_match, best_ratio = None, 0.0
407
- for c in candidates:
408
- ratio = SequenceMatcher(None, c.text.strip().lower(), text_lower).ratio()
409
- if ratio >= THRESHOLD and ratio > best_ratio:
410
- best_match, best_ratio = c, ratio
411
- return best_match
412
-
413
- # On match: bump confidence +0.1, keep longer text, set is_active=True
414
- # On no match: create new entry
425
+ return [c for c in candidates
426
+ if SequenceMatcher(None, c.text.strip().lower(), text_lower).ratio()
427
+ >= THRESHOLD]
428
+
429
+ # Every candidate goes through the contradiction gate below.
430
+ # Only the gate's verdict decides merge / supersede / coexist / create.
415
431
  ```
416
432
 
417
433
  ---
418
434
 
419
435
  ## Conflict Resolution Pattern
420
436
 
421
- Detect when new learning contradicts existing ones:
437
+ A memory record carries five mandatory fields beside its text, and the gate
438
+ reads THEM — never the prose:
439
+
440
+ ```
441
+ MEMORY RECORD:
442
+ entity what the statement is about ("customer-data-sharing")
443
+ attribute which property of it ("external-sharing-policy")
444
+ scope where it applies ("project-A" | "global" | …)
445
+ provenance who/what asserted it, when (session, agent, outcome, user)
446
+ validity observed_at + volatile|stable (+ freshness window if volatile)
447
+ value the normalized position ("allow" | "deny" | "30s" | …)
448
+ ```
422
449
 
423
450
  ```python
424
- CONFLICT_INDICATORS = {"use", "prefer", "always", "never", "should",
425
- "instead", "not", "avoid", "correct", "wrong"}
426
-
427
- def resolve_conflicts(existing_learnings, new_lesson, new_confidence):
428
- new_keywords = {w for w in new_lesson.lower().split() if w in CONFLICT_INDICATORS}
429
- for old in existing_learnings:
430
- old_keywords = {w for w in old.lesson.lower().split() if w in CONFLICT_INDICATORS}
431
- shared = new_keywords & old_keywords
432
- if not shared: continue
433
-
434
- has_negation_flip = (
435
- ("not" in new_keywords) != ("not" in old_keywords) or
436
- ("never" in new_keywords) != ("never" in old_keywords) or
437
- ("avoid" in new_keywords) != ("avoid" in old_keywords))
438
-
439
- if has_negation_flip and old.confidence <= new_confidence:
440
- old.is_active = False # superseded
451
+ def contradiction_gate(old, new):
452
+ if (old.entity, old.attribute) != (new.entity, new.attribute):
453
+ return "unrelated" # similarity alone never merges anything
454
+ if not scopes_overlap(old.scope, new.scope):
455
+ return "coexist" # a correction wins only in its own scope
456
+ if values_compatible(old.value, new.value):
457
+ return "corroborates" # +confidence iff provenance is independent
458
+ return "contradicts"
459
+
460
+ def apply_verdict(verdict, old, new):
461
+ if verdict == "contradicts":
462
+ # Temporal supersession, reversible: the old record STAYS in history.
463
+ old.is_active = False
464
+ old.superseded_by = new.id # never deleted, never bumped
465
+ return create(new) # starts at its own initial confidence
466
+ if verdict == "corroborates" and independent(old.provenance, new.provenance):
467
+ old.confidence = min(1.0, old.confidence + 0.1)
468
+ old.provenance.append(new.provenance)
469
+ return old
470
+ if verdict in ("coexist", "unrelated"):
471
+ return create(new) # both live; different scope or subject
472
+ return old # self-repetition: no change at all
441
473
  ```
442
474
 
475
+ Keyword heuristics (negation flips, `always`/`never` pairs) may FLAG a pair
476
+ for the gate; they never decide it. The second measured counterexample is
477
+ why: *"Use Python"* and *"Never use production credentials"* share `use` and
478
+ a negation flip, and the old keyword rule could supersede one with the other
479
+ — two statements about different entities entirely. A number or unit change
480
+ ("timeout is 30s" → "timeout is 60s") is a contradiction the negation
481
+ heuristic cannot see and the value comparison catches.
482
+
443
483
  ---
444
484
 
445
485
  ## Cross-Resource Learning Transfer