diff --git a/CHANGELOG.md b/CHANGELOG.md index aeb8655..f596423 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,48 @@ All notable changes to OLP land here. Per `CLAUDE.md` release_kit overlay, this ## Unreleased +(empty — Phase 2 entries land here once Phase 2 opens) + +## v0.1.1 — 2026-05-25 + +### Phase 1 cleanup — pre-Phase-2 batch (D35–D42, closes 16 of 17 issues) + +**Overview.** v0.1.1 closes the post-v0.1.0 cleanup batch covering all 17 pre-Phase-2 issues raised during the 6-round cold-audit cycle on the Phase 1 deliverable. 8 D-day commits (D35–D42) shipped between 2026-05-24 and 2026-05-25. 16 issues closed; issue #16 (streaming singleflight) stays OPEN as the v1.x tracker with its design ratified in ADR 0005 Amendment 8. + +**Test count: 416 (v0.1.0) → 468 (v0.1.1).** +52 tests across the cleanup batch. + +### D35 — pre-Phase-2 batch #1 (issues #4 #9 #10 #11 #12) + +- **#4 — X-OLP-Latency-Ms uniform.** Audit confirmed already-correct via D32; D35 adds the `#4-audit` regression test pinning the 5-header invariant on the 503 no-provider sendError so future drift is caught immediately. +- **#9 — Streaming empty-then-clean-exit headers.** Zero-chunk streaming path now guards `!res.headersSent` and emits Content-Type=text/event-stream, Cache-Control=no-cache, Connection=keep-alive, X-Accel-Buffering=no, plus all 5 X-OLP-* headers via olpHeaders before writing `SSE_DONE`. Zero-chunk path correctly does NOT cache. +- **#10 — Streaming post-first-chunk error truncation marker.** Two sibling fixes: catch-block-firstChunkEmitted=true and error-chunk-after-first-chunk both now emit synthetic `{type:'stop', finish_reason:'length'}` via `irChunkToOpenAISSE` + `SSE_DONE` + `res.end()`. Per ADR 0004 § Fallback safety: post-first-chunk truncation surfaces as `length` finish, never a hang. +- **#11 — `validateIRRequest` irVersion strict check.** ADR 0003 IR contract pins irVersion to `'1.0'`. Validator now: `obj.irVersion !== undefined && obj.irVersion !== '1.0'` → rejection. Strict string match — `undefined` accepted (back-compat), `'1.0'` accepted, `'2.0'` rejected, numeric `1.0` rejected (`1.0 !== '1.0'`). +- **#12 — `alignment.yml` scripts/** trigger removal.** Removed from both `push.paths` and `pull_request.paths` since the `scripts/` directory does not currently exist (planned for Phase 7). +- **Test count:** 416 → 424 (+8). + +### D36 — pre-Phase-2 batch #2 (issues #2 #5 #6 #13 #14 #15) + +- **#2 — cache_control partial-noop debug log.** `server.mjs handleChatCompletions` fires `logEvent('debug', 'cache_control_partial_noop', { chain, marker_count })` at most once per request when markers present AND chain has at least one non-Anthropic hop. Per ADR 0005 § D2. +- **#5 — ADR 0002 vibe.mjs → mistral.mjs.** § Decision filesystem layout corrected to match the shipped file naming convention (file named after provider key, not CLI binary). Amendment 5 documents the correction + makes the convention statement explicit for future contributors. +- **#6 — mistral.mjs A5 flip + ALIGNMENT.md table update.** Header A5 (model flag) flipped from `UNPINNED-D-later-verifies` to `CONFIRMED-NOT-APPLICABLE` with DeepWiki citation; ALIGNMENT.md Speculative-Candidate table mistral row updated to remove A5. +- **#13 — /v1/models alias governance.** ALIGNMENT.md gains "Controlled deviations (entry-surface scope)" subsection documenting the alias surface as a controlled Rule 2(b) deviation; `docs/openai-spec-pin.md` gains the alias-surfacing subsection with full 4-field contract table. +- **#14 — cache_control slot determinism regression test.** 4 tests in test-features.mjs construct hand-built IRs with synthetic markers (bypassing openAIToIR which strips them at v0.1) and verify the cache key SHA-256 is deterministic. Per ALIGNMENT.md Rule 2 (No Invention), no `sortMarkers` helper shipped — the slot is dead-code at v0.1. +- **#15 — Anthropic v2.1.89 transcript artifact.** New file `docs/provider-audits/anthropic.md` as a single living version-capture artifact. Records observed `claude --version` (2.1.132 at capture date 2026-05-24), pinned version (v2.1.89 from D4), drift note, sample invocation, flag-surface table for 5 OLP-consumed flags. Closes the circular ALIGNMENT.md ↔ plugin header citation by anchoring on an external artifact. +- **Test count:** 424 → 431 (+7). + +### D37 — release.yml phase_rolling_mode gate (issue #17) + +- **CI gate enforcing phase_rolling_mode promotion discipline.** New "Enforce phase_rolling_mode (Unreleased must be promoted)" step in `release.yml` between the version-match check and the CHANGELOG extraction step. Awk extracts content between `## Unreleased` and the next `## ` heading; sed strips blank lines and parenthetical-sentinel-only lines. Non-trivial remaining content fails the workflow with `::error::` instructing the maintainer to promote Unreleased → `## v` per CLAUDE.md release_kit.phase_rolling_mode. +- **Dry-run validated against 4 cases:** current sentinel-only Unreleased → PASS; synthetic non-trivial Unreleased → FIRES with offending lines reported; no Unreleased section → PASS; multi-sentinel + blank lines → PASS. +- **Gate is purely additive** — fires only on tag push to `v*.*.*`, does not affect normal push/PR CI. +- **Test count:** 431 → 431 (no test change — CI workflow only). + ### D38 — maxConcurrent runtime enforcement (issue #1) - **Spawn lifecycle gate** — `hints.maxConcurrent` is now enforced at runtime per ADR 0002 Amendment 6: `lib/providers/index.mjs` exports a per-provider `tryAcquireSpawn` / `releaseSpawn` / `getActiveSpawnCount` semaphore; `server.mjs` gates both the buffered and streaming spawn call sites in `handleChatCompletions` with a try/finally release. Saturation surfaces as `ProviderError(CONCURRENCY_LIMIT)` which the fallback engine treats as a hard trigger (ADR 0004 Amendment 4) — the chain advances to the next hop instead of queueing. If the entire chain is saturated, the user receives a chain-exhausted error via the existing exhaustion path. Closes #1. Queue+timeout deferred (see ADR 0002 Amendment 6 § Design choice). Test count 431 → 447. +- **Spawn lifecycle gate** — `hints.maxConcurrent` is now enforced at runtime per ADR 0002 Amendment 6: `lib/providers/index.mjs` exports a per-provider `tryAcquireSpawn` / `releaseSpawn` / `getActiveSpawnCount` semaphore; `server.mjs` gates both the buffered and streaming spawn call sites in `handleChatCompletions` with a try/finally release. Saturation surfaces as `ProviderError(CONCURRENCY_LIMIT)` which the fallback engine treats as a hard trigger (ADR 0004 Amendment 4) — the chain advances to the next hop instead of queueing. If the entire chain is saturated, the user receives a chain-exhausted error via the existing exhaustion path. Closes #1. Queue+timeout deferred (see ADR 0002 Amendment 6 § Design choice). Test count 431 → 447. + ### D39 — D16 follow-ups (issue #3): explicit cache delete + eviction log + SPAWN_TIMEOUT asymmetry doc - **Part 1 — `CacheStore.delete(keyId, cacheKey)`** — adds an explicit eviction primitive to `lib/cache/store.mjs`. Returns `boolean` (true if entry present and removed; false otherwise) and removes empty per-keyId namespace `Map` entries from the outer store for memory hygiene (matches the D38 `_activeSpawns` pattern). `server.mjs` D16 salvage path replaces `cacheStore.set(..., ttlMs=0)` (lazy tombstone that lived in the namespace `Map` until the next `get`/`peek` purged it) with `cacheStore.delete(...)` (immediate removal). Cache semantics unchanged — truncated responses still don't persist. ADR 0005 § "Cache write conditions" item 1 authority. @@ -45,6 +83,30 @@ All notable changes to OLP land here. Per `CLAUDE.md` release_kit overlay, this - **Authority:** ADR 0005 Amendment 8 (this commit); ADR 0005 Amendment 6 (D34 — original deferral note); GitHub issue #16 (round-6 F13 — sibling TOCTOU); ADR 0002 Amendment 6 (D38 — `tryAcquireSpawn` semantics that §7 coordination builds on); ADR 0004 Amendment 5 (D40 — observability pattern §11 extends); `CLAUDE.md` release_kit_overlay phase_rolling_mode — under Unreleased; CC 开发铁律 v1.6 § 10.x (design-only amendment; fresh-context reviewer not required per the Iron Rule 10 implementation-phase scope, documented in the amendment's procedural mechanism). - **Test count:** 468 → 468 (no test change — design-only). +### Phase 1 cleanup release_kit checklist + +- [x] All 8 D-day deliverables landed on main (D35-D42) +- [x] CI green on every D-day commit + on this release commit's head +- [x] Cold-audit round 7 (fresh-context opus full-pass) — PASS_WITH_MINOR, 0 P1/P2 findings +- [x] 16 of 17 pre-Phase-2 GitHub issues closed (#1-#15 and #17); #16 stays OPEN as v1.x tracker +- [x] Issue #16 status comment posted referencing ADR 0005 Amendment 8 design ratification +- [x] CHANGELOG "Unreleased" promoted to "## v0.1.1 — 2026-05-25" with D35-D42 entries +- [x] `package.json` bumped from 0.1.0 → 0.1.1 +- [x] `docs/v1x-roadmap.md` created — 7 deferred items with anchors + start triggers +- [ ] Tag pushed (next step in this PR's lifecycle) +- [ ] `release.yml` triggered + GitHub Release created (auto on tag push; D37 phase_rolling_mode gate will pass because Unreleased is now sentinel-only) + +### Known limitations carried to v1.x + +Full list with code anchors + start triggers in [`docs/v1x-roadmap.md`](./docs/v1x-roadmap.md): +- Streaming-path singleflight (issue #16, ADR 0005 Amendment 8 design ratified) +- Multi-key auth (`lib/keys.mjs`) +- Soft-trigger reactivation (ADR 0004 Amendment 2) +- `/health` activeSpawns integration (ADR 0002 Amendment 6 forward note) +- Provider-level `cacheKeyFields` mask (ADR 0005 Amendment 7 forward note) +- Streaming-path SPAWN_FAILED salvage (bundled with #1 in v1.x) +- D40 AUTH_MISSING tuple test coverage (test polish) + ## v0.1.0 — 2026-05-24 ### Phase 1 Close — Multi-provider proxy core diff --git a/docs/adr/0002-plugin-architecture.md b/docs/adr/0002-plugin-architecture.md index 2eff2d2..94377e5 100644 --- a/docs/adr/0002-plugin-architecture.md +++ b/docs/adr/0002-plugin-architecture.md @@ -7,6 +7,8 @@ ## Amendments +> **Note on numbering.** Sequence is 1, 3, 4, 5, 6 — Amendment 2 was never written. The reserved slot was originally planned for a separate `maxConcurrent` ratification, but that content was folded into Amendment 1 (the retroactive contract-sync amendment) at filing time and the gap was not backfilled. The gap is intentional and load-bearing — no missing content; do not renumber Amendments 3+ to close it (cross-references to Amendment N from other docs would silently break). + ### Amendment 6 — 2026-05-24: `maxConcurrent` runtime enforcement landed (D38, issue #1) - **Finding:** Amendment 1 (2026-05-23) ratified `maxSpawnTimeMs` into the Provider contract but explicitly noted that `hints.maxConcurrent` remained **declarative-only at v0.1** — type-validated at startup in `lib/providers/base.mjs` (`validateProvider` requires it to be a non-negative integer) but unenforced at runtime (no semaphore / in-flight counter / spawn queue in `server.mjs`). Cold-audit catch from D11 (commit `f659e29`): the diff-review reviewer grep-verified that the original ADR draft's claim "Enforced by the spawn-concurrency guard in `server.mjs`" was false. GitHub issue #1 was filed to track the gap. D38 closes that gap. diff --git a/docs/adr/0005-cache-cross-provider.md b/docs/adr/0005-cache-cross-provider.md index 2d2030d..128493c 100644 --- a/docs/adr/0005-cache-cross-provider.md +++ b/docs/adr/0005-cache-cross-provider.md @@ -11,7 +11,7 @@ **Status:** Design ratified. Implementation deferred to v1.x. -**Context.** Amendment 6 (D34) formally deferred streaming-path D4 singleflight participation with the note "the design alone warrants a dedicated ADR." Round-6 cold-audit F13 (filed as issue #16) raised the sibling TOCTOU window: `server.mjs:782 preCheckHit = await cacheStore.peek(...)` followed by streaming-branch entry at line 811 (gated on `!preCheckHit`) creates a race where, between peek and spawn, a concurrent populator can write the cache OR a TTL can expire. The streaming branch is path-locked at the moment of the peek result. +**Context.** Amendment 6 (D34) formally deferred streaming-path D4 singleflight participation with the note "the design alone warrants a dedicated ADR." Round-6 cold-audit F13 (filed as issue #16) raised the sibling TOCTOU window: `server.mjs:782 preCheckHit = await cacheStore.peek(...)` followed by the streaming-branch entry conditionals at lines 817–823 (the TODO anchor sits just above at line ~810 and is the navigable landmark; line numbers may drift across commits) creates a race where, between peek and spawn, a concurrent populator can write the cache OR a TTL can expire. The streaming branch is path-locked at the moment of the peek result. This amendment ratifies the v1.x design so the implementation work has a single specification to follow. @@ -59,7 +59,7 @@ This amendment ratifies the v1.x design so the implementation work has a single - For each `client ∈ attachedClients`: if `client.queueByteSize + chunkSize > PER_CLIENT_QUEUE_CAP` (default 1 MB), the client is disconnected with `STREAM_BACKPRESSURE` (see §8). Otherwise push the chunk into `client.queue`, update `queueByteSize`, fire `resolveNext` if pending. When the source iterator returns/throws/aborts, the tee task: - - On normal completion: writes `accumulatedChunks` to cache via the standard cache-write conditions (cacheable opt-out, truncated-not-cached, size cap — all from Amendment 1/3/5). Resolves all clients' `resolveNext` with their remaining queue then sentinel-marks `done`. Releases the D38 spawn slot once. Removes the entry from `_streamingInflight`. + - On normal completion: writes `accumulatedChunks` to cache via the standard cache-write conditions — `truncated-not-cached` from § Decision § "Cache write conditions" item 1; `cacheable=false` opt-out from Amendment 3; `claude -p --output-format text` wire-shape limitation from Amendment 5; size cap from Amendment 3. (Note: ADR 0005 has no Amendment 1 heading — the section §-Decision body item-1 is the source for `truncated-not-cached`, NOT a numbered amendment.) Resolves all clients' `resolveNext` with their remaining queue then sentinel-marks `done`. Releases the D38 spawn slot once. Removes the entry from `_streamingInflight`. - On source error: rejects all clients with the error. Does NOT write cache. Releases the D38 spawn slot. Removes entry. - On source abort (all clients disconnected): cancels the iterator via AbortController, releases the slot, removes entry. No cache write (partial response not persisted, matches D16 buffered-path SPAWN_FAILED salvage NOT applying to abort). diff --git a/package.json b/package.json index f795d63..534e203 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "olp", - "version": "0.1.0", + "version": "0.1.1", "description": "Personal multi-provider LLM proxy. Successor to OCP. One HTTP endpoint, multiple subscriptions behind it, automatic routing + fallback + caching.", "type": "module", "main": "server.mjs",