@miller-tech/uap 2.16.7 → 2.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/config/capacity-policy.json +33 -0
  2. package/config/model-profiles/qwen38.json +32 -33
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/bin/cli.js +57 -0
  5. package/dist/bin/cli.js.map +1 -1
  6. package/dist/capacity/policy.d.ts +32 -1
  7. package/dist/capacity/policy.d.ts.map +1 -1
  8. package/dist/capacity/policy.js +123 -4
  9. package/dist/capacity/policy.js.map +1 -1
  10. package/dist/capacity/probe.d.ts +26 -0
  11. package/dist/capacity/probe.d.ts.map +1 -1
  12. package/dist/capacity/probe.js +84 -3
  13. package/dist/capacity/probe.js.map +1 -1
  14. package/dist/cli/inference.d.ts +4 -0
  15. package/dist/cli/inference.d.ts.map +1 -1
  16. package/dist/cli/inference.js +74 -8
  17. package/dist/cli/inference.js.map +1 -1
  18. package/dist/cli/merge-gate.d.ts +59 -0
  19. package/dist/cli/merge-gate.d.ts.map +1 -0
  20. package/dist/cli/merge-gate.js +238 -0
  21. package/dist/cli/merge-gate.js.map +1 -0
  22. package/dist/cli/merge-queue.d.ts +13 -0
  23. package/dist/cli/merge-queue.d.ts.map +1 -1
  24. package/dist/cli/merge-queue.js +48 -2
  25. package/dist/cli/merge-queue.js.map +1 -1
  26. package/dist/cli/mission.d.ts +25 -0
  27. package/dist/cli/mission.d.ts.map +1 -0
  28. package/dist/cli/mission.js +225 -0
  29. package/dist/cli/mission.js.map +1 -0
  30. package/dist/cli/orch.d.ts +27 -0
  31. package/dist/cli/orch.d.ts.map +1 -0
  32. package/dist/cli/orch.js +70 -0
  33. package/dist/cli/orch.js.map +1 -0
  34. package/dist/cli/systemd-services.d.ts.map +1 -1
  35. package/dist/cli/systemd-services.js +44 -0
  36. package/dist/cli/systemd-services.js.map +1 -1
  37. package/dist/delivery/merge-gate.d.ts +168 -0
  38. package/dist/delivery/merge-gate.d.ts.map +1 -0
  39. package/dist/delivery/merge-gate.js +309 -0
  40. package/dist/delivery/merge-gate.js.map +1 -0
  41. package/dist/inference/analysis.d.ts +69 -3
  42. package/dist/inference/analysis.d.ts.map +1 -1
  43. package/dist/inference/analysis.js +123 -7
  44. package/dist/inference/analysis.js.map +1 -1
  45. package/dist/inference/probe.d.ts +38 -1
  46. package/dist/inference/probe.d.ts.map +1 -1
  47. package/dist/inference/probe.js +303 -30
  48. package/dist/inference/probe.js.map +1 -1
  49. package/dist/inference/strata.d.ts +95 -0
  50. package/dist/inference/strata.d.ts.map +1 -0
  51. package/dist/inference/strata.js +167 -0
  52. package/dist/inference/strata.js.map +1 -0
  53. package/dist/mission/database.d.ts +29 -0
  54. package/dist/mission/database.d.ts.map +1 -0
  55. package/dist/mission/database.js +131 -0
  56. package/dist/mission/database.js.map +1 -0
  57. package/dist/mission/ledger.d.ts +109 -0
  58. package/dist/mission/ledger.d.ts.map +1 -0
  59. package/dist/mission/ledger.js +294 -0
  60. package/dist/mission/ledger.js.map +1 -0
  61. package/dist/mission/poll-cycle.d.ts +73 -0
  62. package/dist/mission/poll-cycle.d.ts.map +1 -0
  63. package/dist/mission/poll-cycle.js +100 -0
  64. package/dist/mission/poll-cycle.js.map +1 -0
  65. package/dist/mission/poll-executor.d.ts +91 -0
  66. package/dist/mission/poll-executor.d.ts.map +1 -0
  67. package/dist/mission/poll-executor.js +328 -0
  68. package/dist/mission/poll-executor.js.map +1 -0
  69. package/dist/models/types.d.ts.map +1 -1
  70. package/dist/models/types.js +33 -27
  71. package/dist/models/types.js.map +1 -1
  72. package/dist/self-tuning/profiles/qwen38.d.ts +13 -14
  73. package/dist/self-tuning/profiles/qwen38.d.ts.map +1 -1
  74. package/dist/self-tuning/profiles/qwen38.js +17 -18
  75. package/dist/self-tuning/profiles/qwen38.js.map +1 -1
  76. package/dist/types/mission.d.ts +80 -0
  77. package/dist/types/mission.d.ts.map +1 -0
  78. package/dist/types/mission.js +12 -0
  79. package/dist/types/mission.js.map +1 -0
  80. package/docs/architecture/adr/0003-mission-orchestrator.md +99 -0
  81. package/docs/architecture/adr/0004-deterministic-merge-gate.md +127 -0
  82. package/docs/architecture/adr/0005-ollama-compatible-surface.md +74 -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 +50 -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 +1271 -89
  101. package/tools/agents/scripts/requirements-proxy.txt +1 -1
  102. package/tools/agents/tests/test_anthropic_proxy_streaming.py +171 -0
  103. package/tools/agents/tests/test_gate_failclosed_plane_admin.py +248 -0
  104. package/tools/agents/tests/test_ollama_compat.py +320 -0
  105. package/tools/agents/tests/test_plane_admin_protect.py +124 -0
  106. package/tools/agents/tests/test_stream_heartbeat.py +77 -0
@@ -5,21 +5,20 @@
5
5
  * Carried over from QWEN36_PROFILE unchanged EXCEPT for concurrency, which is
6
6
  * not a preference here but a property of the server.
