@@ -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
124151The request-log owner (` src/server/request-log.ts:150-154,218-246 ` ) must add per-entry
125152UTF-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
128157Model cache and usage summary values receive owner-local byte accounting before they can
129158register. Usage summary overflow aggregates excess model cardinality into an ` other `
130159bucket 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
172249appOwnedBytes : 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+
219306Extend ` 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
232320Run:
233321
234322``` bash
235323bun 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
236326bun run typecheck
237327bun run test
238328bun 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