@miller-tech/uap 2.16.6 → 2.19.5

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 (105) hide show
  1. package/README.md +1 -1
  2. package/config/capacity-policy.json +33 -0
  3. package/config/model-profiles/qwen38.json +32 -33
  4. package/dist/.tsbuildinfo +1 -1
  5. package/dist/bin/cli.js +57 -0
  6. package/dist/bin/cli.js.map +1 -1
  7. package/dist/capacity/policy.d.ts +32 -1
  8. package/dist/capacity/policy.d.ts.map +1 -1
  9. package/dist/capacity/policy.js +123 -4
  10. package/dist/capacity/policy.js.map +1 -1
  11. package/dist/capacity/probe.d.ts +26 -0
  12. package/dist/capacity/probe.d.ts.map +1 -1
  13. package/dist/capacity/probe.js +84 -3
  14. package/dist/capacity/probe.js.map +1 -1
  15. package/dist/cli/inference.d.ts +4 -0
  16. package/dist/cli/inference.d.ts.map +1 -1
  17. package/dist/cli/inference.js +74 -8
  18. package/dist/cli/inference.js.map +1 -1
  19. package/dist/cli/merge-gate.d.ts +59 -0
  20. package/dist/cli/merge-gate.d.ts.map +1 -0
  21. package/dist/cli/merge-gate.js +238 -0
  22. package/dist/cli/merge-gate.js.map +1 -0
  23. package/dist/cli/merge-queue.d.ts +13 -0
  24. package/dist/cli/merge-queue.d.ts.map +1 -1
  25. package/dist/cli/merge-queue.js +48 -2
  26. package/dist/cli/merge-queue.js.map +1 -1
  27. package/dist/cli/mission.d.ts +25 -0
  28. package/dist/cli/mission.d.ts.map +1 -0
  29. package/dist/cli/mission.js +225 -0
  30. package/dist/cli/mission.js.map +1 -0
  31. package/dist/cli/orch.d.ts +27 -0
  32. package/dist/cli/orch.d.ts.map +1 -0
  33. package/dist/cli/orch.js +70 -0
  34. package/dist/cli/orch.js.map +1 -0
  35. package/dist/cli/systemd-services.d.ts.map +1 -1
  36. package/dist/cli/systemd-services.js +44 -0
  37. package/dist/cli/systemd-services.js.map +1 -1
  38. package/dist/delivery/merge-gate.d.ts +168 -0
  39. package/dist/delivery/merge-gate.d.ts.map +1 -0
  40. package/dist/delivery/merge-gate.js +309 -0
  41. package/dist/delivery/merge-gate.js.map +1 -0
  42. package/dist/inference/analysis.d.ts +69 -3
  43. package/dist/inference/analysis.d.ts.map +1 -1
  44. package/dist/inference/analysis.js +123 -7
  45. package/dist/inference/analysis.js.map +1 -1
  46. package/dist/inference/probe.d.ts +38 -1
  47. package/dist/inference/probe.d.ts.map +1 -1
  48. package/dist/inference/probe.js +303 -30
  49. package/dist/inference/probe.js.map +1 -1
  50. package/dist/inference/strata.d.ts +95 -0
  51. package/dist/inference/strata.d.ts.map +1 -0
  52. package/dist/inference/strata.js +167 -0
  53. package/dist/inference/strata.js.map +1 -0
  54. package/dist/mission/database.d.ts +29 -0
  55. package/dist/mission/database.d.ts.map +1 -0
  56. package/dist/mission/database.js +131 -0
  57. package/dist/mission/database.js.map +1 -0
  58. package/dist/mission/ledger.d.ts +109 -0
  59. package/dist/mission/ledger.d.ts.map +1 -0
  60. package/dist/mission/ledger.js +294 -0
  61. package/dist/mission/ledger.js.map +1 -0
  62. package/dist/mission/poll-cycle.d.ts +73 -0
  63. package/dist/mission/poll-cycle.d.ts.map +1 -0
  64. package/dist/mission/poll-cycle.js +100 -0
  65. package/dist/mission/poll-cycle.js.map +1 -0
  66. package/dist/mission/poll-executor.d.ts +91 -0
  67. package/dist/mission/poll-executor.d.ts.map +1 -0
  68. package/dist/mission/poll-executor.js +328 -0
  69. package/dist/mission/poll-executor.js.map +1 -0
  70. package/dist/models/types.d.ts.map +1 -1
  71. package/dist/models/types.js +33 -27
  72. package/dist/models/types.js.map +1 -1
  73. package/dist/self-tuning/profiles/qwen38.d.ts +13 -14
  74. package/dist/self-tuning/profiles/qwen38.d.ts.map +1 -1
  75. package/dist/self-tuning/profiles/qwen38.js +17 -18
  76. package/dist/self-tuning/profiles/qwen38.js.map +1 -1
  77. package/dist/types/mission.d.ts +80 -0
  78. package/dist/types/mission.d.ts.map +1 -0
  79. package/dist/types/mission.js +12 -0
  80. package/dist/types/mission.js.map +1 -0
  81. package/docs/architecture/adr/0003-mission-orchestrator.md +99 -0
  82. package/docs/architecture/adr/0004-deterministic-merge-gate.md +127 -0
  83. package/docs/guides/CAPACITY_POLICY.md +49 -0
  84. package/docs/guides/INFERENCE_HEALTH.md +65 -8
  85. package/docs/guides/MISSIONS.md +170 -0
  86. package/docs/guides/PROXY.md +2 -0
  87. package/docs/reports/2026-10-02-plane-admin-fail-closed.md +70 -0
  88. package/docs/reports/2026-10-02-plane-admin-protect-session.md +116 -0
  89. package/docs/visual/merge-gate.html +404 -0
  90. package/docs/visual/mission-orchestrator.html +472 -0
  91. package/package.json +2 -2
  92. package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
  93. package/src/policies/enforcers/__pycache__/expert_review_required.cpython-312.pyc +0 -0
  94. package/src/policies/enforcers/__pycache__/quality_metrics_gate.cpython-312.pyc +0 -0
  95. package/src/policies/enforcers/plane_admin_protect.py +133 -0
  96. package/src/policies/schemas/policies/plane-admin-protect.md +55 -0
  97. package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
  98. package/templates/hooks/uap-policy-gate.sh +33 -0
  99. package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
  100. package/tools/agents/scripts/anthropic_proxy.py +522 -45
  101. package/tools/agents/tests/test_anthropic_proxy_streaming.py +171 -0
  102. package/tools/agents/tests/test_gate_failclosed_plane_admin.py +248 -0
  103. package/tools/agents/tests/test_plane_admin_protect.py +124 -0
  104. package/tools/agents/tests/test_repeat_call_guard.py +64 -0
  105. package/tools/agents/tests/test_stream_heartbeat.py +77 -0
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Stub markers in ADDED code. Applied to non-test, non-docs files: test
3
+ * suites legitimately stub seams (the house style injects stubs everywhere),
4
+ * and docs/ADRs legitimately quote the markers they describe. A `todo!()`
5
+ * left in production code is exactly the half-delivery this check exists to
6
+ * catch.
7
+ */
8
+ export const STUB_PATTERNS = [
9
+ { label: 'todo macro', re: /todo!\s*\(/ },
10
+ { label: 'NotImplementedError', re: /\bNotImplementedError\b/ },
11
+ { label: 'unimplemented', re: /\bunimplemented\b|\bnot implemented\b/i },
12
+ // Bounded gap (was `.*`): an adversarial multi-megabyte single line made
13
+ // this pattern quadratic; 120 chars is far more than any real
14
+ // placeholder sentence needs (security review P2).
15
+ { label: 'placeholder implementation', re: /\bplaceholder\b.{0,120}\b(implementation|for now)\b|\bstub(?:bed)? (?:body|implementation|for now)\b/i },
16
+ { label: 'TODO marker', re: /\bTODO\b/ },
17
+ { label: 'FIXME marker', re: /\bFIXME\b/ },
18
+ ];
19
+ /** Apology / escape phrasing — the "sorry delta". Scanned in added lines of
20
+ * every changed file and in the run's declared summaries. */
21
+ export const SORRY_PATTERNS = [
22
+ { label: 'apology', re: /\bsorry\b|\bi apolog(i|y)/i },
23
+ { label: 'gave up', re: /\bi couldn'?t\b|\bi was unable\b|\bdidn'?t manage to\b|\bnot able to (?:complete|finish|fulfill)\b/i },
24
+ { label: 'refusal', re: /\bcannot (?:complete|fulfill)\b|\bunable to (?:complete|fulfill|finish)\b/i },
25
+ ];
26
+ /**
27
+ * Paths that ARE the gate system. A diff touching any of these without
28
+ * `allowGateInfra` fails axiom-honesty — including the merge gate, its
29
+ * evidence gatherer, and the ledger functions the provenance and
30
+ * green-recompute checks consume: a PR may not weaken (or rewrite) the checks
31
+ * that judge it.
32
+ *
33
+ * Deliberately hard-coded in source, not a config file: an externalized list
34
+ * could be edited by the very PR it judges, and any edit to THIS list trips
35
+ * axiom-honesty against the gate itself (the list protects itself by being
36
+ * the gate — architect review F6).
37
+ */
38
+ export const GATE_INFRA_PREFIXES = [
39
+ '.codex/hooks/',
40
+ '.github/workflows/',
41
+ 'src/policies/enforcers/',
42
+ 'src/delivery/merge-gate.ts',
43
+ 'src/delivery/gate-evidence.ts',
44
+ 'src/cli/merge-gate.ts',
45
+ 'src/cli/merge-queue.ts',
46
+ 'src/mission/ledger.ts',
47
+ 'src/mission/database.ts',
48
+ 'scripts/version-bump.sh',
49
+ '.uap/quality-baseline.json',
50
+ 'config/capacity-policy.json',
51
+ ];
52
+ /** Test paths are exempt from stub-marker scanning (seams are stubbed by
53
+ * design in this codebase); docs paths are exempt because they quote the
54
+ * markers they describe. */
55
+ function isTestPath(path) {
56
+ return /(^|\/)test\//.test(path) || /\.(test|spec)\.[cm]?[jt]s$/.test(path);
57
+ }
58
+ function isDocsPath(path) {
59
+ return /\.(md|html|txt)$/.test(path);
60
+ }
61
+ /** Adversarial long lines (minified blobs, lockfiles) make multi-token
62
+ * patterns expensive; cap per-line scanning (security review P2). */
63
+ const SCAN_LINE_MAX = 8192;
64
+ /** First matching pattern label, or null. Long lines are skipped — a real
65
+ * stub marker sits on a human-scale line. */
66
+ function firstMatch(text, patterns) {
67
+ for (const line of text.split('\n')) {
68
+ if (line.length > SCAN_LINE_MAX)
69
+ continue;
70
+ for (const p of patterns) {
71
+ if (p.re.test(line))
72
+ return p.label;
73
+ }
74
+ }
75
+ return null;
76
+ }
77
+ /**
78
+ * PURE decision core. Every check runs and reports (a full finding list, not
79
+ * first-fail) so an operator sees every reason at once; `pass` is the AND of
80
+ * all findings.
81
+ */
82
+ export function evaluateMergeGate(inputs) {
83
+ const findings = [];
84
+ // ── 1. provenance / statement immutability ──
85
+ const markerOk = inputs.ledgerIntact && inputs.markerHash8 !== undefined && inputs.markerHash8 === inputs.acceptanceHash8;
86
+ findings.push({
87
+ check: 'provenance',
88
+ ok: markerOk,
89
+ detail: !inputs.ledgerIntact
90
+ ? 'ledger tampered: stored acceptance text no longer hashes to the stored hash'
91
+ : inputs.markerHash8 === undefined
92
+ ? `no [mission:#${inputs.missionId}:hash8] marker ties this work to frozen acceptance`
93
+ : inputs.markerHash8 === inputs.acceptanceHash8
94
+ ? `marker hash ${inputs.markerHash8} matches frozen acceptance`
95
+ : `marker hash ${inputs.markerHash8} is superseded (current: ${inputs.acceptanceHash8}) — the work was built under replanned-away criteria`,
96
+ });
97
+ // ── 2. green recompute ──
98
+ let greenOk = true;
99
+ const greenDetails = [];
100
+ // Tier 1 — the evidence artifact deliver records on success. Production
101
+ // delivered runs persist WITHOUT a checkpoint (deliver clears it), so the
102
+ // artifact is the seam that survives a real delivery; a present-but-invalid
103
+ // artifact (red gate, hatch, sha drift) is a hard failure.
104
+ if (inputs.evidence !== null) {
105
+ if (!inputs.evidence.present) {
106
+ greenDetails.push(`no gate-evidence artifact for ${inputs.evidence.expectedSha.slice(0, 12)}… (.uap/evidence/)`);
107
+ }
108
+ else if (!inputs.evidence.shaMatches) {
109
+ greenOk = false;
110
+ greenDetails.push('evidence artifact is bound to a different candidate sha');
111
+ }
112
+ else if (!inputs.evidence.hatchesEmpty) {
113
+ greenOk = false;
114
+ greenDetails.push('evidence artifact records gate-affecting environment hatches');
115
+ }
116
+ else if (!inputs.evidence.gatesAllZero) {
117
+ greenOk = false;
118
+ greenDetails.push('evidence artifact records a non-zero gate exit');
119
+ }
120
+ }
121
+ // Tier 2 — run-state history (present on interrupted/resumed shapes and
122
+ // hand-crafted fixtures; absent on the production delivered shape).
123
+ if (inputs.runState === null) {
124
+ greenOk = false;
125
+ greenDetails.push('no linked run (or run-state unreadable)');
126
+ }
127
+ else {
128
+ if (inputs.runState.status !== 'delivered') {
129
+ greenOk = false;
130
+ greenDetails.push(`run state is '${sanitize(inputs.runState.status)}', not 'delivered'`);
131
+ }
132
+ if (inputs.pidCorroborated === false) {
133
+ greenOk = false;
134
+ greenDetails.push('run pid does not match the pid the orchestrator spawned');
135
+ }
136
+ const history = inputs.runState.history ?? [];
137
+ if (history.length > 0) {
138
+ const last = history[history.length - 1];
139
+ if (!last.passed) {
140
+ greenOk = false;
141
+ greenDetails.push(`last recorded iteration failed its gates`);
142
+ }
143
+ else if (last.acceptanceMet !== undefined && last.acceptanceMet < 1) {
144
+ greenOk = false;
145
+ greenDetails.push(`final acceptance judge met only ${Math.round(last.acceptanceMet * 100)}% of criteria`);
146
+ }
147
+ }
148
+ else if (inputs.evidence !== null && inputs.evidence.present) {
149
+ // artifact-backed: status + artifact is sufficient.
150
+ }
151
+ else if (inputs.evidence === null) {
152
+ // caller did not check the artifact; history is the only evidence.
153
+ }
154
+ else {
155
+ // No artifact AND no history: the "delivered" status string alone is
156
+ // testimony, not evidence — refuse it (code review P1).
157
+ greenOk = false;
158
+ greenDetails.push('no recorded evidence beyond the delivered status string (deliver clears checkpoints on success; expected .uap/evidence/<sha>.json)');
159
+ }
160
+ }
161
+ if (inputs.openSalvageCount > 0) {
162
+ greenOk = false;
163
+ greenDetails.push(`${inputs.openSalvageCount} salvage proposal(s) await a ruling (approval-gated — landing would bypass the ruling)`);
164
+ }
165
+ const artifactNote = inputs.evidence !== null && inputs.evidence.present && inputs.evidence.gatesAllZero && inputs.evidence.hatchesEmpty && inputs.evidence.shaMatches
166
+ ? ' + evidence artifact green'
167
+ : '';
168
+ findings.push({
169
+ check: 'green-recompute',
170
+ ok: greenOk,
171
+ detail: greenOk
172
+ ? `fresh run-state: delivered${artifactNote}, no open salvage`
173
+ : greenDetails.join('; '),
174
+ });
175
+ // ── 3. sorry-delta ──
176
+ const stubHits = [];
177
+ const sorryHits = [];
178
+ for (const f of inputs.changedFiles) {
179
+ if (f.addedText.length === 0)
180
+ continue;
181
+ if (!isTestPath(f.path) && !isDocsPath(f.path)) {
182
+ const stub = firstMatch(f.addedText, STUB_PATTERNS);
183
+ if (stub)
184
+ stubHits.push(`${f.path} (${stub})`);
185
+ }
186
+ const sorry = firstMatch(f.addedText, SORRY_PATTERNS);
187
+ if (sorry)
188
+ sorryHits.push(`${f.path} (${sorry})`);
189
+ }
190
+ const summaries = (inputs.runState?.phaseSummaries ?? []).join('\n');
191
+ if (summaries.length > 0) {
192
+ const sorry = firstMatch(summaries, SORRY_PATTERNS);
193
+ if (sorry)
194
+ sorryHits.push(`run summaries (${sorry})`);
195
+ }
196
+ const sorryOk = stubHits.length === 0 && sorryHits.length === 0;
197
+ findings.push({
198
+ check: 'sorry-delta',
199
+ ok: sorryOk,
200
+ detail: sorryOk
201
+ ? `no stub or apology markers in ${inputs.changedFiles.length} changed file(s)`
202
+ : `stub markers: ${stubHits.length ? stubHits.join(', ') : 'none'}; apology markers: ${sorryHits.length ? sorryHits.join(', ') : 'none'}`,
203
+ });
204
+ // ── 4. axiom-honesty (gate-infra tampering) ──
205
+ const infraHits = inputs.changedFiles
206
+ .map((f) => f.path)
207
+ .filter((p) => GATE_INFRA_PREFIXES.some((prefix) => p === prefix || p.startsWith(prefix)));
208
+ const infraOk = infraHits.length === 0 || inputs.allowGateInfra === true;
209
+ findings.push({
210
+ check: 'axiom-honesty',
211
+ ok: infraOk,
212
+ detail: infraHits.length === 0
213
+ ? 'diff does not touch gate infrastructure'
214
+ : infraOk
215
+ ? `gate-infra changes explicitly acknowledged: ${infraHits.join(', ')}`
216
+ : `diff touches gate infrastructure without acknowledgment (${infraHits.join(', ')}) — pass --allow-gate-infra (or label the PR gate-infra) only if this is deliberate`,
217
+ });
218
+ return { missionId: inputs.missionId, pass: findings.every((f) => f.ok), findings };
219
+ }
220
+ /**
221
+ * Strip terminal escapes before echoing untrusted strings (run-state status
222
+ * comes from an agent-writable state.json — CWE-117, security review P3).
223
+ */
224
+ function sanitize(text) {
225
+ // eslint-disable-next-line no-control-regex
226
+ return String(text).replace(/[\u001b\u009b]/g, '');
227
+ }
228
+ /**
229
+ * Extract the mission provenance marker from free text (a PR title or body).
230
+ * Prefix-pinned like the poll-executor's parser: the bracketed form must match
231
+ * exactly, so argv injection and partial collisions cannot forge linkage.
232
+ */
233
+ export function missionMarkerOf(text) {
234
+ const m = /\[mission:#(\d+):([0-9a-f]{8})\]/.exec(text ?? '');
235
+ return m ? { missionId: Number(m[1]), hash8: m[2] } : null;
236
+ }
237
+ /**
238
+ * Parse a unified diff into changed files with their ADDED lines. Accepts the
239
+ * output of `git diff --unified=0 <base>...HEAD` and `gh pr diff`. Pure — the
240
+ * command execution lives in the callers, tests exercise only this parser.
241
+ *
242
+ * Path coverage is the security property: DELETED files (`+++ /dev/null`),
243
+ * RENAMED files (no `+++` line at all), and BINARY files (no headers) must all
244
+ * reach the axiom-honesty prefix match — `git rm .codex/hooks/x.sh` is the
245
+ * purest form of weakening a gate, so the deleted path is captured from the
246
+ * `diff --git` / `--- a/` lines (security review P1, architect F1).
247
+ *
248
+ * An ADDED content line that itself starts with `+++` is not misread as a
249
+ * header: `+++` only opens a file when the previous line was its `---` pair.
250
+ */
251
+ export function parseUnifiedDiff(diff) {
252
+ const byPath = new Map();
253
+ const fileFor = (path) => {
254
+ let f = byPath.get(path);
255
+ if (!f) {
256
+ f = { path, addedText: '' };
257
+ byPath.set(path, f);
258
+ }
259
+ return f;
260
+ };
261
+ let current = null;
262
+ let inHunk = false; // true after a file's first @@ line
263
+ let prevWasHeaderPair = false; // previous line was a `--- ` file header
264
+ for (const line of diff.split('\n')) {
265
+ if (line.startsWith('diff --git ')) {
266
+ // `diff --git a/<path> b/<path>` — authoritative for renames (no +++
267
+ // line follows) and binaries. Register both sides; the b-side becomes
268
+ // the add target if a +++ header never appears.
269
+ const m = /^diff --git a\/(.+?) b\/(.+)$/.exec(line);
270
+ if (m) {
271
+ fileFor(m[1]); // old path (deletions, rename source)
272
+ current = fileFor(m[2]);
273
+ }
274
+ inHunk = false;
275
+ prevWasHeaderPair = false;
276
+ continue;
277
+ }
278
+ if (!inHunk && line.startsWith('--- ')) {
279
+ const old = line.slice(4);
280
+ // `--- a/<path>`: register the path (deletion evidence) but do not
281
+ // retarget added lines — only the `+++` header does that.
282
+ if (old.startsWith('a/') && old !== 'a/')
283
+ fileFor(old.slice(2));
284
+ prevWasHeaderPair = true;
285
+ continue;
286
+ }
287
+ if (!inHunk && line.startsWith('+++ ') && prevWasHeaderPair) {
288
+ const target = line.slice(4);
289
+ current =
290
+ target === '/dev/null'
291
+ ? null // pure deletion: added lines cannot follow
292
+ : fileFor(target.startsWith('b/') ? target.slice(2) : target);
293
+ prevWasHeaderPair = false;
294
+ continue;
295
+ }
296
+ prevWasHeaderPair = false;
297
+ if (line.startsWith('@@')) {
298
+ inHunk = true;
299
+ continue;
300
+ }
301
+ if (line.startsWith('+') && current) {
302
+ // Inside a hunk every `+`-leading line is content — including one that
303
+ // merely LOOKS like a header (`++ b/x` renders as `+++ b/x`).
304
+ current.addedText += line.slice(1) + '\n';
305
+ }
306
+ }
307
+ return [...byPath.values()];
308
+ }
309
+ //# sourceMappingURL=merge-gate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge-gate.js","sourceRoot":"","sources":["../../src/delivery/merge-gate.ts"],"names":[],"mappings":"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"}
@@ -32,6 +32,29 @@ export interface PrefillSample {
32
32
  /** Reported throughput for that prefill. */
33
33
  tokensPerSecond: number;
34
34
  }
35
+ /** The shape `analyzeTrend` needs: a throughput reading with the workload
36
+ * quantity that determines how comparable it is (prompt size for prefill,
37
+ * prompt DEPTH for decode). Structural supertype of PrefillSample, so
38
+ * existing callers pass unchanged; strata decode samples reuse it with
39
+ * `tokens` = depth, which is what makes decode buckets compare like-with-like. */
40
+ export interface TrendSample {
41
+ at: number;
42
+ /** The quantity whose size determines the bucket — NOT always "tokens
43
+ * transferred" (see the decode case above). */
44
+ tokens: number;
45
+ tokensPerSecond: number;
46
+ }
47
+ /** A turn's prefix-cache reuse accounting. Used by the strata backend, which
48
+ * reports reuse as a FACT (tokens served from cache), not a counterfactual.
49
+ * `at` is the strata sample clock: line index for log samples, epoch ms for
50
+ * ring samples — comparable within one source, never across sources. */
51
+ export interface ReuseSample {
52
+ at: number;
53
+ /** Prompt tokens the client sent. */
54
+ incoming: number;
55
+ /** Tokens served from the cache. */
56
+ reused: number;
57
+ }
35
58
  /** A turn's cache/checkpoint divergence accounting. */
36
59
  export interface CheckpointSample {
37
60
  /** Prompt tokens the client sent. */
@@ -44,7 +67,7 @@ export interface CheckpointSample {
44
67
  export interface InferenceSnapshot {
45
68
  /** Seconds the server process has been running, when known. */
46
69
  uptimeSeconds?: number;
47
- /** Parallel rails (`-np`). */
70
+ /** Parallel rails (`-np`), or /slots length on strata. */
48
71
  rails?: number;
49
72
  /** Shared KV pool size in cells (`-c`). Under --kv-unified this is TOTAL. */
50
73
  poolCells?: number;
@@ -61,6 +84,20 @@ export interface InferenceSnapshot {
61
84
  generationTimeouts?: number;
62
85
  /** Distinct server PIDs seen in the window. >1 means it spans a restart. */
63
86
  processCount?: number;
87
+ /**
88
+ * Strata-backend fields. All optional so the llama.cpp path constructs the
89
+ * snapshot exactly as before.
90
+ */
91
+ /** Which backend produced this snapshot — the report says so out loud. */
92
+ backend?: 'llamacpp' | 'strata';
93
+ /** Model id the backend advertises (strata /metrics engine.model). */
94
+ engineModel?: string;
95
+ /** Decode throughput per request, bucketed by prompt DEPTH. */
96
+ decode?: TrendSample[];
97
+ /** Per-request prefix-cache reuse (strata reports reuse as a fact). */
98
+ reuse?: ReuseSample[];
99
+ /** Backend-reported free VRAM in MiB (strata engine.vram_free_mib). */
100
+ vramFreeMiB?: number;
64
101
  }
65
102
  export interface Thresholds {
66
103
  /** Ratio of recent to early prefill below which we call it degraded. */
@@ -74,6 +111,15 @@ export interface Thresholds {
74
111
  kvFloorSlack: number;
75
112
  /** Any timeout in the window is at least a WARN. */
76
113
  timeoutsWarn: number;
114
+ /** Mean prefix-cache reuse fraction below which the cache is failing. */
115
+ reuseWarn: number;
116
+ /** Backend-reported free VRAM below which we warn (OOM territory). */
117
+ vramHeadroomWarnMiB: number;
118
+ /** How many recent reuse samples the cache-miss check looks at. The cache
119
+ * question is "is it tracking NOW", not "did every conversation since the
120
+ * process start reuse well" — a 23h log legitimately contains dozens of
121
+ * cold-start turns. */
122
+ reuseTailSamples: number;
77
123
  }
78
124
  export declare const DEFAULT_THRESHOLDS: Thresholds;
79
125
  /** Prompt-size buckets. Prefill tok/s is strongly size-dependent, so an
@@ -102,14 +148,18 @@ export interface TrendResult {
102
148
  note?: string;
103
149
  }
104
150
  /**
105
- * Compare early-life against recent prefill throughput WITHIN a size bucket.
151
+ * Compare early-life against recent throughput WITHIN a size bucket.
106
152
  *
107
153
  * Splitting the samples in half and averaging each side would be wrong: if
108
154
  * the workload's prompt sizes drift (and they do — conversations grow), the
109
155
  * mean moves for reasons that have nothing to do with the server. So pick the
110
156
  * bucket that has enough samples on BOTH sides and compare only within it.
157
+ *
158
+ * Generic over TrendSample: prefill samples carry prompt size in `tokens`,
159
+ * strata decode samples carry prompt DEPTH — the quantity that determines
160
+ * comparability is whatever the caller puts there.
111
161
  */
112
- export declare function analyzeTrend(samples: PrefillSample[], thresholds?: Thresholds): TrendResult;
162
+ export declare function analyzeTrend(samples: TrendSample[], thresholds?: Thresholds): TrendResult;
113
163
  export interface CheckpointResult {
114
164
  /** Mean fraction of reusable tokens actually recovered. */
115
165
  recovery?: number;
@@ -119,6 +169,17 @@ export interface CheckpointResult {
119
169
  }
120
170
  /** How much of the reusable prefix the checkpoints actually deliver. */
121
171
  export declare function analyzeCheckpoints(samples: CheckpointSample[]): CheckpointResult;
172
+ export interface ReuseResult {
173
+ /** Mean fraction of the sent prompt served from cache. */
174
+ fraction?: number;
175
+ /** Mean tokens re-read per turn that a healthy cache would have supplied. */
176
+ wastedTokens?: number;
177
+ samples: number;
178
+ }
179
+ /** Mean prefix-cache reuse over the strata samples. Strata reports reuse as a
180
+ * fact, so there is no reusable-vs-restored split — the health question is
181
+ * simply "is the cache tracking the conversation". */
182
+ export declare function analyzeReuse(samples: ReuseSample[]): ReuseResult;
122
183
  export interface Finding {
123
184
  health: Exclude<InferenceHealth, 'UNKNOWN'>;
124
185
  /** Stable id so monitors can match on it without parsing prose. */
@@ -132,6 +193,11 @@ export interface InferenceReport {
132
193
  findings: Finding[];
133
194
  trend: TrendResult;
134
195
  checkpoints: CheckpointResult;
196
+ /** Strata-backend additions; undefined for the llama.cpp path so existing
197
+ * monitors keep their parsed shape working. */
198
+ backend?: 'llamacpp' | 'strata';
199
+ decodeTrend?: TrendResult;
200
+ reuse?: ReuseResult;
135
201
  }
136
202
  /** Roll a snapshot up into a verdict. Pure; no I/O, no clock. */
137
203
  export declare function assessInference(snap: InferenceSnapshot, thresholds?: Thresholds): InferenceReport;
@@ -1 +1 @@
1
- {"version":3,"file":"analysis.d.ts","sourceRoot":"","sources":["../../src/inference/analysis.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"analysis.d.ts","sourceRoot":"","sources":["../../src/inference/analysis.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,MAAM,GAAG,KAAK,GAAG,SAAS,CAAC;AAEnE,2EAA2E;AAC3E,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,EAAE,EAAE,MAAM,CAAC;IACX,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,eAAe,EAAE,MAAM,CAAC;CACzB;AAED;;;;kFAIkF;AAClF,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX;mDAC+C;IAC/C,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;CACzB;AAED;;;wEAGwE;AACxE,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,qCAAqC;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,oCAAoC;IACpC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,uDAAuD;AACvD,MAAM,WAAW,gBAAgB;IAC/B,qCAAqC;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAC;IACjB,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0DAA0D;IAC1D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2DAA2D;IAC3D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oEAAoE;IACpE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,mDAAmD;IACnD,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,2CAA2C;IAC3C,WAAW,EAAE,gBAAgB,EAAE,CAAC;IAChC,wDAAwD;IACxD,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,4EAA4E;IAC5E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,0EAA0E;IAC1E,OAAO,CAAC,EAAE,UAAU,GAAG,QAAQ,CAAC;IAChC,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+DAA+D;IAC/D,MAAM,CAAC,EAAE,WAAW,EAAE,CAAC;IACvB,uEAAuE;IACvE,KAAK,CAAC,EAAE,WAAW,EAAE,CAAC;IACtB,uEAAuE;IACvE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,wEAAwE;IACxE,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,+DAA+D;IAC/D,gBAAgB,EAAE,MAAM,CAAC;IACzB,kEAAkE;IAClE,sBAAsB,EAAE,MAAM,CAAC;IAC/B,qDAAqD;IACrD,YAAY,EAAE,MAAM,CAAC;IACrB,oDAAoD;IACpD,YAAY,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,SAAS,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,mBAAmB,EAAE,MAAM,CAAC;IAC5B;;;2BAGuB;IACvB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,eAAO,MAAM,kBAAkB,EAAE,UA2BhC,CAAC;AAEF;6DAC6D;AAC7D,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAKjD;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,2DAA2D;IAC3D,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,kDAAkD;IAClD,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB,4DAA4D;IAC5D,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,WAAW,EAAE,EACtB,UAAU,GAAE,UAA+B,GAC1C,WAAW,CAmEb;AAED,MAAM,WAAW,gBAAgB;IAC/B,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wEAAwE;AACxE,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,gBAAgB,EAAE,GAAG,gBAAgB,CAUhF;AAED,MAAM,WAAW,WAAW;IAC1B,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6EAA6E;IAC7E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;sDAEsD;AACtD,wBAAgB,YAAY,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,WAAW,CAQhE;AAED,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE,OAAO,CAAC,eAAe,EAAE,SAAS,CAAC,CAAC;IAC5C,mEAAmE;IACnE,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,2BAA2B;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,eAAe,CAAC;IACxB,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,KAAK,EAAE,WAAW,CAAC;IACnB,WAAW,EAAE,gBAAgB,CAAC;IAC9B;mDAC+C;IAC/C,OAAO,CAAC,EAAE,UAAU,GAAG,QAAQ,CAAC;IAChC,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,KAAK,CAAC,EAAE,WAAW,CAAC;CACrB;AAYD,iEAAiE;AACjE,wBAAgB,eAAe,CAC7B,IAAI,EAAE,iBAAiB,EACvB,UAAU,GAAE,UAA+B,GAC1C,eAAe,CA+MjB"}
@@ -33,6 +33,22 @@ export const DEFAULT_THRESHOLDS = {
33
33
  checkpointRecoveryWarn: 0.5,
34
34
  kvFloorSlack: 0.15,
35
35
  timeoutsWarn: 1,
36
+ // Strata's own healthy conversations reuse 90%+ of the prefix
37
+ // (observed 2026-10-04: 38270/38659 = 99%). Under half means most turns
38
+ // re-read the whole prompt — the compaction/checkpoint starvation analog.
39
+ reuseWarn: 0.5,
40
+ // The strata reserve-aware floor. NOTE this is NOT the capacity policy's
41
+ // gpuMinFreeMiB (200 for the strata entry): that one is HOST-wide free
42
+ // VRAM from nvidia-smi; this one is strata's own /metrics reading, which
43
+ // already respects its --vram-reserve-mib 1200. Both floors being 600 in
44
+ // the first draft was a coincidence, not a coupling — tune them
45
+ // independently.
46
+ vramHeadroomWarnMiB: 600,
47
+ // Observed live 2026-10-04: a whole-process mean (39% over 4400 requests)
48
+ // fired a WARN that was really dozens of legitimate cold-start turns from
49
+ // OTHER sessions; the active session's recent turns reuse 95-99%. The tail
50
+ // is the signal.
51
+ reuseTailSamples: 50,
36
52
  };
37
53
  /** Prompt-size buckets. Prefill tok/s is strongly size-dependent, so an
38
54
  * average across mixed sizes is not comparable over time. */
@@ -46,16 +62,20 @@ export function sizeBucket(tokens) {
46
62
  return '>30k';
47
63
  }
48
64
  /**
49
- * Compare early-life against recent prefill throughput WITHIN a size bucket.
65
+ * Compare early-life against recent throughput WITHIN a size bucket.
50
66
  *
51
67
  * Splitting the samples in half and averaging each side would be wrong: if
52
68
  * the workload's prompt sizes drift (and they do — conversations grow), the
53
69
  * mean moves for reasons that have nothing to do with the server. So pick the
54
70
  * bucket that has enough samples on BOTH sides and compare only within it.
71
+ *
72
+ * Generic over TrendSample: prefill samples carry prompt size in `tokens`,
73
+ * strata decode samples carry prompt DEPTH — the quantity that determines
74
+ * comparability is whatever the caller puts there.
55
75
  */
56
76
  export function analyzeTrend(samples, thresholds = DEFAULT_THRESHOLDS) {
57
77
  if (samples.length < thresholds.minSamplesPerEra * 2) {
58
- return { earlyCount: 0, recentCount: 0, note: 'not enough prefill samples for a trend' };
78
+ return { earlyCount: 0, recentCount: 0, note: 'not enough samples for a trend' };
59
79
  }
60
80
  // Bucket FIRST, then split each bucket at its OWN median time.
61
81
  //
@@ -133,6 +153,19 @@ export function analyzeCheckpoints(samples) {
133
153
  samples: usable.length,
134
154
  };
135
155
  }
156
+ /** Mean prefix-cache reuse over the strata samples. Strata reports reuse as a
157
+ * fact, so there is no reusable-vs-restored split — the health question is
158
+ * simply "is the cache tracking the conversation". */
159
+ export function analyzeReuse(samples) {
160
+ const real = samples.filter((s) => s.incoming > 0);
161
+ if (real.length === 0)
162
+ return { samples: 0 };
163
+ return {
164
+ fraction: real.reduce((a, s) => a + s.reused / s.incoming, 0) / real.length,
165
+ wastedTokens: real.reduce((a, s) => a + Math.max(0, s.incoming - s.reused), 0) / real.length,
166
+ samples: real.length,
167
+ };
168
+ }
136
169
  /** Name the other slowed buckets, so a single headline number does not read
137
170
  * as though only one prompt size is affected. */
138
171
  function alsoAffected(trend) {
@@ -226,18 +259,101 @@ export function assessInference(snap, thresholds = DEFAULT_THRESHOLDS) {
226
259
  `(${Math.floor(snap.poolCells / snap.rails).toLocaleString()} each if evenly divided)`,
227
260
  });
228
261
  }
229
- const worst = findings.reduce((acc, f) => (rank[f.health] > rank[acc] ? f.health : acc), findings.length > 0 ? 'GREEN' : 'UNKNOWN');
262
+ // 7. Decode decay (strata): the same bucketed early/recent comparison over
263
+ // decode samples, with depth as the bucketing quantity. This is the
264
+ // signal llama.cpp journals cannot give — they never log decode rate.
265
+ // "Sampled window" rather than "process's life": the sample set may be
266
+ // the whole serve log (a process lifetime) or the bounded /metrics
267
+ // ring (a dozen recent requests), and the message must not claim more
268
+ // coverage than the samples carry.
269
+ const decodeTrend = snap.decode !== undefined ? analyzeTrend(snap.decode, thresholds) : undefined;
270
+ if (decodeTrend?.ratio !== undefined) {
271
+ const pct = Math.round((1 - decodeTrend.ratio) * 100);
272
+ if (decodeTrend.ratio < thresholds.degradeRatioRed) {
273
+ findings.push({
274
+ health: 'RED',
275
+ code: 'decode-decay',
276
+ message: `decode in the ${decodeTrend.bucket}-depth bucket fell ${pct}% across the sampled window ` +
277
+ `(${Math.round(decodeTrend.earlyMean)} -> ${Math.round(decodeTrend.recentMean)} tok/s)` +
278
+ alsoAffected(decodeTrend),
279
+ remedy: 'restart the strata serve layer; if it returns within days, check the expert cache and KV residency',
280
+ });
281
+ }
282
+ else if (decodeTrend.ratio < thresholds.degradeRatioWarn) {
283
+ findings.push({
284
+ health: 'WARN',
285
+ code: 'decode-decay',
286
+ message: `decode in the ${decodeTrend.bucket}-depth bucket is down ${pct}% ` +
287
+ `(${Math.round(decodeTrend.earlyMean)} -> ${Math.round(decodeTrend.recentMean)} tok/s)` +
288
+ alsoAffected(decodeTrend),
289
+ remedy: 'watch it; a restart restores throughput if the decay continues',
290
+ });
291
+ }
292
+ }
293
+ // 8. Prefix-cache failure (strata): recent turns re-reading most of the
294
+ // prompt. Only the TAIL counts: the question is whether the cache is
295
+ // tracking the conversation NOW, and a whole-process mean mixes in
296
+ // every legitimate cold start since the process began — measured on the
297
+ // live 2026-10-04 log, that mean (39%) fired on a stack whose active
298
+ // session was reusing 99%. A single 0% turn right after a client
299
+ // compaction must not fire this on its own, hence the mean over the tail.
300
+ // Samples are SORTED by `at` first: the log source is oldest-first, but
301
+ // the /metrics ring is NEWEST-first, and slicing an unsorted array takes
302
+ // the first N by position — the OLDEST half of a ring, inverting exactly
303
+ // the current-vs-cold-start signal the tail exists for.
304
+ const reuse = snap.reuse !== undefined
305
+ ? analyzeReuse([...snap.reuse]
306
+ .sort((a, b) => a.at - b.at)
307
+ .slice(Math.max(0, snap.reuse.length - thresholds.reuseTailSamples)))
308
+ : undefined;
309
+ if (reuse?.fraction !== undefined && reuse.fraction < thresholds.reuseWarn) {
310
+ findings.push({
311
+ health: reuse.wastedTokens > 20_000 ? 'RED' : 'WARN',
312
+ code: 'cache-miss',
313
+ message: `the prefix cache is re-reading ${Math.round((1 - reuse.fraction) * 100)}% of prompts ` +
314
+ `(~${Math.round(reuse.wastedTokens).toLocaleString()} tokens re-read per turn)`,
315
+ remedy: 'a fresh or just-compacted conversation legitimately re-reads; sustained misses mean the ' +
316
+ 'conversation cache is not tracking — check conversation_cache slots and restart if it persists',
317
+ });
318
+ }
319
+ // 9. VRAM headroom (strata's own reserve-aware reading). The capacity
320
+ // doctor checks the HOST-wide number; this one catches another process
321
+ // eating the card from under strata.
322
+ if (snap.vramFreeMiB !== undefined && snap.vramFreeMiB < thresholds.vramHeadroomWarnMiB) {
323
+ findings.push({
324
+ health: 'WARN',
325
+ code: 'vram-headroom',
326
+ message: `strata reports only ${snap.vramFreeMiB} MiB free (reserve-aware) — below the ` +
327
+ `${thresholds.vramHeadroomWarnMiB} MiB headroom floor`,
328
+ remedy: 'something is eating the card (another model process, a vision request); free it or lower --max-context',
329
+ });
330
+ }
331
+ const worst = findings.reduce((acc, f) => (rank[f.health] > rank[acc] ? f.health : acc), 'GREEN');
230
332
  // No evidence at all is UNKNOWN, never a fabricated GREEN — the same
231
333
  // fail-open posture the capacity doctor uses.
232
334
  //
233
- // But "no evidence" is about the SAMPLED signals (trend, checkpoints, KV).
335
+ // But "no evidence" is about the SAMPLED signals (trend, checkpoints, KV,
336
+ // decode, reuse, VRAM). When they ARE present and nothing crossed a
337
+ // threshold, GREEN is the honest verdict: the 2026-10-03 draft left a
338
+ // healthy single-rail stack at UNKNOWN (GREEN previously required a
339
+ // finding to exist, and only multi-rail llama.cpp emitted one — the
340
+ // shared-pool info), which made a working strata backend indistinguishable
341
+ // from a blind probe for every monitor parsing --json.
342
+ //
234
343
  // A finding that fired is itself evidence, so an actionable one must never
235
344
  // be masked by an UNKNOWN rollup: generation timeouts are counted from the
236
345
  // proxy journal and can fire with none of the sampled signals present,
237
346
  // which let a RED finding pass --strict with exit 0.
238
347
  const actionable = findings.some((f) => f.health !== 'GREEN');
239
- const noSampledEvidence = trend.ratio === undefined && checkpoints.samples === 0 && snap.kvBitsPerValue === undefined;
240
- const health = actionable || !noSampledEvidence ? worst : 'UNKNOWN';
241
- return { health, findings, trend, checkpoints };
348
+ const noSampledEvidence = trend.ratio === undefined &&
349
+ checkpoints.samples === 0 &&
350
+ snap.kvBitsPerValue === undefined &&
351
+ (snap.decode?.length ?? 0) === 0 &&
352
+ (snap.reuse?.length ?? 0) === 0 &&
353
+ snap.vramFreeMiB === undefined;
354
+ const health = noSampledEvidence && !actionable ? 'UNKNOWN' : worst;
355
+ return snap.backend !== undefined
356
+ ? { health, findings, trend, checkpoints, backend: snap.backend, decodeTrend, reuse }
357
+ : { health, findings, trend, checkpoints };
242
358
  }
243
359
  //# sourceMappingURL=analysis.js.map