Skip to content

Commit b4393ad

Browse files
committed
docs(plan): anchor the byte budget to the delivered hooks and real boundaries
Three audit rounds against the post-035 tree rewrote the stale halves of the observability plan: registrations become named adapters over the hook shapes that actually shipped, the config field lands on /api/settings because /api/config PUT is disabled, and the usage 'other' bucket must bound the per-day model maps as well as the top-level one. The phase also absorbs three verified external findings as pre-work — the unbounded snapshot retry, stub-before-resident demotion, and the unwired GCP expiry sweep — with the persistence follow-up contract locked to one bounded same-path pass at shutdown.
1 parent 3a0b17b commit b4393ad

1 file changed

Lines changed: 107 additions & 12 deletions

File tree

devlog/_plan/260801_zero_leak_state_stores/040_app_bytes_observability.md

Lines changed: 107 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,32 @@ water counters but never evicted by this budget. Their hard admission is owned b
1919

2020
## Current code and anchors
2121

22-
- `src/server/management/system-routes.ts:1-31` documents scalar/privacy constraints.
23-
- `src/server/management/system-routes.ts:33-95` assembles process memory,
22+
- `src/server/management/system-routes.ts:1-30` documents scalar/privacy constraints.
23+
- `src/server/management/system-routes.ts:35-97` assembles process memory,
2424
`responseState`, inspector counters, watchdog, and active-turn scalars.
25-
- `src/responses/state.ts:371-408` is an existing observe-only retained-store seam.
25+
- `src/responses/state.ts:640-695` is the observe-only retained-store seam
26+
(`responseStateMetrics()` — resident/stub/tombstone counts, payload bytes, spill
27+
counters from 010). It does NOT yet expose `evictableBytes`, `pinnedBytes`, or the
28+
oldest-resident timestamp; this phase adds a resident-only snapshot/eviction API
29+
(see the continuation pre-work section below).
2630
- `src/server/memory-watchdog.ts:53-60,102-155` owns a separate warn-only RSS/native
2731
watchdog with a 360-sample bounded ring; it does not manage app-owned state.
2832
- `src/types.ts:531-725` contains the top-level `OcxConfig` runtime fields.
29-
- `src/config.ts:703-733` begins the Zod config schema and reaches adjacent scalar
30-
fields; write-time validation follows
31-
the existing positive-integer helpers at `:530-550`.
32-
- `src/server/index.ts:300-316` starts process-wide memory/scheduler singletons.
33-
- `tests/memory-watchdog.test.ts:162-233` pins the current endpoint scalar shape.
33+
- `src/config.ts:739-748` begins the Zod config schema; positive-integer helpers are
34+
at `:566-585`. Load-time degradation and write-time rejection are SEPARATE
35+
boundaries: the schema rule degrades malformed persisted edits to the default, and
36+
`validateConfigCandidate` (`src/config.ts:1438-1444`) rejects invalid candidates.
37+
The actual MANAGEMENT boundary is `/api/settings` PUT
38+
(`src/server/management/config-routes.ts:74-76,192-227`) — full `/api/config` PUT
39+
is disabled — so this phase adds `appOwnedMemoryBudgetMb` to `/api/settings`
40+
GET/PUT: the PUT validates the integer 64..4096 range, persists, then calls
41+
`configureAppOwnedMemoryBudget` + `enforceAppOwnedMemoryBudget` synchronously.
42+
Tests extend `tests/settings-stream-mode.test.ts`.
43+
- `src/server/index.ts:264-273,311-317` starts process-wide singletons: state
44+
reconciliation at 266/273, watchdog at 316, state-store sweeper at 317. The
45+
app-owned registrations + first enforcement run belong beside the sweeper start.
46+
- `tests/memory-watchdog.test.ts:162-236` pins the current endpoint scalar shape
47+
(11 responseState scalars, no appOwnedBytes yet).
3448

3549
## Config decision
3650

@@ -112,7 +126,20 @@ store as zeros; it never invokes `evictOldest()`.
112126

113127
## Retained-store registrations
114128