7
7
  *
8
- * CORRECTED 2026-09-21. This header said the model was served by
9
- * `ninfer-serve --max-concurrency 1` and that there was ONE rail. That engine
10
- * is not what runs: the backend is buun-llama-cpp behind
11
- * uap-gsq-rco-server.service with `-np 2`, and the proxy was raised to match
12
- * (PROXY_CONCURRENCY_LIMIT / UAP_MODEL_SLOTS / PROXY_SESSION_ADMISSION_LIMIT
13
- * all 2). The reasoning below still holds, only the number changed: slots must
14
- * track what the server and proxy will actually run in parallel. Declaring
15
- * MORE than that does not get more in flight — it gets one running and the
16
- * rest queued, the extra slots buy latency and a longer wedge window rather
17
- * than throughput, and the adaptive controller then reads the queueing delay
18
- * as backpressure and throttles a server that was never saturated.
8
+ * HISTORY: corrected 2026-09-21 (ninfer-serve -> buun-llama-cpp, 1 -> 2
9
+ * rails), then SUPERSEDED 2026-10-04 — the backend is now the strata serve
10
+ * layer on :8080 (flash-next IQ3_XXS pack), which runs ONE rail over a
11
+ * 131072-cell pool. Slots drop 2 -> 1 for the same reason they rose 1 -> 2:
12
+ * slots must track what the server and proxy will actually run in parallel.
13
+ * Declaring MORE than that does not get more in flight — it gets one running
14
+ * and the rest queued, the extra slots buy latency and a longer wedge window
15
+ * rather than throughput, and the adaptive controller then reads the
16
+ * queueing delay as backpressure and throttles a server that was never
17
+ * saturated.
19
18
  *
20
- * Whether the second rail EARNS anything is a separate question from whether
21
- * it exists: check llamacpp:n_busy_slots_per_decode in /metrics (>1 means
22
- * requests genuinely overlapped), or `uap inference health`.
19
+ * Whether a future second strata rail EARNS anything is a separate question
20
+ * from whether it exists: `uap inference health` (backend strata) answers it
21
+ * once one does.
23
22
  *
24
23
  * Everything else is deliberately identical to the qwen36 seed: nothing has
25
24
  * been re-measured on 3.8 yet, and the tuning loop's job is to beat this seed,
