Files
olp/docs/v1x-roadmap.md
T
taodengandClaude Opus 4.7 b0c080db13 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>
2026-05-25 06:58:45 +10:00

10 KiB
Raw Blame History

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 — 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 — 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 ~811817 (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 = 200400 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 — 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 — 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 buildDefaultChainexecuteHopFncomputeCacheKey).
  • Design ADR. docs/adr/0005-cache-cross-provider.md Amendment 7 § Forward path.
  • 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.