You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Adjudicated 13 findings from a 6-agent deep-review of #638 (api×2 +
data-integrity×2 + concurrency×2). 9 REAL + 1 PARTIAL + 3 NOISE.
This commit applies the 9 REAL fixes; PARTIAL (buffer backpressure
hardening) and NOISE (matches-existing-pattern) are deferred.
**F1 (Blocker) — Replication exclusion.** Added 'hdb_model_calls' to
`NON_REPLICATING_SYSTEM_TABLES` in `resources/databases.ts`. The new
table mimicked `getRawAnalyticsTable()`'s `audit: true` declaration
but missed that `hdb_raw_analytics` is in the exclusion list. Without
this, per-call rows would replicate to every cluster node — high-cost
audit-shape replication (the 150MB ProxiedRequestLog incident
pattern) and concurrent writes from two nodes could produce
identical process-local `getNextMonotonicTime()` IDs that LWW would
silently drop. One-line fix; matches the established convention.
**F2 (Important) — Pre-call errors record.** `resolveEmbedding` /
`resolveGenerative` / `requireCapability` were called BEFORE the
try/catch in `embed`/`generate`/`generateStream`, so pre-call
failures (capability mismatch, registry miss) bypassed the analytics
record. The class doc promises "both successful and failed calls
land in the table for billing visibility" — broken by these paths.
Moved resolution into the try block; failures now record with
`backend: 'unknown'` when the registry missed entirely, or with the
backend's name when it was found but the capability didn't match.
`error_code` is `'backend_not_found'`, `'capability_unsupported'`,
or `'pending_unsupported'` as appropriate.
**F3 (Important) — Context type drift.** Added `handlerPath?:
string` and `signal?: AbortSignal` to the `Context` interface in
`resources/ResourceInterface.ts`. These fields existed on the
`Request` object that becomes the ALS-bound Context for HTTP/WS
paths (via `transaction(request, ...)` at `REST.ts:92, 368`), but
weren't declared on the Context interface — the facade was reading
them via an `as any` cast. Drift bug latent until the next
refactor; documenting them on Context makes the contract explicit
and lets us drop the `as any` cast in `Models.ts`.
**F4 (Important) — Pending result duplicate row.** When a backend
returned `{ status: 'pending' }`, the facade wrote a success row
via `#record(...)`, THEN `unwrap()` threw inside the same try and
the catch wrote a second (failure) row. Replaced `unwrap()` with an
inline status check that throws BEFORE recording; pending now
produces exactly one row with `success: false` and
`error_code: 'pending_unsupported'`. Latent today (no Phase 1
backend emits pending) but ships in the contract.
**F5 (Important) — `isReadOnlyMode()` gate.** `flush()` and
`cleanup()` now short-circuit on `isReadOnlyMode()`, mirroring
`resources/analytics/write.ts:643-650`. Without this, read-only
nodes would log put/remove failures every 10s indefinitely.
**F6 (Important) — Async put rejection logging.** `Promise.allSettled`
was awaited but its results were discarded — async LMDB rejections
were silently swallowed (sync throws were already caught and
logged per-record). Iterate the settled results and `log.warn`
on each rejection so billing-visible record losses are observable.
**F13 (Important) — Stream completion flag.** `#wrapStream`'s
`finally` block recorded `success=true` whenever no exception
fired — including when the consumer `break`s the for-await loop
early. Added a `completed` flag set only after the inner for-await
exits normally; early termination now records `success=false` with
`error_code: 'aborted'`. The model did real work the caller didn't
consume; logging that as success poisoned billing aggregates.
**F11 (Nice-to-have) — Remove unregistered config key.** The writer
called `envGet('analytics.modelCallRetentionDays')` to override
retention, but the key isn't registered in `CONFIG_PARAM_MAP`, so
`getConfigValue` silently returned undefined. Removed the dead
envGet call; 90-day default is hardcoded with a TODO comment noting
that operator-tunable retention will land in Phase 2 alongside the
YAML→registry bootstrapper that owns models.* config.
**F12 (Nice-to-have) — id spread order.** Flipped `{ id: ...,
...record }` to `{ ...record, id: getNextMonotonicTime() }` so a
record that accidentally carries an `id` field can't override the
monotonic primary key. Defense-in-depth; no current caller hits
this.
Test updates:
- New: capability error writes a record (F2).
- New: pending result writes exactly ONE record (F4 — was silently a
double-write before).
- New: ModelBackendNotFoundError records with backend='unknown' (F2).
- New: stream consumer `break` records success=false + 'aborted' (F13).
- New: pre-call generateStream failure records (F2).
- New: async put rejection (Promise.reject from primaryStore.put)
doesn't silently swallow remaining records (F6).
- Existing tests updated where contract widened (capability test now
asserts both the throw AND the record).
Coverage on `resources/models/`: 94.5% statements / 95.2% branches
(up from 94/92.6). Models.ts at 100% statements. All 19 Models.test
+ 12 analyticsTable.test + 8 backendRegistry.test + 11 TestBackend.test
pass.
NOT addressed in this commit (deferred):
- F7 PARTIAL (concurrent-flush backpressure hardening). Best-effort
posture acknowledged in file header; matches analytics/write.ts.
- F8 NOISE (stale _table cache). Matches getRawAnalyticsTable
pattern; system-DB recycling not a supported runtime op.
- F9 NOISE (eager singleton intervals). .unref()'d; matches
existing analytics writer.
- F10 NOISE (async-gen abandonment without .return()). JS-spec
violation by consumer; for-await consumers are safe.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0 commit comments