@@ -38,10 +37,10 @@ export const QWEN38_PROFILE = {
38
37
  'handsfree.enabled': true,
39
38
  'handsfree.intensity': 'aggressive',
40
39
  UAP_HANDSFREE_STAGNATION_LIMIT: 6,
41
- // Concurrency: TWO rails (see the header) — matches `-np 2` on the server
42
- // and the proxy's concurrency/admission limits. Adaptive stays on so a
43
- // genuinely overloaded server still backs off.
44
- 'modelConcurrency.slots': 2,
40
+ // Concurrency: ONE rail (see the header) — matches the strata server's
41
+ // single slot and the proxy's concurrency/admission limits. Adaptive stays
42
+ // on so a genuinely overloaded server still backs off.
43
+ 'modelConcurrency.slots': 1,
45
44
  'modelConcurrency.adaptive': true,
46
45
  // Memory: bigger short-term window + pattern RAG compensate for weak planning.
47
46
  'memory.shortTerm.maxEntries': 80,
@@ -1 +1 @@
1
- {"version":3,"file":"qwen38.js","sourceRoot":"","sources":["../../../src/self-tuning/profiles/qwen38.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAIH,MAAM,CAAC,MAAM,cAAc,GAAe;IACxC,yEAAyE;IACzE,iBAAiB,EAAE,IAAI;IACvB,gBAAgB,EAAE,QAAQ;IAC1B,6BAA6B,EAAE,GAAG;IAClC,iBAAiB,EAAE,CAAC;IACpB,wBAAwB,EAAE,KAAK;IAC/B,yEAAyE;IACzE,mBAAmB,EAAE,IAAI;IACzB,qBAAqB,EAAE,YAAY;IACnC,8BAA8B,EAAE,CAAC;IACjC,0EAA0E;IAC1E,uEAAuE;IACvE,+CAA+C;IAC/C,wBAAwB,EAAE,CAAC;IAC3B,2BAA2B,EAAE,IAAI;IACjC,+EAA+E;IAC/E,6BAA6B,EAAE,EAAE;IACjC,2BAA2B,EAAE,IAAI;IACjC,uEAAuE;IACvE,wBAAwB,EAAE,IAAI;IAC9B,kEAAkE;IAClE,iCAAiC,EAAE,EAAE;IACrC,kBAAkB,EAAE,IAAI;IACxB,iBAAiB,EAAE,IAAI;CACxB,CAAC"}
1
+ {"version":3,"file":"qwen38.js","sourceRoot":"","sources":["../../../src/self-tuning/profiles/qwen38.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAIH,MAAM,CAAC,MAAM,cAAc,GAAe;IACxC,yEAAyE;IACzE,iBAAiB,EAAE,IAAI;IACvB,gBAAgB,EAAE,QAAQ;IAC1B,6BAA6B,EAAE,GAAG;IAClC,iBAAiB,EAAE,CAAC;IACpB,wBAAwB,EAAE,KAAK;IAC/B,yEAAyE;IACzE,mBAAmB,EAAE,IAAI;IACzB,qBAAqB,EAAE,YAAY;IACnC,8BAA8B,EAAE,CAAC;IACjC,uEAAuE;IACvE,2EAA2E;IAC3E,uDAAuD;IACvD,wBAAwB,EAAE,CAAC;IAC3B,2BAA2B,EAAE,IAAI;IACjC,+EAA+E;IAC/E,6BAA6B,EAAE,EAAE;IACjC,2BAA2B,EAAE,IAAI;IACjC,uEAAuE;IACvE,wBAAwB,EAAE,IAAI;IAC9B,kEAAkE;IAClE,iCAAiC,EAAE,EAAE;IACrC,kBAAkB,EAAE,IAAI;IACxB,iBAAiB,EAAE,IAAI;CACxB,CAAC"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Mission ledger types — the durable goal layer above deliver runs.
3
+ *
4
+ * A mission is a user-level goal with FROZEN acceptance criteria (the
5
+ * statement-immutability analog): the hash is taken at creation and every
6
+ * later change is an explicit, recorded replan. Missions link to durable
7
+ * deliver runs and survive reboots, crashes, and session boundaries — the
8
+ * orchestrator daemon polls this ledger to keep incomplete missions moving
9
+ * until their acceptance criteria are verified met.
10
+ */
11
+ export type MissionStatus = 'active' | 'paused' | 'delivered' | 'failed';
12
+ /** What one launch/execution attempt of (part of) a mission ended as. */
13
+ export type MissionAttemptOutcome = 'launched' | 'delivered' | 'failed' | 'interrupted';
14
+ export type SalvageStatus = 'proposed' | 'approved' | 'rejected';
15
+ export interface Mission {
16
+ id: number;
17
+ goal: string;
18
+ /** Acceptance criteria text, frozen at creation (see acceptanceHash). */
19
+ acceptance: string;
20
+ /** sha256 of the trimmed acceptance text at freeze time. */
21
+ acceptanceHash: string;
22
+ status: MissionStatus;
23
+ /** Model preset the mission's runs should use (optional). */
24
+ presetId?: string;
25
+ /** Durable deliver run most recently attached to this mission. */
26
+ latestRunId?: string;
27
+ /** Total launches (fresh + resume). Drives the relaunch backoff. */
28
+ launchCount: number;
29
+ /** Resume launches after an interruption. Drives the relaunch backoff. */
30
+ relaunchCount: number;
31
+ /** ISO time of the last launch — the backoff's anchor. */
32
+ lastLaunchAt?: string;
33
+ /** pid of the launch we spawned — the provenance anchor for run linkage. */
34
+ launchPid?: number;
35
+ createdAt: string;
36
+ updatedAt: string;
37
+ }
38
+ export interface MissionAttempt {
39
+ id: number;
40
+ missionId: number;
41
+ /** Deliver run id this attempt executed (absent for a whole-mission note). */
42
+ runId?: string;
43
+ /** Epic/phase/task ref the attempt targeted (absent = the whole run). */
44
+ taskRef?: string;
45
+ outcome: MissionAttemptOutcome;
46
+ /** One-line harness-owned summary (what happened / what remains). */
47
+ summary?: string;
48
+ /**
49
+ * What this observation saw (run.updatedAt / exit.at). Dedupes identical
50
+ * RE-observations while letting genuinely new failures of the same run
51
+ * accumulate into the struggle metric.
52
+ */
53
+ observedAt?: string;
54
+ createdAt: string;
55
+ }
56
+ /**
57
+ * Choir-style salvage: a failed attempt's residue — what it learned, what
58
+ * partial work exists — seeded as a follow-up. Proposals are APPROVAL-GATED:
59
+ * nothing is scheduled until a human or the overseer approves.
60
+ */
61
+ export interface SalvageProposal {
62
+ id: number;
63
+ missionId: number;
64
+ fromAttemptId?: number;
65
+ taskRef?: string;
66
+ proposal: string;
67
+ status: SalvageStatus;
68
+ createdAt: string;
69
+ decidedAt?: string;
70
+ }
71
+ /** An explicit supersede of a mission's frozen acceptance criteria. */
72
+ export interface AcceptanceRevision {
73
+ id: number;
74
+ missionId: number;
75
+ priorAcceptance: string;
76
+ priorHash: string;
77
+ reason: string;
78
+ createdAt: string;
79
+ }
80
+ //# sourceMappingURL=mission.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mission.d.ts","sourceRoot":"","sources":["../../src/types/mission.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,QAAQ,GAAG,WAAW,GAAG,QAAQ,CAAC;AAEzE,yEAAyE;AACzE,MAAM,MAAM,qBAAqB,GAAG,UAAU,GAAG,WAAW,GAAG,QAAQ,GAAG,aAAa,CAAC;AAExF,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,UAAU,GAAG,UAAU,CAAC;AAEjE,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,UAAU,EAAE,MAAM,CAAC;IACnB,4DAA4D;IAC5D,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,aAAa,CAAC;IACtB,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,WAAW,EAAE,MAAM,CAAC;IACpB,0EAA0E;IAC1E,aAAa,EAAE,MAAM,CAAC;IACtB,0DAA0D;IAC1D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,8EAA8E;IAC9E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,qBAAqB,CAAC;IAC/B,qEAAqE;IACrE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,aAAa,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,uEAAuE;AACvE,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Mission ledger types — the durable goal layer above deliver runs.
3
+ *
4
+ * A mission is a user-level goal with FROZEN acceptance criteria (the
5
+ * statement-immutability analog): the hash is taken at creation and every
6
+ * later change is an explicit, recorded replan. Missions link to durable
7
+ * deliver runs and survive reboots, crashes, and session boundaries — the
8
+ * orchestrator daemon polls this ledger to keep incomplete missions moving
9
+ * until their acceptance criteria are verified met.
10
+ */
11
+ export {};
12
+ //# sourceMappingURL=mission.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mission.js","sourceRoot":"","sources":["../../src/types/mission.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
@@ -0,0 +1,99 @@
1
+ # ADR 0003: The mission orchestrator — a durable goal layer above deliver runs
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-10-03 (branch `feature/331-mission-orchestrator`, Choir-uplift PR 1 of 2)
5
+ - Guide: `docs/guides/MISSIONS.md` · Visual: `docs/visual/mission-orchestrator.html`
6
+
7
+ ## Context
8
+
9
+ `uap deliver` runs converge against real gates and survive their tool call
10
+ (detached missions), but nothing owns the **goal** across process lifetimes:
11
+ a reboot, a crashed service, or a run that quietly died leaves the work
12
+ incomplete with nothing to resume it. The Choir protocol (Weber-GeoML,
13
+ Apache-2.0; arXiv 2609.31903) contributes the key insight: the loop clock and
14
+ the state must be durable and external to any agent process.
15
+
16
+ UAP already had every inner mechanism (durable run state, resume cursors, a
17
+ watch-don't-drive supervisor, convergence loops, an in-deliver blackboard
18
+ orchestrator, epic controller). What was missing was the outer loop and its
19
+ durable state: a mission ledger plus a poll daemon that keeps incomplete
20
+ missions moving until their frozen acceptance criteria are met.
21
+
22
+ ## Decision
23
+
24
+ 1. **A mission ledger, separate from the coordination DB.** Missions are
25
+ operator-owned durable goals (months), coordination state is per-session
26
+ agent registry (minutes). Different lifecycles, different owners; merging
27
+ them would couple a long-lived artifact to session hygiene. Stored at
28
+ `.uap/missions.db` (SQLite, WAL, `PRAGMA user_version = 1`, `foreign_keys ON`).
29
+ 2. **Frozen acceptance criteria (statement immutability).** sha256 of the
30
+ trimmed acceptance text at creation; every change is an explicit
31
+ `uap mission replan` that records the prior text, hash, and reason in
32
+ `acceptance_revisions`. A replan RESETS the run link and backoff counters:
33
+ the prior run embeds the old criteria and must not deliver against
34
+ superseded acceptance. The acceptance travels inside every run
35
+ instruction with `--acceptance`, so the behavioral judge grades the frozen
36
+ text. PR 2's merge gate will verify this same hash before any merge.
37
+ 3. **The run marker embeds the acceptance hash.** `[mission:#N:<hash8>]` in
38
+ the instruction links a deliver run back to its mission. Because the
39
+ hash slice is replan-scoped, an old-criteria run can never be relinked
40
+ after a replan, and goal text mentioning another mission's marker cannot
41
+ collide. The marker prefix also pins the argv injection posture (the
42
+ positional token can never begin with `-`).
43
+ 4. **Linkage is provenance-checked.** Run-state files are untrusted input
44
+ (the loader sanitizes format, not intent). A discovered run links only
45
+ when its instruction carries the mission's marker AND its pid matches the
46
+ pid the orchestrator itself spawned (recorded by `claimLaunch`/
47
+ `setLaunchPid`). A planted state file cannot know a future child pid, so
48
+ it can never steer the daemon into resuming it or closing the mission.
49
+ 5. **A pure decision table + a seam-injected executor.** `decideMissionActions`
50
+ (`poll-cycle.ts`) is PURE and unit-tested; `runPollCycle`
51
+ (`poll-executor.ts`) applies its actions through injected seams
52
+ (supervisor, spawner, board), matching the project's house style
53
+ (`plan.ts`, `deliver.ts`). A cycle never throws — a bad state produces a
54
+ report note, not a dead daemon.
55
+ 6. **Launches are claimed atomically.** `claimLaunch` is a single-statement
56
+ conditional UPDATE (60-second window), so the daemon poll and
57
+ `uap mission create --launch` cannot both spawn the same mission.
58
+ 7. **Backoff: first relaunch immediate, then exponential (15·2^n, cap 1 day).**
59
+ A clean interruption is the healthy case; repeated failures back off.
60
+ Struggle (failed/interrupted attempts per mission) is deduped per
61
+ (mission, run, outcome, taskRef, observedAt) — identical re-observations
62
+ dedupe, genuinely new failures accumulate — and 3+ struggle turns salvage
63
+ proposals into escalation asks. Salvage is APPROVAL-GATED (the Choir
64
+ overseer split): the orchestrator proposes, the human rules
65
+ (`uap mission salvage approve|reject`).
66
+ 8. **The daemon is systemd-owned.** `uap-orchestrator.service` runs
67
+ `uap mission poll --loop --interval 300` (hidden alias: `uap orch poll` —
68
+ deliberately distinct from `uap orchestrator on|off`, which toggles the
69
+ in-deliver blackboard orchestrator). Node and the CLI entrypoint are
70
+ captured absolute at install time; `uap doctor` probes the unit against
71
+ its declared RSS budget in `config/capacity-policy.json`.
72
+
73
+ ## Consequences
74
+
75
+ - A mission survives reboots, crashes, session teardown, and context
76
+ exhaustion: the daemon re-decides every active mission every 300 s.
77
+ - The launch grace window (10 min) prevents double-launching before
78
+ `uap deliver` registers its run state; discovery then links the run.
79
+ - fd hygiene matters in daemon mode (the parent closes the spawn log fd;
80
+ the child holds a dup) and log files use 0600 like supervisor state.
81
+ - The attempt ledger is the struggle metric; whole-mission notes (no runId)
82
+ are never deduped so launch-loops escalate instead of retrying silently.
83
+ - PR 2 (merge gate) consumes exactly: `verifyAcceptanceHash`,
84
+ `hashAcceptance`, the revision history, and the mission↔run linkage — it
85
+ must never parse acceptance text out of the instruction string.
86
+
87
+ ## Alternatives considered
88
+
89
+ - **Fold missions into the coordination DB** — rejected: lifecycle mismatch
90
+ (see Decision 1).
91
+ - **Let deliver own the goal loop** — rejected: deliver already owns
92
+ in-run orchestration (blackboard, epics); the goal layer must survive the
93
+ run process, so it lives above it.
94
+ - **Adopt Choir itself as the backend** — deferred: single-machine today;
95
+ the protocol's distributed-worker surface (GitHub issues, forks) is a
96
+ follow-up if distribution becomes a requirement.
97
+ - **Auto-execute approved escalations** — rejected: salvage and escalation
98
+ are approval-gated by user decision (2026-10-03); the daemon proposes,
99
+ never schedules.
@@ -0,0 +1,127 @@
1
+ # ADR 0004: The deterministic merge gate — evidence, not testimony
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-10-03 (branch `feature/332-merge-gate`, Choir-uplift PR 2 of 2)
5
+ - Guide: `docs/guides/MISSIONS.md` (Merge gate section) · Visual: `docs/visual/merge-gate.html`
6
+ - Builds on: ADR-0003 (mission orchestrator; this gate is the landing half of the seam ADR-0003 documented)
7
+
8
+ ## Context
9
+
10
+ ADR-0003's orchestrator keeps a mission moving until a deliver run reports DONE
11
+ against frozen acceptance. But a DONE report is **testimony**, not evidence:
12
+ a run can claim delivery with a stub still in the diff, with its final
13
+ iteration actually failing, under acceptance criteria a later replan has
14
+ superseded, or — worst case — in a diff that also rewrites the very gates that
15
+ would judge it. Choir's merge discipline contributes the closing insight: the
16
+ merge is the moment to recompute the verdict deterministically from recorded
17
+ evidence, before the work becomes irreversible.
18
+
19
+ The merge queue (`uap merge queue`) already serializes landings and re-syncs
20
+ impacted PRs, but its pre-merge check was only `gh pr checks` — CI greenness.
21
+ CI cannot see mission semantics at all.
22
+
23
+ ## Decision
24
+
25
+ Four deterministic checks run between a mission-linked PR and master. All four
26
+ are recomputed from recorded evidence at gate time — no model calls (the
27
+ acceptance judge already ran inside deliver; the gate verifies its recorded
28
+ outcome, it does not re-judge), and no parsing of acceptance text out of
29
+ instruction strings (the ADR-0003 seam: `verifyAcceptanceHash`, the marker
30
+ hash, the attempt ledger).
31
+
32
+ 1. **Provenance / statement immutability.** The PR's `[mission:#N:<hash8>]`
33
+ marker must match the mission's CURRENT frozen acceptance hash. A PR cut
34
+ before a replan was built to superseded criteria — it fails, even though
35
+ its CI is green. Independently, the ledger itself must be intact: the stored
36
+ acceptance text must still hash to the stored hash (`verifyAcceptanceHash`),
37
+ so a hand-edited `.uap/missions.db` cannot launder acceptance.
38
+ 2. **Green recompute.** Tier 1: the gate-evidence artifact deliver records on
39
+ success (`.uap/evidence/<candidateSha>.json`, uplift 1.4) — every gate's
40
+ exit code bound to the exact HEAD it proved. Production delivered runs
41
+ persist WITHOUT a run checkpoint (deliver clears it on success), so the
42
+ artifact, not run-state history, is the seam that survives a real
43
+ delivery; a present-but-invalid artifact (red gate exit, environment
44
+ hatch, candidate-sha drift, malformed JSON) is a hard failure. Tier 2:
45
+ when no artifact was checked, run-state history must show a passing final
46
+ iteration (and 100% acceptance when the judge ran). Tier 3 is refused:
47
+ a bare `delivered` status with no artifact and no history is testimony,
48
+ not evidence. Status is read FRESH from disk, and when both pids are
49
+ known the run-state pid must match the pid the orchestrator spawned
50
+ (PR-1 provenance discipline). No salvage proposal may await a ruling —
51
+ landing past an open proposal would bypass the approval gate. Note: a
52
+ `history`-tier run whose `acceptanceMet` is undefined also covers the
53
+ case where the acceptance judge itself errored mid-deliver (the
54
+ convergence loop fails open and records no fraction) — closing that
55
+ ambiguity is PR-1 follow-up, tracked there.
56
+ 3. **Sorry-delta.** The ADDED lines of the diff (never removed lines —
57
+ deleting a TODO is progress) are scanned for stub markers (`todo!()`,
58
+ `NotImplementedError`, TODO/FIXME, placeholder implementations) in
59
+ non-test, non-docs files (tests stub seams by design; docs quote the
60
+ markers they describe), and for apology/escape phrasing ("sorry",
61
+ "I couldn't", "unable to complete") in added lines and the run's
62
+ declared summaries. Lines longer than 8 KB are skipped — a bounded-gap
63
+ placeholder pattern was quadratic on adversarial multi-megabyte lines
64
+ (security review).
65
+ 4. **Axiom-honesty.** The diff may not touch gate infrastructure — enforcer
66
+ hooks, CI workflows, policy enforcers, the quality baseline, the
67
+ version-bump script, capacity policy, the merge gate and queue
68
+ themselves, their evidence gatherer, and the mission ledger functions the
69
+ first two checks consume — without explicit acknowledgment. DELETED,
70
+ RENAMED, and BINARY gate-infra files are captured too (`git rm` on a
71
+ hook is the purest form of weakening it). The prefix list is hard-coded
72
+ in the gate's own source, not a config file: an externalized list could
73
+ be edited by the very PR it judges, while any edit to the list trips
74
+ axiom-honesty against the gate itself — the list protects itself by
75
+ being the gate. A PR may not weaken (or rewrite) the checks that judge
76
+ it. Acknowledgment is operator-visible: `--allow-gate-infra` on the CLI,
77
+ or a `gate-infra` PR label in the queue. There is deliberately NO
78
+ environment escape hatch (`UAP_*_OFF` is not consulted), and the queue's
79
+ `--force` flag skips stale CI checks but NOT this gate — a force flag is
80
+ exactly the gate-off escape this check exists to refuse.
81
+
82
+ The pure decision core is `evaluateMergeGate` (`src/delivery/merge-gate.ts`);
83
+ evidence gathering (`gatherGateEvidence`) and PR wiring (`gatePr`) live in
84
+ `src/cli/merge-gate.ts`. The queue gates only PRs carrying a mission marker;
85
+ unmarked PRs proceed as before (the gate is opt-in by provenance, not a tax
86
+ on every PR).
87
+
88
+ ## Consequences
89
+
90
+ - `uap merge gate <missionId>` is the operator surface (also `--json` for CI).
91
+ - The queue skips (never force-merges) a PR whose gate fails, listing every
92
+ failed check so an operator sees all reasons at once — the core reports a
93
+ full finding list, not first-fail. A transient `gh pr diff` failure maps
94
+ to a per-PR skip (`gate: error`), never a crash of the whole batch; the
95
+ diff is fetched lazily, so marker-less PRs cost no extra `gh` calls.
96
+ - A delivered run's recorded history is trusted only in aggregate: status,
97
+ final-iteration pass, acceptance fraction. The gate never trusts summaries
98
+ as evidence of completion — summaries are scanned for apologies, which is
99
+ the opposite direction of trust.
100
+ - Honest limits, recorded so the messages are not oversold: `verifyAcceptanceHash`
101
+ detects only INCONSISTENT hand-edits (text edited without recomputing the
102
+ stored hash); an attacker who edits both passes — it is a consistency
103
+ check, not a signature. The PR marker is author-asserted: any collaborator
104
+ can paste a green mission's marker into an unrelated PR, though doing so
105
+ only ever ADDS checks (an omitted marker faces none), and the queue warns
106
+ loudly about unmarked agent-style PRs on worktree-pattern branches. The
107
+ residual trust bottom is the agent-writable `state.json`/evidence files
108
+ themselves; the pid corroboration and the ledger-side attempt history
109
+ narrow, but do not eliminate, that surface.
110
+ - A single marker gates only the mission it names; multi-mission PRs are
111
+ follow-up work (extract all markers, require all linked missions to pass).
112
+
113
+ ## Alternatives considered
114
+
115
+ - **Re-run the acceptance judge at merge time** — rejected: a model call at
116
+ the gate makes the gate non-deterministic and re-bills the most expensive
117
+ step to guard against a claim that was already judged once. The recorded
118
+ fraction plus the stub/infra scans close the honest-failure modes
119
+ deterministically.
120
+ - **Gate every PR through mission semantics** — rejected: PRs without
121
+ missions (docs, chores) have no acceptance to verify; forcing a marker
122
+ would create the exact kind of ceremony agents learn to game. Provenance
123
+ opts a PR in.
124
+ - **Block gate-infra changes outright** — rejected: legitimate work sometimes
125
+ IS the gate system (this PR). The requirement is acknowledgment, not
126
+ prohibition — and the acknowledgment is recorded in the PR's labels where
127
+ a reviewer sees it.
@@ -0,0 +1,74 @@
1
+ # ADR 0005: The Ollama-compatible surface — a third inbound dialect on a second port
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-10-06 (branch `feature/340-ollama-compat-endpoint`)
5
+ - Guide: `docs/guides/PROXY.md`
6
+
7
+ ## Context
8
+
9
+ The anthropic proxy already serves two inbound dialects — Anthropic Messages
10
+ (`/v1/messages`) and OpenAI Chat Completions (`/v1/chat/completions`) — both
11
+ normalized through a single guarded Anthropic pipeline (admission control,
12
+ rail semaphore, loop detection, tool narrowing, malformed-tool retry, context
13
+ pruning). Ollama-native tooling is a third client family that auto-discovers
14
+ `localhost:11434` and generally cannot be re-pointed at a path-prefixed
15
+ alternative, so "just document the OpenAI surface" does not reach it.
16
+
17
+ ## Decision
18
+
19
+ 1. **Same process, second listener port.** The Ollama surface is served by the
20
+ SAME FastAPI app on `PROXY_OLLAMA_PORT` (default 11434; 0 disables). A
21
+ second process was rejected on a hard constraint, not taste: admission
22
+ control and the upstream rail semaphore are per-process in-memory state
23
+ (session admission limit=1, upstream slot limit=1), so a second process
24
+ would silently double the concurrency budget the backend was sized for and
25
+ break the rail. Path-prefixing on one port was rejected because Ollama
26
+ tooling cannot be pointed at it.
27
+
28
+ 2. **Translation only, through the shared guarded core.** Ollama requests are
29
+ translated to OpenAI shape and run through `_guarded_openai_completion` —
30
+ the same core the OpenAI route uses — then translated back to Ollama wire
31
+ shapes. This buys a parity guarantee: an Ollama client cannot drift from
32
+ the other surfaces in guardrail coverage, because there is only one
33
+ pipeline. The cost is double translation: only fields that survive BOTH
34
+ hops are mapped (see consequence 4).
35
+
36
+ 3. **Buffered streaming, same tradeoff as the OpenAI surface.** Streaming is
37
+ buffered through the guardrails, then emitted as Ollama NDJSON (one content
38
+ line plus a final `done: true` line) — not token-by-token from upstream.
39
+ Token-granular streaming would require bypassing the pipeline; correctness
40
+ of the guardrails was ranked above stream granularity. This is the same
41
+ call the OpenAI inbound already made; clients see their reply arrive at
42
+ once instead of incrementally.
43
+
44
+ 4. **Default on at 11434.** The surface's target audience auto-discovers that
45
+ port, so default-off would make the feature unreachable by its own users.
46
+ Blast radius is bounded: loopback bind by default, the same auth token
47
+ gates model-serving routes, a busy port disables the surface without
48
+ touching the Anthropic surface, and `PROXY_OLLAMA_PORT=0` is a config-only
49
+ rollback. The known asymmetry: if this proxy binds 11434 first, a
50
+ later-started real Ollama fails to bind and the error appears in Ollama's
51
+ logs, not ours.
52
+
53
+ ## Consequences
54
+
55
+ - uvicorn serves one app on two ports via a companion listener that installs
56
+ no signal handlers (SIGTERM reaches it by mirroring the primary's exit) and
57
+ runs with `lifespan="off"` so app startup runs exactly once. The proxy
58
+ requires `uvicorn>=0.30.0` for the `capture_signals` override to be
59
+ honored.
60
+ - A companion bind failure must never take the primary down: uvicorn signals
61
+ bind failure with `sys.exit(1)` inside `serve()`, and `SystemExit` is a
62
+ `BaseException` that `gather(return_exceptions=True)` does not contain —
63
+ the companion task is therefore wrapped in an explicit containment guard.
64
+ - Option mapping is deliberately minimal (`temperature`, `top_p`, `top_k`,
65
+ `stop`, `num_predict`): Ollama's `format` (structured output),
66
+ `min_p`/`repeat_penalty`/`seed` and `num_ctx` are dropped honestly at the
67
+ boundary. Wiring `response_format` through the Anthropic pipeline is a
68
+ tracked contract change that deserves its own schema-diff pass.
69
+ - The model card data (`/api/tags`, `/api/show`, `/api/ps`) mirrors only what
70
+ the backend actually exposes (llama.cpp `/props`), cached per model, and
71
+ never invents plausible-looking numbers a client would display as fact.
72
+ - The Claude contract ids that `/v1/models` advertises for Anthropic-SDK
73
+ compatibility are protocol fictions and do NOT appear in `/api/tags` — an
74
+ Ollama client must only see ids the backend actually serves.
@@ -32,6 +32,55 @@ the project, then `~/.config/uap/capacity-policy.json`.
32
32
  }
33
33
  ```
34
34
 
35
+ ## HTTP-probed services (no systemd unit)
36
+
37
+ Since 2026-10-04 a service may declare an **`http`** probe instead of
38
+ `systemd` — exactly one of the two, never both (which one would be the
39
+ truth?). This exists for the strata serve layer, the local Qwen3.8 backend:
40
+ launched by a plain script and reparented to `systemd --user`, it has no
41
+ unit to show, no `NRestarts`, no `MemoryCurrent` — before this, `uap doctor`
42
+ simply could not see the backend at all.
43
+
44
+ ```json
45
+ {
46
+ "name": "strata-server",
47
+ "http": {
48
+ "url": "http://127.0.0.1:8080",
49
+ "kind": "strata",
50
+ "metricsMustMatch": { "kv": "int8", "max_context": 131072 }
51
+ },
52
+ "budget": { "note": "…which flags run-iq3_xxs.sh launches it with…" },
53
+ "headroom": { "gpuMinFreeMiB": 600 }
54
+ }
55
+ ```
56
+
57
+ - **Liveness** is "it answers `/metrics` as the declared kind". Not answering
58
+ at all is DARK (absent, not degraded); the probe tool failing (no curl,
59
+ timeout) is UNKNOWN — the same fail-open posture as a missing `systemctl`.
60
+ A server that answers but is *not* the declared document (a llama.cpp
61
+ Prometheus body behind the port, say) is DARK too: that is exactly the
62
+ wrong-engine misroute this check exists to catch.
63
+ - **`http.metricsMustMatch`** is `execStartMustContain` for a service with no
64
+ ExecStart: `engine.<key>` values that must equal the live document. A
65
+ missing key is RED (unverified, not quietly compliant), a mismatch is RED
66
+ (configuration drift). Put the load-bearing numbers from the `note` here —
67
+ kv kind and pool size — or they will drift the way `note` did.
68
+ - **`headroom`** works exactly as for unit services (one shared
69
+ nvidia-smi probe per run). There is no RSS budget or restartBudget: with
70
+ no unit there is no `MemoryCurrent` and no `NRestarts`; liveness IS the
71
+ probe. Declaring `budget.rssMiB` or `restartBudget` on an http service is
72
+ now a **parse error**, not a silently-ignored field — a budget that cannot
73
+ be verified is the anti-pattern this policy exists to prevent.
74
+ - **URL validation is strict** — http(s) base URL only, no userinfo, query,
75
+ fragment, or path — because the probe passes it to curl as a positional
76
+ argument. That is the same first-wall doctrine as the unit-name charset.
77
+ The URL must also be **canonical**: a raw spelling that only *looks*
78
+ parseable (percent-encoded dot-segments, `/a/..`, a bare `?`, leading
79
+ whitespace) is rejected, because curl must fetch exactly the form that
80
+ was validated.
81
+
82
+ ## Fields
83
+
35
84
  - **budget** — what the service is configured to stay inside (free-text
36
85
  `note` records the provenance: which incident, which flag).
37
86
  - `budget.vramMiB` declares the service's *own* allocator limit (e.g.
@@ -16,13 +16,55 @@ uap inference health --json # for monitors and dashboards
16
16
  uap inference health --strict # exit 1 on WARN or RED
17
17
  ```
18
18
 
19
+ ## Two backends, auto-detected
20
+
21
+ Since 2026-10-04 the local Qwen3.8 backend is the **strata serve layer**
22
+ (one rail, a 131072-cell int8 pool, MTP speculative decoding, no systemd
23
+ unit). The command fetches `/metrics` once and classifies the backend: a
24
+ JSON document with an `engine` block is strata; Prometheus text with
25
+ `llamacpp:` counters is a llama.cpp server. `--backend llamacpp|strata`
26
+ forces the choice.
27
+
28
+ The strata path reads two sources:
29
+
30
+ - **`/metrics` (JSON)** — the `engine` block gives pool size, KV kind and
31
+ reserve-aware free VRAM; `live` gives the in-flight request; `requests` is
32
+ a bounded ring of completed requests (observed: 12) with real timestamps.
33
+ - **the serve log** (`--strata-log` or `$UAP_STRATA_LOG`, e.g.
34
+ `/home/cogtek/dev/strata/strata-iq3_xxs.log`) — one `strata serve: prompt
35
+ …` line per completed request, for the process's whole life. These lines
36
+ carry **no timestamps**, so samples use a monotonic line-index clock: good
37
+ enough for the early/recent trend split (the log is one process's life),
38
+ never windowable by `--since`/`--until`. A replay therefore uses the
39
+ timestamped ring only, **filtered to the requested window** — an unfiltered
40
+ ring would analyse the current process's last few requests under the
41
+ REPLAY banner — and honestly reports thin samples.
42
+
43
+ Strata adds checks the llama.cpp path cannot have (its journal never logs
44
+ decode rate):
45
+
46
+ | Check | Code | Why it exists |
47
+ | --- | --- | --- |
48
+ | Decode throughput decay | `decode-decay` | Decode speed depends on prompt DEPTH, so samples bucket by depth — the same compare-like-with-like rule prefill uses. |
49
+ | Prefix-cache failure | `cache-miss` | Strata reports reuse as a fact (`R reused`); under half the prompt served from cache means the conversation cache is not tracking. One honest 0% turn right after a client compaction does not fire it — the mean over the window must be low. |
50
+ | VRAM headroom | `vram-headroom` | The backend's own reserve-aware free-VRAM reading, catching another process eating the card from under it. |
51
+
52
+ The 2026-10-04 bug worth remembering: an *inactive* systemd unit still
53
+ reports its `ExecStart` to `systemctl`, and the old probe trusted it — the
54
+ dead gsq-rco unit's `-c 229376` printed as the live pool while strata served
55
+ 131072. Unit flags now count only when the unit is ACTIVE; `/slots` is the
56
+ live geometry otherwise.
57
+
19
58
  ## What it looks at
20
59
 
21
60
  | Check | Code | Why it exists |
22
61
  | --- | --- | --- |
23
62
  | Prefill throughput decay | `prefill-decay` | A long-lived process can get slower without failing. Only visible when samples are bucketed by prompt size. |
24
- | Checkpoint starvation | `checkpoint-starved` | `--ctx-checkpoints` is **per slot**. Too few, and the checkpoint cannot track a growing conversation, so the same prefix is re-prefilled every turn. |
25
- | KV pinned at the VBR floor | `kv-at-floor` | The pool is saturated and the cache has silently dropped to its lowest quality tier. |
63
+ | Checkpoint starvation | `checkpoint-starved` | `--ctx-checkpoints` is **per slot**. Too few, and the checkpoint cannot track a growing conversation, so the same prefix is re-prefilled every turn. (llama.cpp only — strata has no checkpoint allowance.) |
64
+ | KV pinned at the VBR floor | `kv-at-floor` | The pool is saturated and the cache has silently dropped to its lowest quality tier. (llama.cpp VBR only — strata's int8 KV is a fixed tier, no floor to pin at.) |
65
+ | Decode throughput decay | `decode-decay` | Strata: same decay question for decode, bucketed by depth. |
66
+ | Prefix-cache failure | `cache-miss` | Strata: the conversation cache re-reading most of the prompt. |
67
+ | VRAM headroom | `vram-headroom` | Strata: the backend's own free-VRAM reading under the floor. |
26
68
  | Client-visible timeouts | `generation-timeouts` | The symptom users actually feel, counted from the proxy journal. |
27
69
  | Window spans a restart | `spans-restart` | A trend across a restart compares two process lifetimes, not decay within one. |
28
70
  | Rail/pool geometry | `shared-pool` | Informational (never a fault). Under `--kv-unified` the pool is *shared*, so N rails each believing they own it is an overcommit waiting to happen. |
@@ -32,12 +74,18 @@ A fabricated GREEN is precisely what let the original incident run all day, so
32
74
  every probe fails open and any that could not run is listed explicitly as
33
75
  `unverified (probe unavailable)`.
34
76
 
35
- `UNKNOWN` requires *all three* sampled signals to be missing — no usable
36
- throughput trend, no checkpoint samples, and no KV reading. Losing one of them
37
- does not suppress the rest. An **actionable finding always wins**: timeouts are
38
- counted from the proxy journal and can fire with none of the sampled signals
39
- present, and a RED finding must never be masked by an `UNKNOWN` rollup — which
40
- would let it pass `--strict` with exit 0.
77
+ `GREEN` means the sampled evidence is **present and nothing crossed a
78
+ threshold**. An early draft required a finding to exist before the rollup
79
+ could say GREEN — and since only multi-rail llama.cpp emits an info finding
80
+ (`shared-pool`), a healthy single-rail or strata stack read UNKNOWN,
81
+ indistinguishable from a blind probe for monitors parsing `--json`.
82
+
83
+ `UNKNOWN` requires *all* sampled signals to be missing — no usable throughput
84
+ trend, no checkpoint or reuse samples, no KV or VRAM reading. Losing one of
85
+ them does not suppress the rest. An **actionable finding always wins**:
86
+ timeouts are counted from the proxy journal and can fire with none of the
87
+ sampled signals present, and a RED finding must never be masked by an
88
+ `UNKNOWN` rollup — which would let it pass `--strict` with exit 0.
41
89
 
42
90
  ## Why prompt-size bucketing matters
43
91
 
@@ -99,6 +147,15 @@ one in the window. An early version did attribute the current
99
147
  `--ctx-checkpoints 4` to a window in which it had been `1`, and recommended
100
148
  the wrong remedy as a result.
101
149
 
150
+ Strata replays have one extra rule: the ring is the only timestamped source,
151
+ so it is **filtered to the window** — an early version analysed the current
152
+ process's last dozen requests under the REPLAY banner. Draft records carry no
153
+ timestamp, so a strata replay shows no acceptance rate, and an unparseable
154
+ window expression is disclosed in `unverified (probe unavailable)` rather
155
+ than silently widening the window. A live (non-replay) run against the serve
156
+ log cannot apply `--since` (the log has no timestamps) and says so in the
157
+ same list.
158
+
102
159
  ## Reading the output
103
160
 
104
161
  ```