Commit Graph
4 Commits
Author SHA1 Message Date
bddf2cba1e release(v0.5.1): hotfix — quota probe cache/backoff/schema-drift correctness (codex review) (#58)
* release(v0.5.1): hotfix — quota probe cache/backoff/schema-drift correctness (codex review)

Addresses three production-quality findings from codex's post-v0.5.0 review (codex review on
PR #57, reproduced with local mocks):

F1 [P1] — Doctor bypass of cache + backoff (ADR 0013 Rule 3)
  `anthropic.quota_probe_reachable` called `_probeOnce(auth)` directly, bypassing
  `quotaProbeState.backoffUntil`. Successive `olp doctor` invocations within a backoff
  window each hit upstream — violating ADR 0013 Rule 3 (backoff is mandatory for ALL
  consumers). Fix: doctor now routes through `quotaStatus()`. ADR 0013 Rule 3 clarified:
  "All consumers of `quotaStatus()`, including `olp doctor` checks, MUST route through
  `quotaStatus()` and MUST NOT call `_probeOnce()` directly."

F2 [P2] — 200 with empty ratelimit headers cached as live data (ADR 0013 Rule 5)
  A 200 OK with zero `anthropic-ratelimit-*` headers was cached as `stale: false` (live).
  Minimum-viable-schema gate added to `_probeOnce`: requires 5h-utilization + 5h-reset +
  7d-utilization + 7d-reset present; absence → `failureKind: 'schema_drift'`, backoff
  scheduled, result NOT cached. ADR 0013 Rule 5 updated with the gate specification.

F3 [P2] — Dashboard-data loses failure detail (ADR 0013 Rule 6)
  `aggregateProviderQuota()` collapsed all failure modes into `status: 'unavailable'`
  ("no public quota api or probe disabled") — same as providers with no API at all.
  Fix: `quotaStatus()` v0.5.1 contract — `null` ONLY for opt-in-off; failures return
  `{ probe_status: 'unreachable', failure: { kind, message, backoff_until? } }`.
  New `failure_kind` enum: no_credentials | auth_failed | rate_limited | schema_drift |
  network | other. Dashboard renders `unreachable` with red border + failure detail.

Authority:
  ADR 0013 Rules 3, 5, 6 (cache + backoff + schema-drift + failure transparency)
  ADR 0008 Amendment 2 (richer quota_v2 shape; new unreachable status)
  ADR 0002 Amendment 8 unchanged (constitutional permission for the probe)
  Codex review findings F1–F3 (codex on PR #57)

Changes:
  - lib/providers/anthropic.mjs: quotaProbeState gains lastError + failureKind;
    _probeOnce: min-field gate + failureKind population; quotaStatus(): v0.5.1 contract
    (null=disabled only; probe_status:live/stale/unreachable); doctorChecks routes
    through quotaStatus(); reset functions updated
  - lib/audit-query.mjs: _normalizeAnthropicQuota handles probe_status field;
    aggregateProviderQuota emits failure/failure_kind/backoff_until; unreachable status
  - dashboard.html: unreachable CSS classes + render path + footer v0.5.1
  - test-features.mjs: 38f/j/l updated for v0.5.1 shape; 38r refactored for F1;
    38g/k gain probe_status assertions; 38u/v/w new regression tests; 756→759 tests
  - docs/adr/0008: Amendment 2 (richer ProviderQuotaEntry + quotaStatus contract)
  - docs/adr/0013: Rule 3 clarification (doctor must use quotaStatus);
    Rule 5 min-viable-schema gate specification
  - package.json: 0.5.0 → 0.5.1
  - CHANGELOG.md: v0.5.1 hotfix entry promoted from Unreleased
  - README.md / AGENTS.md: Phase 5 closed at v0.5.1; Phase 6 next

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs+code: PR #58 fold-in — reviewer Nits #1 + #2 (ADR doc-drift + opt_in_off enum)

Fresh-context reviewer (PR #58) verdict: APPROVE_WITH_MINOR, 0 blocking,
4 nits. Folding in #1 + #2 (both 1-line cosmetic-but-correct fixes).
Deferring #3 (test-only edge case in module-level seam restore) and #4
(pre-existing codex F4 — bin/olp.mjs + olp-plugin still read legacy
quota shape; separate PR planned).

Nit #1 — ADR 0002 Amendment 8 documentation drift.

Amendment 8 at v0.5.0 line 20 said "the function returns `null` rather
than throwing", and line 33 said "stale-cache-on-failure (`null` is
returned only when no cache entry exists; if a stale entry exists it's
returned with a `stale: true` marker)". v0.5.1 refined this contract:
`null` is now reserved STRICTLY for opt-in-off, and all failure modes
return `{ probe_status: 'unreachable' | 'stale', failure: {...} }`.

The substantive idempotent-failure constraint (no throw to caller) is
unchanged. The operational description in Amendment 8 was stale — fixed
to cross-reference ADR 0008 Amendment 2 + ADR 0013 Rule 6 for the
v0.5.1 contract refinement. Also references Suite 38 (38u/38v/38w) as
the regression coverage producing the new shape.

Nit #2 — `failureKind: 'opt_in_off'` declared but never produced.

The enum value was listed in both the code comment (anthropic.mjs:250)
and ADR 0008 Amendment 2 (line 30) but never actually assigned —
because when opt-in is off, `quotaStatus()` returns the literal `null`
BEFORE any state mutation happens. The enum value was dead.

Fix: removed `opt_in_off` from both enum declarations + added an
inline note explaining that consumers (audit-query, doctor) distinguish
opt-in-off by checking `quotaStatus() === null`, not via failureKind.

Deferred:

- Nit #3 (test-seam restore edge case): if a caller pre-sets
  `_quotaAuthReadFnForTest` AND passes a non-default `_authReadFn`,
  the finally block restores to null clobbering pre-set value.
  Test-only impact, no production risk. Pure hygiene; defer.

- Nit #4 (codex F4): bin/olp.mjs cmdUsage and olp-plugin/index.js
  still read legacy `body.quota` field, never consume `quota_v2`.
  Reviewer confirmed neither crashes — both gracefully fall through
  to "no quota api" branch. Out of scope for this hotfix per the
  hotfix dispatch contract; separate PR will migrate them.

Tests: 759/759 still pass post-fold-in. No test changes needed.

Authority: PR #58 review thread + ADR 0013 Rule 6 (failure transparency)
+ ADR 0008 Amendment 2 (ProviderQuotaEntry v0.5.1 shape).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 09:33:47 +10:00
5288493f19 feat: D81 — dashboard-data quota_v2 shape + models-registry schema_version (Phase 5) (#53)
Authority citations (required per CLAUDE.md § Hard requirements):
  1. ADR 0012 D81 — the D-day being implemented (Phase 5 charter, audit-query
     + dashboard-data extension row in § D-day table)
  2. ADR 0013 Rule 5 — schema_version in models-registry.json mandate. D80
     used a local constant QUOTA_SCHEMA_VERSION = '2026-05-26' (reviewer nit
     #4 at PR #52). D81 folds in Rule 5 compliance: adds quota_probe.schema_version
     to models-registry.json and has anthropic.mjs read from there with the
     constant as fallback via _resolveSchemaVersion().
  3. ADR 0008 — the audit-query design being amended (Amendment 1 added at D81
     to docs/adr/0008-dashboard-and-audit-query.md documenting: quota_probe in
     registry, aggregateProviderQuota() API shape, quota_v2 key, deprecation
     timeline for legacy quota key).
  4. D80 PR #52 (commit 82d2e1c) — producer of the quotaStatus() shape that
     D81 normalizes. The 13-field anthropic-ratelimit-unified-* shape from
     _parseRateLimitHeaders() is consumed by _normalizeAnthropicQuota() here.

Changes (A–G per D81 spec):

A. models-registry.json — new top-level quota_probe key
   - quota_probe.schema_version = '2026-05-26'
   - quota_probe.anthropic.{ source, endpoint, fields_pinned[13] }
   - fields_pinned is load-bearing for drift detection (ADR 0013 Rule 5)

B. lib/providers/anthropic.mjs — _resolveSchemaVersion() helper
   - Reads quota_probe.schema_version from modelsRegistryRaw at call time
   - Falls back to QUOTA_SCHEMA_VERSION constant if registry field absent
   - quotaStatus() now uses _resolveSchemaVersion() instead of raw constant

C. lib/audit-query.mjs — aggregateProviderQuota() export
   - Normalizes per-provider quotaStatus() returns to ProviderQuotaEntry shape
   - Live → { status:'live', utilization, reset, representative_claim, … }
   - Stale → { status:'stale', … }
   - null return → { status:'unavailable', reason:'no public quota api or probe disabled' }
   - throw → { status:'unavailable', reason:<error.message> }
   - getQuotaStatus injection point for full test isolation (D81 §F)

D. server.mjs handleManagementDashboardData — quota_v2 added
   - Calls auditAggregateProviderQuota({ providers: loadedProviders })
   - Both legacy quota (backwards compat) and quota_v2 in response
   - Graceful degradation: quota_v2 failure → warn log + empty array, rest of
     payload unaffected

E. server.mjs handleManagementQuota — quota_v2 added (mirrors D)

F. test-features.mjs — Suite 37 (7 new tests)
   - 37a: live shape normalization (status=live, utilization, reset, etc.)
   - 37b: stale shape (status=stale)
   - 37c: null returns → unavailable entries
   - 37d: throw path → unavailable with reason
   - 37e: mixed providers (live + unavailable)
   - 37f: getQuotaStatus injection verified
   - 37g: models-registry.json has quota_probe.schema_version + 13 fields_pinned

G. docs/adr/0008-dashboard-and-audit-query.md — Amendment 1 added

Test delta: 720 → 727 (+7), 0 failures.

Live quota_v2 JSON sample (illustrative — probe is opt-in per ADR 0013 Rule 4;
real values require quota_probe_enabled:true + valid OAuth credentials):

  GET /v0/management/dashboard-data (owner-only)
  {
    "quota_v2": [
      {
        "provider": "anthropic",
        "status": "live",
        "schema_version": "2026-05-26",
        "last_fresh_at": 1748300000000,
        "utilization": { "5h": 0.49, "7d": 0.31 },
        "reset": { "5h": 1748290000, "7d": 1748500000, "overall": 1748300000, "overage": null },
        "representative_claim": "five_hour",
        "fallback_percentage": 0.5,
        "overage": { "status": "rejected", "disabled_reason": "org_level_disabled_until" },
        "raw_available": true
      },
      {
        "provider": "openai",
        "status": "unavailable",
        "reason": "no public quota api or probe disabled",
        "schema_version": null,
        "last_fresh_at": null,
        "utilization": null,
        "reset": null,
        "representative_claim": null,
        "fallback_percentage": null,
        "overage": null,
        "raw_available": false
      }
    ]
  }

When probe is disabled (default), anthropic entry also returns unavailable:
  { "provider": "anthropic", "status": "unavailable",
    "reason": "no public quota api or probe disabled", ... }

NOT modified: dashboard.html (D82), codex.mjs, mistral.mjs, any quotaStatus()
return shape from anthropic.mjs (D80 contract unchanged — normalization is in
the new audit-query layer only).

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 17:06:41 +10:00
0048481764 feat+docs: D60 — Phase 4 charter (ADR 0010) + default OLP_PORT 3456 → 4567 (#40)
* feat+docs: D60 — Phase 4 charter (ADR 0010) + default OLP_PORT 3456 → 4567

Opens Phase 4 (Operator + Client UX) end-to-end. Per release_kit.phase_rolling_mode,
the version bump fires at Phase 4 close (v0.4.0), not at this D-day; D60 only
ships governance + the port default change.

## ADR 0010 — Phase 4 Charter

Phase 4 scope = 5 D-day groups (~13 D-days total):
- D60 (this commit): charter + default port
- D61-D63: SSE heartbeat + recentErrors[20] ring + /status combined endpoint
- D64-D67: olp Node-based CLI scaffold + olp doctor next_action framework
- D68-D70: olp-connect zero-config IDE setup + /health.anonymousKey + ADR 0011
- D71-D73: olp-plugin/ OpenClaw gateway plugin + docs/integrations/*.md bundle

Charter records the EXPLICIT DECISION to DEFER /v1/messages (Anthropic-shape
entry surface) on the rationale: under ADR 0009 P0 failure it provides no
billing benefit AND degrades worse on fallback than OpenAI-shape clients
(because OpenAI tool schema is the cross-provider lingua franca; Anthropic-
specific features cache_control / computer_use / text_editor / thinking
blocks have no clean fallback mapping). Re-open trigger: (a) ADR 0009 P0
success AND (b) maintainer-named family CC user.

README posture updated: Claude Code listed as NOT SUPPORTED as an OLP
client; recommended alternative is "Cline + OLP" (same fallback chain
available, better cross-provider compatibility).

## Default port 3456 → 4567

server.mjs:74 default value moves so OLP and OCP (which stays on 3456) can
co-host on the same machine without OLP_PORT env override. Existing
deployments wanting the pre-D60 default can set OLP_PORT=3456 in launchd
plist / shell env.

Verified port-change invariants:
- All test-features.mjs suites use port: 0 (ephemeral) — 0 test-surface impact
- Cache / fallback / provider plugins port-agnostic
- Dashboard 30s poll + management endpoints use relative paths
- 623/623 tests pass on D60 branch HEAD

## ADR amendments

- ADR 0001 § "Decision" port-conflict paragraph: struck + amended (co-host
  is now possible via 3456 → 4567 + launchd labels dev.olp.proxy vs
  dev.ocp.proxy)
- ADR 0008 § 6.6 default-port reference updated
- docs/adr/README.md index gains ADR 0010 row

## Authority

- ADR 0010 (this commit)
- ADR 0009 (interactive-mode placeholder — /v1/messages defer rationale)
- 2026-05-26 brainstorm: OCP comprehensive feature audit (subagent output)
  + multi-provider proxy / IDE integration prior-art survey (subagent
  output, both this session)
- docs/v1x-roadmap.md (Phase 4 was the named destination)
- CLAUDE.md release_kit.phase_rolling_mode (current_phase already Phase 4;
  this charter formalizes contents)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: D60 reviewer P2-1 — soften ADR 0001 launchd-label assertion to forward-tense

Reviewer flagged the ADR 0001 amendment's launchd-label collision claim
("avoided via dev.olp.proxy vs dev.ocp.proxy") as present-tense fact when
the OLP plist generator hasn't shipped yet (lands D64-D70 per ADR 0010).
Soften to forward-tense with explicit cross-reference. The factual
claim ("co-host is possible") still stands because plist label is
controlled by the OLP project anyway; this is precision, not correction.

P2-2 (README "since v0.4.0" forward-dated branding) explicitly accepted
as prior-art-consistent with D44+ Phase 2 mid-window doc conventions —
no change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 07:59:11 +10:00
c0b696984f docs: D48 — ADR 0008 Phase 3 design draft (Dashboard + audit query layer) (#25)
* docs: D48 — ADR 0008 Phase 3 design draft (Dashboard + audit query layer, design-only)

First Phase 3 D-day. Design-only. Ratifies the storage / query model /
rotation / dashboard / refresh / scope decisions ahead of D49+
implementation D-days. Opens ADR 0007 § 12 deferral for Dashboard +
audit query layer + rotation.

NEW docs/adr/0008-dashboard-and-audit-query.md (~368 lines): 13 sections
+ Consequences + Authority citations. Per maintainer-pinned lanes
A/A/B/A/B from Phase 3 kickoff brief 2026-05-25:

  Lane 1 (tech stack): A — static HTML + vanilla JS + fetch; no build
    step; matches OLP "no bundler" ethos.
  Lane 2 (query model): A — in-memory scan of audit ndjson per request;
    O(N) per call; family-scale acceptable; defers SQLite hybrid to
    Option 3 trigger per ADR 0007 § 13 (requires engines bump as
    separate prior PR).
  Lane 3 (rotation): B — daily rotation, audit-YYYY-MM-DD.ndjson on
    first append after UTC midnight + optional bin/olp-audit-rotate.mjs
    external cron.
  Lane 4 (refresh): A — 30s page poll (no SSE infra at v0.3.0;
    pause when document.visibilityState is hidden).
  Lane 5 (dashboard scope): B — full per spec § 4.6: 4 panels (per-
    provider quota / 24h request+cache+fallback / 30d spend trend /
    top-N fallback chains).

ADR sections:

  §1 Context (what Phase 3 closes; what stays out)
  §2 Decision (5 lanes pinned)
  §3 Storage layout (~/.olp/logs/ with rotated date files)
  §4 lib/audit-query.mjs API (readAuditWindow, aggregateRequests,
     topFallbackChains, spendTrendDaily, cacheHitRateWindow)
  §5 Audit rotation (first-append-after-UTC-midnight trigger + cron
     alternative + concurrent-safety per-process lock)
  §6 Dashboard panels (4 panels per spec § 4.6, refresh + localhost-
     bound notes)
  §7 Server endpoints (/dashboard, /v0/management/dashboard-data, /v0/
     management/quota, /cache/stats — owner-only gated)
  §8 Auth gating (reuses ADR 0007 § 7 owner_only_endpoints config)
  §9 Failure modes + degradation (per-panel error states)
  §10 Acceptance criteria (15 testable items for D49-D54)
  §11 Forward path (Phase 4+ — SQLite migration, SSE push, key-mgmt UI)
  §12 Out of scope (explicitly NOT in Phase 3)
  §13 Phase 3 sprint shape (D48-D55)

CHANGED:

  - docs/adr/README.md: added ADR 0008 row with one-paragraph summary.
  - CHANGELOG.md Unreleased: D48 entry per release_kit overlay
    phase_rolling_mode discipline. Includes Phase 3 sprint shape table.

NOT IN D48 scope:

  - lib/audit-query.mjs (D49)
  - server.mjs endpoints (D50)
  - dashboard.html (D51)
  - lib/audit.mjs rotation extension + bin/olp-audit-rotate.mjs (D52)
  - tried_providers schema fix (D53; D45 P2 deferral)
  - Phase 3 close (D55; v0.3.0; maintainer-triggered)

Test count: 544 / 544 pass (design-only, no test change). Verified
locally via npm test before commit.

AUTHORITY:

  - ADR 0007 § 12 (opens Phase 3 Dashboard + audit query deferral).
  - ADR 0007 § 13 (rejects SQLite at Phase 3 per Node baseline +
    documents forward-path trigger).
  - OLP v0.1 spec § 4.6 + § 4.7 (Dashboard + observability endpoints
    planning authority).
  - OCP dashboard.html (prior-art reference for multi-panel HTML
    structure).
  - ADR 0002 (Provider.quotaStatus contract — Panel 1 data source).
  - ADR 0005 § Cache stats (/cache/stats endpoint pre-existing design).
  - CC 开发铁律 v1.6 § 10 — fresh-context opus reviewer required for
    design ADR.
  - Phase 3 kickoff via maintainer "go" 2026-05-25 + standing-autopilot
    grant (~/.cc-rules/memory/auto/standing_autopilot_phase_2.md in
    cc-rules bf0ed9a — Phase 3+ requires new authorization; the "go"
    supplied that).

ALIGNMENT.md scope check: this PR introduces a new ADR with full
authority citations per Rule 1 (Cite First). No provider plugin /
entry surface / IR change in this commit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs: D48 fold-in — opus reviewer findings (1 P2 + 2 P3, all ADR-text polish)

Fresh-context opus reviewer (PR #25) returned APPROVE_WITH_MINOR with 3
findings, all ADR-text polish. No design semantic change beyond the
clarifications.

P2 — § 8 + § 10 #9 gating-mode wording

  Original § 8 implied a new "block non-owner identities" behaviour
  without naming it; § 10 #9 tested only the universal
  allow_anonymous: false 401 case. Fix:
  - § 8 now formalizes two gating modes — owner_only_trim (Phase 2
    /health pattern) vs owner_only_block (new Phase 3 management-
    endpoints pattern) — and explains the management endpoints are
    owner_only_block because the entire payload is sensitive.
  - § 10 #9 now covers both 401 paths: (a) allow_anonymous: true
    + no header → anonymous identity → STILL 401 because management
    endpoints are owner_only_block; (b) allow_anonymous: false + no
    header → 401 at the authenticate middleware itself.

P3 #1 — /cache/stats citation accuracy

  Original § 7.4 + Authority block cited "ADR 0005 § Cache stats"
  which is not a real section. Corrected: planning authority is OLP
  v0.1 spec § 4.6; ADR 0005 references the endpoint in
  Consequences/Mitigations (~line 279) for the per-(provider, model)
  cache-hit-rate breakdown surface.

P3 #2 — cacheStore.stats() shape gap

  § 7.4 now explicitly acknowledges the current shape
  ({ hits, misses, size, inflightCount } global aggregate) lacks the
  per-(provider, model) breakdown spec § 4.6 implies; Phase 3 Panel 2
  sources per-provider counts from aggregateRequests (audit-side)
  instead. If a future panel needs the breakdown, D50 amends the
  store shape + an ADR 0005 amendment fires at that time. Phase 3
  acceptance criteria do not require the breakdown.

CHANGELOG.md Unreleased D48 entry: fold-in bullet added enumerating
the 3 fixes.

Test count: 544 / 544 pass (design-only, no test change). Verified
locally via npm test.

Authority: PR #25 fresh-context opus reviewer findings; CLAUDE.md
release_kit overlay phase_rolling_mode — under Unreleased.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 15:41:26 +10:00