mirror of
https://github.com/dtzp555-max/olp.git
synced 2026-07-21 21:15:10 +00:00
docs: D42 — streaming singleflight design ADR + v1.x safeguards (issue #16)
ADR 0005 Amendment 6 (D34) deferred streaming-path D4 singleflight to v1.x with the note "the design alone warrants a dedicated ADR." Round-6 cold-audit F13 (issue #16) raised the sibling TOCTOU window between server.mjs's preCheckHit peek and the streaming-branch spawn. D42 fulfils Amendment 6's deferral note by ratifying the v1.x design as ADR 0005 Amendment 8 — design only, no implementation. **Design ratified (ADR 0005 Amendment 8, 14 sections):** 1. New `cacheStore.getOrComputeStreaming(keyId, cacheKey, sourceFactory)` API with `{ stream, isFirst }` return shape. Mirrors the buffered path's `getOrCompute` to keep the cache API surface coherent. 2. `StreamingInflightEntry` shape — source iterator + AbortController + accumulated-chunks replay buffer + attached-clients Set + state flags + D38 spawn-slot tracker. 3. `AttachedClient` shape — per-client tee buffer + byte-size meter + late-joiner replay flag + done/resolveNext/rejectNext for the single-reader-multi-writer tee. 4. Tee fan-out loop (one reader drains source, fans chunks to all attached clients). 5. Late-joiner replay policy — accumulated chunks burst-drained on attach. 6. Cache TTL race during inflight — late joiners see the inflight entry and join; expired cache slot is overwritten by inflight completion. 7. D38 maxConcurrent coordination — only first caller acquires; release fires once on source-complete/error/abort. 8. New `STREAM_BACKPRESSURE` error code. NOT a hard trigger. Affected client gets synthetic `{ type: 'stop', finish_reason: 'length' }` + `[DONE]` (matches D35 #10 truncation marker). 9. Mid-stream disconnect — remove from attached set; if 0 remaining, abort source via AbortController. 10. Replay buffer cap (10 MB, matches D23 cache-entry cap) — over cap marks entry not cacheable, late joiners get backpressure error. 11. Observability — 4 new log events (streaming_inflight_join / source_done / abort, stream_backpressure_disconnect) + new `X-OLP-Streaming-Inflight: source | attached | solo` header. 12. Server.mjs wiring — replaces the current peek+spawn pattern; TOCTOU window closes because Map check+insert is synchronous. 13. Test surface (when implementation lands) — 10 scenarios covering single-client / 2-concurrent / 3-concurrent / mid-stream join / first-disconnect / all-disconnect / source-error / backpressure / D38 coordination / TTL race / replay cap / X-OLP-* header values. 14. Defaults — PER_CLIENT_QUEUE_CAP=1MB, ACCUMULATED_REPLAY_CAP=10MB, STREAM_BACKPRESSURE not in HARD_TRIGGER_CODES. **Multi-layer safeguards (the maintainer asked: "保证后面这一块会被处理而不会被忽略"):** 1. **`docs/v1x-roadmap.md` (NEW)** — single living landing page for every Phase-1 deferral. 7 items at D42: - #1 Streaming SF (this amendment) - #2 Multi-key auth (lib/keys.mjs) - #3 Soft trigger reactivation (ADR 0004 Amendment 2) - #4 /health activeSpawns integration (D38) - #5 Provider-level cacheKeyFields mask (ADR 0005 Amendment 7) - #6 Streaming-path SPAWN_FAILED salvage - #7 AUTH_MISSING tuple test coverage (D40 follow-up) Each entry names the ratifying ADR, load-bearing code anchor (file:line), GitHub issue (if any), concrete start trigger, and estimated effort. Maintainer's session-startup discipline grep this file at sprint kickoff. 2. **Issue #16 STAYS OPEN** — not closed in D42. Body updated post- commit to reference Amendment 8 with status "design ratified; implementation pending." Do not close until §13 test surface is green on actual implementation. 3. **`lib/cache/store.mjs#getOrCompute` JSDoc** — TODO comment for the sibling streaming API pointing at Amendment 8 + v1x-roadmap.md #1. 4. **`server.mjs` streaming-branch entry (~line 810)** — TODO comment block citing Amendment 8 + issue #16 + roadmap.md #1, naming the exact code lines the v1.x impl will replace. 5. **`README.md § Known limitations` section** — new subsection surfaces 4 limitations to users (streaming SF / soft triggers / multi-key auth / cacheKeyFields mask), each linking to the v1x-roadmap.md entry. 6. **Amendment 8 § "Cross-references and safeguards"** — explicit cross-link block enumerating the above 4 anchors so a future ADR-only reader knows every breadcrumb. **Maintainer decision recorded:** Option 1 (design ADR only) chosen over Option 2 (design + implementation now). Rationale: 200-400 lines of concurrency primitives + 15-20 tests is not "pre-Phase-2 cleanup" in shape — it is real v1.x feature work. Shipping streaming SF in a v0.1.1 patch release would muddy the Phase 1 / Phase 2 contract that v0.1.0 ratified. Personal/family-scale load makes the deferral safe at v0.1. Changes (6 files, +165 / -0): - `docs/adr/0005-cache-cross-provider.md` — Amendment 8 prepended (133 lines). - `docs/v1x-roadmap.md` — NEW file (148 lines). - `lib/cache/store.mjs` — getOrCompute JSDoc gains TODO block (8 lines). - `server.mjs` — streaming-branch entry gains TODO block (6 lines). - `README.md` — Known limitations section (9 lines). - `CHANGELOG.md` — D42 sub-entry under Unreleased (9 lines). No code-behavior change. No new tests. No package.json bump (phase_rolling_mode). Authority: - ADR 0005 Amendment 8 (this commit) — design ratification - ADR 0005 Amendment 6 (D34) — original deferral with "design ADR needed" note that this commit fulfils - GitHub issue #16 (round-6 F13) — sibling TOCTOU; STAYS OPEN - ADR 0002 Amendment 6 (D38) — tryAcquireSpawn semantics - ADR 0004 Amendment 5 (D40) — observability pattern extension - CC 开发铁律 v1.6 § 10.x — design-only amendment; fresh-context reviewer not required per Iron Rule 10 implementation-phase scope - CLAUDE.md release_kit_overlay phase_rolling_mode — under Unreleased Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
# OLP v1.x Roadmap — Deferred Work Tracker
|
||||
|
||||
**Purpose.** Single landing page for every Phase-1 deferral that an actual v1.x sprint must pick up. Each entry cross-references its ratifying ADR, its GitHub issue (if any), and the load-bearing code anchor so a future maintainer can resume without spelunking the commit history.
|
||||
|
||||
**Status:** Living document. Add new entries at the top. Each item should answer:
|
||||
1. **What** is deferred?
|
||||
2. **Why** was it deferred (link the ratifying ADR amendment).
|
||||
3. **Where** does the work live in the tree today (file + anchor).
|
||||
4. **When** does it need to land (trigger: load profile, security event, governance amendment).
|
||||
|
||||
**Reading order for a v1.x sprint kickoff.** Items #1–#3 are the most architecturally consequential and should be designed in dependency order: #2 (multi-key auth) blocks header gating in #1 and observability ownership in #4. #1 (streaming SF) blocks #5 (soft trigger reactivation) only if soft triggers are wired on streaming requests.
|
||||
|
||||
---
|
||||
|
||||
## #1 — Streaming-path singleflight + TOCTOU close
|
||||
|
||||
- **What.** `cacheStore.getOrComputeStreaming(keyId, cacheKey, sourceFactory)` API replacing the current `peek + spawn` pattern in `server.mjs`. Per-(keyId, cacheKey) inflight Map with tee fan-out, bounded per-client backpressure queues, late-joiner replay buffer, AbortController propagation on all-disconnect.
|
||||
- **Why deferred.** Personal/family-scale single-tenant load — N concurrent identical streaming requests is an edge case that has not been reported. Each concurrent caller receives the correct response; the waste is N CLI processes instead of one.
|
||||
- **Design ADR (ratified).** [`docs/adr/0005-cache-cross-provider.md` Amendment 8](./adr/0005-cache-cross-provider.md) — full design including the inflight Map shape, tee policy, late-joiner replay, backpressure cap, D38 semaphore coordination, abort policy, cache TTL race handling, observability event set, and X-OLP-Streaming-Inflight header. Implementation acceptance criteria are in Amendment 8 §13.
|
||||
- **Tracking issue.** GitHub issue [#16](https://github.com/dtzp555-max/olp/issues/16) — STAYS OPEN as v1.x tracker. Sibling: the closed-but-not-implemented Amendment 6 deferral (D34 F1).
|
||||
- **Code anchors today.**
|
||||
- `server.mjs` lines ~782 (`preCheckHit = await cacheStore.peek(...)`) and ~811–817 (streaming branch entry) — these are the lines the new API replaces.
|
||||
- `lib/cache/store.mjs` `getOrCompute` — sibling API; the new one mirrors its shape on the streaming path.
|
||||
- **Trigger to start.** Any of: (a) report of N>1 concurrent identical streaming requests in the wild, (b) v1.x sprint planning kickoff with the maintainer explicitly opening this scope, (c) downstream feature requiring tee-streaming primitive (e.g., browser-side observer attaching to an existing stream).
|
||||
- **Estimated effort.** Design ADR ratified (Amendment 8) = 30 min done. Implementation = 200–400 lines + 15-20 tests + fresh-context reviewer pass. ~3-4 hours of subagent runtime with full Iron Rule 10 discipline.
|
||||
|
||||
## #2 — Multi-key auth (`lib/keys.mjs`)
|
||||
|
||||
- **What.** Per-API-key identity, namespace scoping for the cache, ownership tier (owner vs guest) for header gating, and audit log of which key issued which request.
|
||||
- **Why deferred.** Phase 1 ships single-tenant — the cache layer carries `keyId='__anonymous__'` (D5). No real user identity is needed for personal/family use today.
|
||||
- **Design ADR (NOT YET RATIFIED).** No design ADR exists yet. v1.x sprint must produce one before implementation.
|
||||
- **Tracking.** Not a GitHub issue (no governance event filed for it). Tracked here + in `AGENTS.md § Key files to know` (`lib/keys.mjs` marked 📋 Planned).
|
||||
- **Blocks.**
|
||||
- `X-OLP-Fallback-Detail` owner-only gating (D40 / ADR 0004 Amendment 5 — currently ungated).
|
||||
- `/health` per-key visibility (currently anonymous-only).
|
||||
- **Code anchors today.**
|
||||
- `lib/cache/store.mjs` per-keyId namespace Map — the data model is already keyed by `keyId`; only the keyId source is hardcoded.
|
||||
- `server.mjs` request handlers — the `keyId='__anonymous__'` constant needs to be replaced by a header/token lookup.
|
||||
- **Trigger to start.** First multi-user deployment of OLP (e.g., maintainer + spouse + child accessing the same instance with separate identities).
|
||||
|
||||
## #3 — Soft trigger reactivation (ADR 0004 Amendment 2)
|
||||
|
||||
- **What.** Per-provider `quotaStatus` polling, `softThreshold` comparisons, soft-skip advancement when quota approaches limit. Currently `evaluateSoftTriggers` always returns `false` because `quotaSnapshot` is never populated.
|
||||
- **Why deferred.** v0.1 hard triggers (SPAWN_FAILED / CLI_NOT_FOUND / SPAWN_TIMEOUT / CONCURRENCY_LIMIT) are sufficient for fallback advancement at personal/family scale. Soft triggers require persistent quota snapshots and a polling mechanism, which adds operational surface (timer drift, snapshot staleness, observability burden).
|
||||
- **Design ADR.** [`docs/adr/0004-fallback-engine.md` Amendment 2](./adr/0004-fallback-engine.md) — explicit v1.x deferral with mitigations (startup warning if user configures soft thresholds without runtime enforcement).
|
||||
- **Tracking.** Not a GitHub issue. Tracked here + via the startup warning in `server.mjs` (the `_softTriggersConfigured` warn emission).
|
||||
- **Blocks.**
|
||||
- Issue #8 (`X-OLP-Provider-Used` chain-origin semantics) — Option A (track `firstAttemptedProvider`) becomes preferable once soft triggers can fire. See ADR 0004 Amendment 6 § v1.x re-evaluation.
|
||||
- `X-OLP-Fallback-Detail` `trigger_type: 'soft'` path — currently dead code, becomes live with this work.
|
||||
- **Code anchors today.**
|
||||
- `lib/fallback/engine.mjs` `evaluateSoftTriggers` (returns false unconditionally at v0.1).
|
||||
- `lib/providers/base.mjs` `Provider.quotaStatus` contract (declared but unused at v0.1).
|
||||
- **Trigger to start.** First quota-rate-limit event in the wild — at which point the operator would want pre-emptive advancement rather than spawn-then-fail.
|
||||
|
||||
## #4 — `/health` `activeSpawns` integration
|
||||
|
||||
- **What.** Surface D38 `getActiveSpawnCount(providerName)` per-provider on the `/health` endpoint at the path `providers.status.<name>.activeSpawns`.
|
||||
- **Why deferred.** D38 (issue #1) shipped the runtime enforcement and exported `getActiveSpawnCount`; `/health` integration was scoped out as forward-looking polish.
|
||||
- **Design ADR.** [`docs/adr/0002-plugin-architecture.md` Amendment 6](./adr/0002-plugin-architecture.md) — names the target path explicitly: "`/health` integration deferred — when surfaced there will land at `providers.status.<name>.activeSpawns`; not wired at D38."
|
||||
- **Tracking.** Not a GitHub issue. Tracked here.
|
||||
- **Code anchors today.**
|
||||
- `lib/providers/index.mjs` exports `getActiveSpawnCount` already.
|
||||
- `server.mjs handleHealth` — extension point for the new field.
|
||||
- **Trigger to start.** First time the maintainer wants per-provider concurrency visibility for capacity planning.
|
||||
|
||||
## #5 — Provider-level `cacheKeyFields` (per-plugin mask)
|
||||
|
||||
- **What.** Per-plugin declaration of which IR fields are actually consumed by the underlying CLI invocation, used by `computeCacheKey` to skip fields that the plugin drops at spawn. Reduces spurious-miss rate from the v0.1 conservative-posture trade-off (Amendment 7).
|
||||
- **Why deferred.** At personal/family scale the extra spawn cost from spurious misses is negligible. The contract extension adds complexity (per-plugin field set + plumbing through `buildDefaultChain` → `executeHopFn` → `computeCacheKey`).
|
||||
- **Design ADR.** [`docs/adr/0005-cache-cross-provider.md` Amendment 7 § Forward path](./adr/0005-cache-cross-provider.md).
|
||||
- **Tracking.** Not a GitHub issue. Tracked here.
|
||||
- **Code anchors today.**
|
||||
- Plugin file headers — each lists its "fields dropped at spawn" table for human reference; the v1.x amendment makes that table machine-readable.
|
||||
- `lib/cache/keys.mjs computeCacheKey` — would accept `pluginCacheKeyMask` parameter.
|
||||
- **Trigger to start.** First time spurious-miss rate becomes a measurable load factor.
|
||||
|
||||
## #6 — Streaming-path SPAWN_FAILED salvage
|
||||
|
||||
- **What.** Currently the streaming branch does NOT participate in D16 salvage (the salvage-on-SPAWN_FAILED + chunks pattern that the buffered path uses). Streaming SPAWN_FAILED mid-stream → the truncation marker (D35 #10) fires, but no salvage logic captures partial chunks for downstream cache reuse.
|
||||
- **Why deferred.** Less impactful than #1 — at most one client benefits per spawn event, and the buffered path already provides salvage for the bulk of requests. Streaming is the minority path.
|
||||
- **Design ADR.** Not yet ratified. Coordinated with #1 because the tee architecture changes the salvage semantics (multiple clients may want different finish_reason interpretations on source-mid-stream-failure).
|
||||
- **Tracking.** Not a GitHub issue. Tracked here.
|
||||
- **Trigger to start.** Bundled with #1 implementation work (the inflight tee architecture changes the salvage semantics, so designing them together is cheaper than serializing).
|
||||
|
||||
## #7 — AUTH_MISSING tuple path test coverage (D40 follow-up)
|
||||
|
||||
- **What.** Dedicated test in `test-features.mjs` Suite D40 that asserts the `fallbackDetail` tuple records the AUTH_MISSING path with `trigger_type: 'auth_missing'`. D40 reviewer flagged this as the last gap in the engine-path matrix; code is structurally correct, just lacks an explicit pin.
|
||||
- **Why deferred.** Low priority — the AUTH_MISSING early-return branch has the tuple push BEFORE it (verified in D40 reviewer pass), so coverage is implicit via the other engine-path tests. A 3-line dedicated test would make the pin explicit.
|
||||
- **Design.** No ADR needed. ~5-line test addition.
|
||||
- **Tracking.** Not a GitHub issue. Tracked here.
|
||||
- **Trigger to start.** Next routine test-suite hardening pass, OR when AUTH_MISSING handling is changed for any reason.
|
||||
|
||||
---
|
||||
|
||||
## Adding a new entry
|
||||
|
||||
When a future D-day defers work, the deferring commit should:
|
||||
|
||||
1. **Always** update this file with a new entry at the top.
|
||||
2. **Always** name the ratifying ADR amendment (or note "no ADR yet — future work needs one").
|
||||
3. **Always** name the load-bearing code anchor (`file:line` form preferred over symbolic names — the symbolic name can drift).
|
||||
4. **Always** name a concrete trigger to start the work — vague triggers ("when needed") let entries rot.
|
||||
5. If the deferral has a GitHub issue, keep it OPEN and reference it here. If it does NOT, leave a note explaining why (e.g., "tracked here only — no external governance event filed").
|
||||
|
||||
The maintainer's session-startup discipline should grep this file at sprint kickoff. If an entry's "trigger to start" condition is met, it leaves this page and becomes a sprint item.
|
||||
Reference in New Issue
Block a user