115-
Register hooks delivered by 010/020/035 and existing owners:
129+
Register hooks delivered by 010/020/035 and existing owners. The delivered 035 hook
130+
shapes use `entries` (not `count`) and omit pinned/evictable fields, so each
131+
registration is a NAMED ADAPTER in `app-owned-memory` registration code mapping the
132+
owner hook onto `RetainedStoreSnapshot` (rings: `evictableBytes = bytes`,
133+
`pinnedBytes = 0`). Delivered hooks:
134+
135+
- `debugBufferMetrics` / `evictOldestDebugEntryForBudget` (`src/lib/debug-log-buffer.ts:65-70`)
136+
- `injectionBufferMetrics` / `evictOldestInjectionEntryForBudget` (`src/lib/injection-debug-log.ts:43-48`)
137+
- `claudeInboundDebugMetrics` / `evictOldestClaudeInboundForBudget` (`src/claude/inbound-debug.ts:155-160`)
138+
- `crashRingMetrics` / `evictOldestCrashTraceForBudget` (`src/lib/crash-guard.ts:277-282`)
139+
- caches: `src/adapters/anthropic-image-normalize.ts:216-235`,
140+
`src/vision/index.ts:119-133`, `src/adapters/google-antigravity-replay.ts:220-247`
141+
- blobs: `src/adapters/cursor/native-exec.ts:84-134,396-421` (provenance/pin classes
142+
map directly onto pinned/evictable)
116143

117144
| Category | Store ids | Demotion rule |
118145
|---|---|---|
@@ -123,11 +150,61 @@ Register hooks delivered by 010/020/035 and existing owners:
123150

124151
The request-log owner (`src/server/request-log.ts:150-154,218-246`) must add per-entry
125152
UTF-8 byte accounting and a centralized oldest delete. Normalize individual retained
126-
diagnostic strings per 035, but preserve retry/failover attempt structure.
153+
diagnostic strings per 035, but preserve retry/failover attempt structure. The
154+
current mutation anchor is `src/server/request-log.ts:244-246` (push/shift only, no
155+
byte hook yet).
127156

128157
Model cache and usage summary values receive owner-local byte accounting before they can
129158
register. Usage summary overflow aggregates excess model cardinality into an `other`
130159
bucket without dropping token/cost totals; it is not permissible to delete totals.
160+
Current owners: `src/codex/model-cache.ts:43-44,133-135` (unaccounted model arrays)
161+
and `src/server/management/logs-usage-routes.ts:82-86,207-211` +
162+
`src/usage/summary.ts:415-417` (unaccounted summaries, unbounded model breakdown —
163+
the `other` bucket does not exist yet and is created in this phase). The `other`
164+
contract covers BOTH the top-level `models` aggregation (`src/usage/summary.ts:415-417`)
165+
AND every per-day `days[].models` breakdown (`src/usage/summary.ts:290-337`), each of
166+
which independently builds an unbounded provider/model map; unique request counts,
167+
attempts, tokens, and cost are preserved in the bucket wherever applicable.
168+
169+
## Continuation pre-work (fold-in of verified external findings)
170+
171+
Three verified defects sit exactly on this phase's continuation/sweeper seam and are
172+
repaired here BEFORE the budget work builds on them:
173+
174+
1. **Bounded snapshot retry (state.ts:448-487, VALID High).** `persistNow()` loops
175+
until `revision === stateRevision`; sustained traffic keeps it spinning and
176+
`flushResponseState()` (:511-518) never settles at shutdown. Fix: cap the rewrite
177+
loop at 4 attempts. If the final write is still revision-unstable, schedule a
178+
follow-up flush and DO NOT drain `pendingSpillUnlinks` — only a revision-stable
179+
snapshot may authorize unlinking superseded spill generations (:488-493).
180+
Follow-up contract: the follow-up retains the SAME captured `path` (the guard at
181+
:501-508 against recomputing `snapshotPath()` stays intact). Background
182+
persistence uses an unref'd timer. Explicit `flushResponseState()` (shutdown path,
183+
`src/server/lifecycle.ts:164-166`) AWAITS one bounded same-path follow-up pass
184+
after the cap; if that pass is still unstable it returns with a best-effort
185+
snapshot and intact pending unlinks — shutdown is never blocked indefinitely.
186+
Test: revision churn during atomic write settles within the bound and leaves
187+
pending unlinks intact until a stable snapshot lands.
188+
189+
2. **Resident-first demotion (state.ts:539-556, VALID High/data loss).** The RAM-cap
190+
loop deletes the oldest spill stub/tombstone (including its durable spill file,
191+
:131-143) whenever it precedes a resident. Fix: scan for the oldest RESIDENT and
192+
demote it first; delete stubs/tombstones only when no resident remains and
193+
bounded metadata alone exceeds the cap. This also makes the 040 continuation
194+
`evictOldest()` callback resident-only by construction.
195+
Test: mixed older-stub/newer-resident state demotes the resident and keeps the
196+
stub's durable spill file on disk.
197+
198+
3. **GCP ADC expiry sweep unwired (VALID Medium).** `sweepExpiredGcpAdcTokens()`
199+
(`src/lib/gcp-adc.ts:71-80`) is exported but `STATE_STORE_REGISTRATIONS`
200+
registers only `reconcileGcpAdcTokens` (`src/lib/state-store-registrations.ts:97`).
201+
Wire the expiry sweep into the registration's TTL callback. Test: expired ADC
202+
token is swept by the periodic pass.
203+
204+
A fourth external claim (sweeper partial-pass fence dropping newly-added-owner
205+
writes) was audited INVALID against current source — every fenced writer also
206+
accepts keys in the owner's live-key set — but a partial-failure/live-key regression
207+
test is added to pin that property.
131208

132209
## Enforcement algorithm
133210

@@ -166,7 +243,7 @@ Edge contracts:
166243

167244
## `/api/system/memory` payload
168245

169-
At `src/server/management/system-routes.ts:74-95`, add:
246+
At `src/server/management/system-routes.ts:76-97` (beside `responseState` at :90), add:
170247

171248
```ts
172249
appOwnedBytes: appOwnedBytesSnapshot(),
@@ -216,6 +293,16 @@ Add `tests/app-owned-memory.test.ts`:
216293
- `translator and serialized-tail observations never invoke budget eviction`
217294
- `budget decrease enforces synchronously in the documented order`.
218295

296+
Continuation pre-work tests (extend `tests/responses-state.test.ts` and
297+
`tests/state-store-sweeper.test.ts` / `tests/gcp-adc.test.ts`):
298+
299+
- `persistNow settles within the bounded rewrite attempts under revision churn`
300+
- `unstable final snapshot defers spill unlinks until a stable snapshot`
301+
- `RAM cap demotes the oldest resident before deleting any older spill stub`
302+
- `stub-only over-cap state still deletes bounded metadata oldest-first`
303+
- `expired GCP ADC token is removed by the periodic sweep registration`
304+
- `partial reconcile failure keeps live-key writes accepted for new owners`.
305+
219306
Extend `tests/memory-watchdog.test.ts`:
220307

221308
- `GET system memory includes privacy-safe appOwnedBytes scalars`
@@ -226,13 +313,16 @@ Config tests:
226313

227314
- `appOwnedMemoryBudgetMb defaults to 256 MiB`
228315
- `accepts integer bounds 64 and 4096`
229-
- `rejects management writes below above fractional or nonnumeric values`
316+
- `settings PUT rejects below/above/fractional/nonnumeric budget values`
317+
- `settings PUT applies a valid budget change synchronously through enforcement`
230318
- `malformed persisted value degrades to default without dropping providers`.
231319

232320
Run:
233321

234322
```bash
235323
bun test tests/app-owned-memory.test.ts tests/memory-watchdog.test.ts tests/config.test.ts
324+
bun test tests/responses-state.test.ts tests/state-store-sweeper.test.ts \
325+
tests/gcp-adc.test.ts tests/settings-stream-mode.test.ts
236326
bun run typecheck
237327
bun run test
238328
bun run privacy:scan
@@ -251,4 +341,9 @@ bun run privacy:scan
251341
- No pin override for live remote Cursor blobs.
252342
- No path/id/account/provider/model/prompt/tool/error content in observability.
253343
- No 030 dependency or accounting hook; expiration sweeping stays independent.
344+
(Exception: the GCP expiry-sweep wiring above touches the 030 registration table
345+
because the defect lives there; it adds no accounting coupling.)
254346
- No GUI redesign beyond consuming the additive payload if desired in docs sync.
347+
348+
Docs sync for the new config field covers English AND the translated configuration
349+
references (`ja`, `ko`, `ru`, `zh-cn`).

0 commit comments

Comments
 (0)