From 1d7099328cdd070cc11057955f2cb70ab17f65ab Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Fri, 21 Aug 2026 23:46:12 +0900 Subject: [PATCH 1/2] devlog: vision external-backend roadmap (160-190) under sidecar-selection unit --- .../160_vision_external_research.md | 110 ++++++++++++++++++ .../170_vision_backend_union.md | 75 ++++++++++++ .../180_vision_describe_executors.md | 52 +++++++++ .../190_vision_surfaces_and_delivery.md | 38 ++++++ 4 files changed, 275 insertions(+) create mode 100644 devlog/_plan/260820_sidecar_selection_unification/160_vision_external_research.md create mode 100644 devlog/_plan/260820_sidecar_selection_unification/170_vision_backend_union.md create mode 100644 devlog/_plan/260820_sidecar_selection_unification/180_vision_describe_executors.md create mode 100644 devlog/_plan/260820_sidecar_selection_unification/190_vision_surfaces_and_delivery.md diff --git a/devlog/_plan/260820_sidecar_selection_unification/160_vision_external_research.md b/devlog/_plan/260820_sidecar_selection_unification/160_vision_external_research.md new file mode 100644 index 0000000000..34ae854f80 --- /dev/null +++ b/devlog/_plan/260820_sidecar_selection_unification/160_vision_external_research.md @@ -0,0 +1,110 @@ +# 160 — Vision external-backend research (xai Grok / Antigravity Gemini describers) + +Continuation of #2188. Web-search shipped four external backends (L6-L9, docs +060-090); the vision sidecar still dispatches only openai-forward and +anthropic-OAuth. GUI evidence: the vision dropdown lists only Codex/Claude +rows while the web-search dropdown already lists Grok/Gemini. + +## Current vision dispatch inventory + +- Types: `OcxVisionSidecarConfig.backend?: "openai" | "anthropic"` (src/types.ts). +- Union: `VisionSidecarBackend` (src/vision/eligibility.ts:30) — 2 arms. +- Candidate mapping: `visionBackendForCandidate` (eligibility.ts:150-165) — + native/openai → openai; anthropic only via the resolved OAuth provider name. +- Options: `visionEligibleModelOptions` (eligibility.ts:201+) iterates + `["openai","anthropic"] as const` and injects `BASELINE_VISION_MODELS`. +- Enabled backends: `enabledVisionBackends` + (src/server/management/vision-sidecar-options.ts:31-43); empty-auth fallback + returns both universal sides. +- Write gate: `visionDescriberIsProvablyBlind` (vision-sidecar-options.ts:94+) + probes ONLY the openai/anthropic vendor tables. +- PUT validation: config-routes.ts:594-596 rejects backends outside the two + literals; hint fall-through at :623; claude-code override near :738-740. +- Runtime plan: `planVisionSidecar` (src/vision/index.ts) — anthropic arm and + openai-forward arm only. `resolveVisionBackend`: explicit > anthropic-if-auth + > openai. +- GUI: `SidecarBackend = "openai" | "anthropic"` (gui/src/pages/ + dashboard-shared.ts:62, claude-manual-env.ts:8). NOTE: this type is shared + with WebSearchModelOption and is ALREADY stale — the server emits + xai/gemini/exa web rows today. + +## Wire research (from shipped web-search executors, probe-verified 2026-08-20/21) + +### xai describe wire + +Mirror src/web-search/xai-executor.ts: POST `https://api.x.ai/v1/responses` +(origin pinned; provider baseUrl honored only on same origin), stored OAuth +bearer via `getValidAccessToken`, `redirect: "manual"`. Body for describe: + +```json +{ + "model": "", + "instructions": "", + "input": [{ "role": "user", "content": [ + { "type": "input_text", "text": "" }, + { "type": "input_image", "image_url": "" } + ]}], + "reasoning": { "effort": "" }, + "stream": true +} +``` + +SSE reduction: reuse the `response.output_text.delta` / `.done` handling +shape from parseXaiResponsesSSE, without the citation/source machinery. +Grok Responses accepts `input_image` with data URLs (same shape the OpenAI +forward describer already posts — describe.ts builds input_image parts). + +### Gemini (Antigravity CCA) describe wire + +Mirror src/web-search/gemini-executor.ts: POST +`{registry base}/v1internal:generateContent`, `ANTIGRAVITY_REQUEST_UA`, +token + projectId via `getValidAccessTokenSnapshot`, envelope: + +```json +{ + "model": "", + "userAgent": "antigravity", "requestType": "agent", + "project": "", "requestId": "agent-", + "request": { + "systemInstruction": { "role": "user", "parts": [{ "text": "" }] }, + "contents": [{ "role": "user", "parts": [ + { "text": "" }, + { "inlineData": { "mimeType": "", "data": "" } } + ]}] + } +} +``` + +inlineData shape matches src/adapters/google.ts:972/:1233. Response mapping: +`candidates[0].content.parts[].text` join (mapCcaGroundedResponse shape, +minus grounding). https: image URLs cannot be inlined without proxy-side +fetch — REJECTED for gemini describe (data: URLs only, documented delta, +same stance as anthropic-describe's stricter base64 rule). + +## Metadata facts + +- xai vendor table: bare grok-2/grok-3/grok-4 are `text`-only; grok-4.x + fast/4.3/4.5/4.6 and grok-2-vision are `text,image`. +- No bare model id collides across the four vendor tables (openai 48, + anthropic 26, xai 32, google 43; collision scan 2026-08-21: zero) — the + "vendor tables never disagree" premise of visionDescriberIsProvablyBlind + survives widening to four families. + +## Audit deltas folded into this unit (sol-medium audit, 2026-08-21) + +- **Blocker A**: `BASELINE_VISION_MODELS` is a TOTAL + `Record`; widening the union without a + decision breaks typecheck. Decision → doc 170: baselines become + descriptor-owned (only openai/anthropic carry one). +- **Blocker B**: `visionDescriberIsProvablyBlind` collapses non-anthropic + hints to openai and probes two families; a bare grok id absent from + candidates would slip the gate. Decision → doc 170: probe all four vendor + families. +- Empty-auth fallback stays `["openai","anthropic"]` — never offer + xai/gemini unauthenticated. +- GUI shared `SidecarBackend` must split (web-search has exa; vision does + not). +- New executors: `sidecarEnter("vision")` (NOT "web-search"), + `signalWithTimeout` + `cancelBodyOnAbort`, `redactSecretString` on all + error paths, timeout-bounds.ts as single authority. + diff --git a/devlog/_plan/260820_sidecar_selection_unification/170_vision_backend_union.md b/devlog/_plan/260820_sidecar_selection_unification/170_vision_backend_union.md new file mode 100644 index 0000000000..5e071e8049 --- /dev/null +++ b/devlog/_plan/260820_sidecar_selection_unification/170_vision_backend_union.md @@ -0,0 +1,75 @@ +# 170 — Backend union + descriptor table (wp2 implementation cycle) + +Depends on: 160. Implements #2188 vision rules for xai/gemini; resolves audit +Blockers A and B. + +## Design decision: VISION_BACKENDS descriptor table + +Mirror WEB_SEARCH_BACKENDS as a SIBLING table (audit Q2) in a new +`src/vision/backends.ts`: + +```ts +interface VisionBackendDescriptor { + backend: VisionSidecarBackend; // "openai" | "anthropic" | "xai" | "gemini" + isActive(auth: SidecarAuthState, config: OcxConfig): boolean; + candidateMatch(candidate: VisionCandidateModel, auth: SidecarAuthState): boolean; + baseline?: string; // only openai/anthropic carry one + rank: number; // stable option ordering +} +``` + +- openai: isActive = auth.isCodexAuth-shaped predicate already used by + enabledVisionBackends (listOpenAiForwardSidecarCandidates > 0); baseline + gpt-5.6-luna; rank 0. +- anthropic: isActive = anthropicSidecar resolved; candidateMatch = + provider === auth.anthropicProviderName; baseline claude-haiku-4-5; rank 1. +- xai: isActive = same predicate as WEB_SEARCH_BACKENDS xai (enabled oauth + "xai" provider + active account !needsReauth); candidateMatch = + candidate.provider === "xai"; NO baseline; rank 2. +- gemini: isActive = Antigravity OAuth + projectId (same as web-search); + candidateMatch = provider === "google-antigravity"; NO baseline; rank 3. +- Empty-auth fallback (no side active): ["openai","anthropic"] only — + xai/gemini are never offered unauthenticated (audit Q1 gap). + +## Blocker A resolution — baselines + +`BASELINE_VISION_MODELS` stays a record of exactly the two universal sides: +type becomes `Partial>` sourced from +descriptor.baseline. visionEligibleModelOptions iterates descriptors (not the +hardcoded 2-tuple), injecting a baseline row only when descriptor.baseline is +set and that side isActive. + +## Blocker B resolution — provably-blind gate + +`visionDescriberIsProvablyBlind` widens its vendor probe from +{openai, anthropic} to {openai, anthropic, xai, google} via +resolveMetadataProvider. Collision scan (160) proved no bare id is shared +across the four tables, so "any positive text-only verdict wins" stays sound. +Regression test: PUT model=grok-4 (bare, text-only in xai table, absent from +candidates) must 400; PUT model=grok-4.3 with xai auth must 200. + +## Files touched (wp2) + +- src/vision/backends.ts (new): descriptor table + sidecarVisionBackends() + helper returning active descriptors. +- src/vision/eligibility.ts: union widens; BASELINE_VISION_MODELS type; + visionBackendForCandidate delegates to descriptor candidateMatch (keeps + signature; gains optional auth arg via new overload consumed by options + path); visionEligibleModelOptions iterates descriptors ranked. +- src/types.ts: OcxVisionSidecarConfig.backend union widens. +- src/server/management/vision-sidecar-options.ts: enabledVisionBackends + delegates to descriptors; visionDescriberIsProvablyBlind four-family probe. +- src/server/management/config-routes.ts: PUT gate literals :594-596, hint + fall-through :623, claude-code override :738-740 — all widen to the union. +- tests: sidecar-settings-vision-filter.test.ts, vision-eligibility.test.ts, + sidecar-settings-vision-controls.test.ts extended; new fixture with xai + + antigravity oauth accounts (pattern from web-search-backend-union.test.ts). + +## Not in wp2 + +Executors (180) — planVisionSidecar keeps its current arms; a persisted +xai/gemini backend without an executor cannot be SELECTED at runtime yet, so +wp2 lands options+gate first with resolveVisionBackend still collapsing +unknown-to-executor backends to the legacy default order. planVisionSidecar +gains its arms in wp3 in the same push train (dev gets both before release). + diff --git a/devlog/_plan/260820_sidecar_selection_unification/180_vision_describe_executors.md b/devlog/_plan/260820_sidecar_selection_unification/180_vision_describe_executors.md new file mode 100644 index 0000000000..9a66b6ab7c --- /dev/null +++ b/devlog/_plan/260820_sidecar_selection_unification/180_vision_describe_executors.md @@ -0,0 +1,52 @@ +# 180 — Describe executors + runtime dispatch (wp3 implementation cycle) + +Depends on: 170. + +## src/vision/xai-describe.ts (new) + +Mirror xai-executor.ts scaffolding: pinned https://api.x.ai origin, +getValidAccessToken("xai"), redirect "manual", fetchWithResetRetry, +signalWithTimeout(settings.timeoutMs) + cancelBodyOnAbort, +sidecarEnter("vision"), redactSecretString on every error path. Body: 160 +wire. Non-stream preferred if probe allows (stream:false) — else reduce SSE +output_text deltas. validateImageUrl reused from describe.ts (data: allowed +mimes + 20MB cap, https passthrough). Reasoning: settings.reasoning passes +through as reasoning.effort only for low|medium|high; xhigh/max clamp to high +(xai ladder). Returns DescribeOutcome, never throws. + +## src/vision/gemini-describe.ts (new) + +Mirror gemini-executor.ts: registry-pinned base, ANTIGRAVITY_REQUEST_UA, +getValidAccessTokenSnapshot (token + projectId), CCA envelope from 160 with +inlineData part; resolveAntigravityEffortWireModel(settings.model, +settings.reasoning, base) for wire model + thinkingLevel; readBoundedResponseBytes; +data: URLs only (https rejected with explicit error, documented delta); +sidecarEnter("vision"); redactSecretString. Returns DescribeOutcome. + +## Runtime dispatch (src/vision/index.ts) + +- VisionPlan gains backend arms: { backend: "xai", xaiSidecar: {providerName, + provider} } and { backend: "gemini", geminiSidecar: {...} } following the + anthropicSidecar shape. +- planVisionSidecar: after resolving cfg.backend, arms for xai/gemini require + their descriptor isActive (else fall through to legacy resolution — a + persisted xai backend with expired auth degrades exactly like anthropic + without OAuth: sidecar unavailable marker, never a crash). +- resolveVisionBackend: explicit backend honored for all four; DEFAULT + (unset) order unchanged: anthropic-if-auth else openai. No default drift. +- executeDescription: two new arms calling the new executors. +- descriptionIdentity: backend already part of the cache key; reasoning is + keyed only for openai — include it for xai too (effort affects output); + gemini keys thinkingLevel via model+reasoning inputs. +- resolveEffectiveVisionModel: per-backend defaults — xai: grok-4.3, + gemini: gemini-3.7-flash (both text,image in metadata); existing openai/ + anthropic defaults unchanged. + +## Tests (wp3) + +- vision-xai.test.ts, vision-gemini.test.ts (new): executor wire shape + (mocked fetch), error taxonomy, redaction, data-URL validation, effort + clamp/wire-model mapping. +- vision-sidecar-e2e.test.ts: plan arms for xai/gemini with oauth fixtures; + degraded no-auth path. + diff --git a/devlog/_plan/260820_sidecar_selection_unification/190_vision_surfaces_and_delivery.md b/devlog/_plan/260820_sidecar_selection_unification/190_vision_surfaces_and_delivery.md new file mode 100644 index 0000000000..11c929d876 --- /dev/null +++ b/devlog/_plan/260820_sidecar_selection_unification/190_vision_surfaces_and_delivery.md @@ -0,0 +1,38 @@ +# 190 — Surfaces, live proof, delivery (wp4 cycle) + +Depends on: 180. + +## GUI + +- Split the shared SidecarBackend (dashboard-shared.ts:62): web-search side + keeps its server-provided backend strings (already emits xai/gemini/exa — + stale type fixed by the split); vision side gets + VisionBackend = "openai" | "anthropic" | "xai" | "gemini". +- visionSidecarBackendForModel fallback stays server-provenance-first; + catalog inference (anthropic-vs-openai guess) only for legacy rows. +- claude-manual-env.ts SidecarOverride backend union widens for vision. +- No new dropdown UI: options arrive from visionModels server list already. + +## CLI + +- src/cli/agent.ts: usage already names xai|gemini; verify backend values + pass through PUT unvalidated client-side (server gate authoritative); + vision --list renders new backends' rows. + +## Live proof (acceptance 3-5) + +- GET /api/sidecar-settings on live :10100 shows visionModels containing + xai/gemini rows (auth present on this machine for both — web-search rows + prove it). +- PUT vision {backend:"xai", model:"grok-4.3"} → 200; PUT model grok-4 + (bare) → 400 provably-blind; restore original settings after proof. +- GUI screenshot of the vision dropdown listing Grok/Gemini rows. + +## Delivery + +- Small commits per layer (backends table / eligibility+gate / executors / + GUI+CLI / tests+devlog), full bun run typecheck + bun run test green at + final head, push directly to dev (user-authorized, no PR). +- devlog docs 160-190 land with the same push train; unit stays in _plan + until the release train closes it. + From 7a0fb255dd951b7696a2f5dbb411f3588788bc7f Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Sat, 22 Aug 2026 00:16:33 +0900 Subject: [PATCH 2/2] =?UTF-8?q?devlog:=20bun=201.4=20follow-up=20memory=20?= =?UTF-8?q?roadmap=20(000-040)=20=E2=80=94=20research=20ledger,=20diagnost?= =?UTF-8?q?ics/GC-relief/smol-worker=20plans,=20macmini=20measurement=20pr?= =?UTF-8?q?otocol?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../000_plan.md | 18 ++++ .../000_research.md | 71 ++++++++++++ .../010_memory_diagnostics.md | 74 +++++++++++++ .../020_watchdog_gc_relief.md | 101 ++++++++++++++++++ .../030_smol_workers.md | 45 ++++++++ .../040_macmini_measurement.md | 28 +++++ 6 files changed, 337 insertions(+) create mode 100644 devlog/_plan/260822_260822-bun14-followup-memory/000_plan.md create mode 100644 devlog/_plan/260822_260822-bun14-followup-memory/000_research.md create mode 100644 devlog/_plan/260822_260822-bun14-followup-memory/010_memory_diagnostics.md create mode 100644 devlog/_plan/260822_260822-bun14-followup-memory/020_watchdog_gc_relief.md create mode 100644 devlog/_plan/260822_260822-bun14-followup-memory/030_smol_workers.md create mode 100644 devlog/_plan/260822_260822-bun14-followup-memory/040_macmini_measurement.md diff --git a/devlog/_plan/260822_260822-bun14-followup-memory/000_plan.md b/devlog/_plan/260822_260822-bun14-followup-memory/000_plan.md new file mode 100644 index 0000000000..ddc2d178b1 --- /dev/null +++ b/devlog/_plan/260822_260822-bun14-followup-memory/000_plan.md @@ -0,0 +1,18 @@ +# 000_plan — unit map + +- 000_research.md — claim ledger + gap analysis +- 010_memory_diagnostics.md — extraMemorySize observability (PR parent, base dev) +- 020_watchdog_gc_relief.md — measurement-FIRST GC evaluation (Phase A harness), + conditional idle-gated production hook (Phase B) per the 260731 gate +- 030_smol_workers.md — smol:true gated on per-worker large-fixture A/B +- 040_macmini_measurement.md — live measurement protocol (feeds 020 Phase A) + +Stack shape: PR-A(010, base dev) → PR-B(020 Phase A harness + evaluation, +base PR-A head) → conditional PR for Phase B only on gate PASS; +PR-C(030, base dev, lands per-call-site with A/B evidence). +One decade doc = one work-phase = one PABCD cycle (LOOP-UNIT-CHAIN-01). +Audit round 1: FAIL (4 findings) → docs revised: 020 restructured +measurement-first honoring 260731_macos_rss_retention/040_allocator_residual +gate; 010 static-import sync seam; 030 pre-landing A/B gate; separate +lastReliefAt. See ledger. + diff --git a/devlog/_plan/260822_260822-bun14-followup-memory/000_research.md b/devlog/_plan/260822_260822-bun14-followup-memory/000_research.md new file mode 100644 index 0000000000..1e4462c5c7 --- /dev/null +++ b/devlog/_plan/260822_260822-bun14-followup-memory/000_research.md @@ -0,0 +1,71 @@ +# 000 — Bun 1.4 follow-up memory patches: research and claim ledger + +Date: 2026-08-22 +Unit: 260822_260822-bun14-followup-memory +Question: from today's viewpoint (bundled Bun 1.4.0, released 2026-08-19), which +ADDITIONAL memory patches are possible and worthwhile in opencodex? + +## Method + +Luna 5-lane discovery swarm (official releases / GitHub issues+PRs / JSC-runtime / +community / server-SSE-proxy), then Tier-2 proof by the main agent via `gh api` +against oven-sh/bun. App-side baseline re-audited against +devlog/_fin/260813_bun_canary_dogfood/050_memory_patch_roadmap.md and current src/. + +## Claim ledger (Tier-2 proven unless noted) + +| # | Claim | Proof | Status | +|---|---|---|---| +| C1 | No Bun 1.4.x patch release exists after v1.4.0 (2026-08-19). | `gh api repos/oven-sh/bun/releases` → latest tag `bun-v1.4.0`; bun-v1.4.1/2/3 404. | verified | +| C2 | Bun PR #36467 (TLS Bun.serve use-after-free on `server.stop(true)` sibling-socket close) merged 2026-07-31, sha 529adec09, and IS an ancestor of bun-v1.4.0 (`compare/bun-v1.4.0...sha` → status=behind). Already in our bundled runtime; no action. | gh api pulls/36467 + compare | verified | +| C3 | Bun PR #32662 (fetch: release buffered response body + error reader on streaming abort) merged 2026-07-22, sha 4b7241669, ancestor of v1.4.0. In bundled runtime. | gh api pulls/32662 + compare | verified | +| C4 | Bun PR #35093 (fetch: error body stream when fully-buffered response aborted) merged 2026-07-28, sha 789be97db, ancestor of v1.4.0. In bundled runtime. | gh api pulls/35093 + compare | verified | +| C5 | Bun issue #34917: `--max-old-space-size`, `BUN_JSC_gcMaxHeapSize`, `BUN_JSC_forceRAMSize` are NOT reliable heap caps on the 1.4 line; still OPEN (created 2026-07-21, closed:null). Container/OOM bounding must come from app-side watchdog + supervision, not JSC flags. | gh api issues/34917 | verified | +| C6 | `Bun.gc(true)` on 1.4 asks JSC to collect AND asks mimalloc to release fragmented non-JS pages (allocator shared with JSC since the 1.4 Rust/allocator work). | Bun docs (bun.com/reference/bun/gc) opened by L3; local probe `typeof Bun.gc === "function"` on 1.4.0. | verified (docs) | +| C7 | `bun:jsc` heapStats exposes `extraMemorySize`/`heapCapacity`; `Bun.unsafe.mimallocDump` exists on 1.4.0. | local probe on bundled 1.4.0: `{"heapSize":…,"heapCapacity":…,"extraMemorySize":…}`, mimallocDump:function | verified (executed) | +| C8 | `new Worker(url, {smol:true})` works on bundled 1.4.0 (selects JSC Small heap growth policy per Bun docs). | local probe: "smol worker OK" | verified (executed) | +| C9 | RSS retention after GC (issue #27514) and SSE-proxy reader-cancel segfault (#31159) were closed as DUPLICATES, not demonstrated fixed; #26321 (Windows file-stream RSS) duplicate-closed too. Continued A/B measurement remains necessary. | gh issue pages opened by L2/L5 | verified | +| C10 | Community: Bun 1.4 advertises up to ~35% memory reduction (allocator rewrite, thread-local page purging, lazy zeroing); no long-running independent RSS measurements yet. | Reddit announcements (L4), snippet-grade | lead | +| C11 | Medium post claims 1.4-era HTTP long-connection RSS still grew 280→340MB over 7 days; page returned 403. | unreachable | candidate — unverified | + +## App-side baseline (what is already done — do not re-patch) + +- 260813 roadmap patches #1–#4 ALL landed since: native-main hardened-identity LRU + (src/codex/native-main-claim.ts:25-33), installation-salt LRU + (src/lab/subject/installation-salt.ts:7-17), mode-hint capability LRU + (src/codex/features.ts:1097-1106), Lab ledger event-id process index REMOVED + (no `eventIdIndexByLedger` in src/lab/ledger/store.ts). +- `Bun.serve({ idleTimeout: 255 })` (src/server/index.ts:736) and per-request + `server.timeout(req, 0)` for streaming (src/server/responses/fetch-helpers.ts:113) + already implement the SSE-timeout guidance the swarm surfaced. +- eager-relay vs legacy-tee runtime gate: src/lib/bun-stream-caps.ts + (MIN_FIXED_BUN_VERSION="1.4.0"). +- Memory watchdog: warn-only ring sampler (src/server/memory-watchdog.ts), exposed at + /api/system/memory with bun:jsc heapSize/heapCapacity/objectCount. +- 36-store bounded-memory audit closed (devlog/_fin/260813…/050): only remaining + investigation is model-cache generation tombstones — needs an authority-token + redesign, NOT an eviction patch; excluded from this unit. + +## Gap analysis → patch set for THIS unit + +What Bun 1.4 newly makes possible, that opencodex does not use yet: + +1. **Diagnostics gap** — /api/system/memory and the watchdog ignore + `extraMemorySize` (JSC-visible native memory) and the watchdog samples carry no + JSC data at all. On 1.4, extraMemorySize is the counter that moved most + (external-memory reporting fixes #31422/#32653/#34142). → doc 010. +2. **Reclaim gap** — nothing in the tree ever calls `Bun.gc`. On 1.4 a full + `Bun.gc(true)` also purges mimalloc pages (C6) — the exact mitigation for the + "heap shrinks, RSS stays" pattern (#27514) that JSC flags cannot deliver (C5). + A config-gated, rate-limited watchdog relief hook is now worth having. → doc 020. +3. **Worker heap gap** — history/restore/policy workers are short-lived batch jobs; + `smol: true` (C8) bounds their JSC heap growth policy at a small perf cost, + reducing peak RSS during storage jobs. → doc 030. +4. **Proof gap** — every claim above is config/diagnostic-grade until measured. + macmini-cf (arm64, bun 1.3.14 installed → good A/B host) runs the live + measurement protocol. → doc 040. + +Explicit non-goals: no Bun runtime patching/fork (upstream 1.4.0 already carries +C2–C4); no JSC env-var "caps" (C5 proves them unreliable); no smol for the main +proxy process (throughput cost, unmeasured); no model-cache tombstone work. + diff --git a/devlog/_plan/260822_260822-bun14-followup-memory/010_memory_diagnostics.md b/devlog/_plan/260822_260822-bun14-followup-memory/010_memory_diagnostics.md new file mode 100644 index 0000000000..c7eeb2cd49 --- /dev/null +++ b/devlog/_plan/260822_260822-bun14-followup-memory/010_memory_diagnostics.md @@ -0,0 +1,74 @@ +# 010 — Memory diagnostics: extraMemorySize in samples and API + +Depends on: 000. Standalone PR (parent of the stack, targets dev). + +## Why + +Bun 1.4's biggest memory changes are external-memory reporting fixes +(#31422/#32653/#34142 per 260813 canary table). The counter that reflects them is +`heapStats().extraMemorySize` — JSC-visible native memory. Today +/api/system/memory reports jscHeap {heapSize, heapCapacity, objectCount} but NOT +extraMemorySize, and watchdog samples carry no JSC counters at all, so the exact +signal 1.4 improved is invisible in our 6h ring. + +## Changes + +### src/server/management/system-routes.ts +jscHeap block gains one field: +```diff + jscHeap = { + heapSize: stats.heapSize, + heapCapacity: stats.heapCapacity, + objectCount: stats.objectCount, ++ extraMemorySize: typeof stats.extraMemorySize === "number" ? stats.extraMemorySize : 0, + }; +``` +Type of local `jscHeap` widens accordingly. + +### src/server/memory-watchdog.ts — SYNC-SAFE seam (audit finding 4) + +defaultSample() is synchronous and MUST stay synchronous. Dynamic import is +async, so the seam is a STATIC import: this repository is Bun-native (AGENTS.md +runtime constraint — the proxy and `bun test` always run under Bun), so +`import { heapStats } from "bun:jsc"` at module top is justified; tsc strict +passes with the pinned Bun 1.4 types. The CALL is still guarded: + +```diff ++import { heapStats } from "bun:jsc"; + ... + export type MemorySampleBase = { + ... + arrayBuffers: number; ++ /** JSC heapStats().heapSize, when introspection is available. */ ++ jscHeapSize?: number; ++ /** JSC heapStats().extraMemorySize — JSC-visible native memory. */ ++ jscExtraMemorySize?: number; + }; + ... + function defaultSample(now: () => number): MemorySample { + const usage = process.memoryUsage(); ++ let jscHeapSize: number | undefined; ++ let jscExtraMemorySize: number | undefined; ++ try { ++ const stats = heapStats(); ++ jscHeapSize = stats.heapSize; ++ jscExtraMemorySize = stats.extraMemorySize; ++ } catch { /* introspection failure must never break sampling */ } + const base = { ..., jscHeapSize, jscExtraMemorySize }; +``` +observedMemoryCounter() UNCHANGED — thresholding remains rss/external/ +arrayBuffers. Observability only, no behavior change. Injected `opts.sample` +seam already lets tests supply samples without bun:jsc. + +### src/cli/doctor.ts +Service memory line appends `jscExtra=…` when the API returns +`body.jscHeap.extraMemorySize`. jsShare heuristic unchanged. + +## Tests +tests/memory-watchdog.test.ts: injected sample with jsc fields round-trips +through snapshot(); default sampler under bun test records numeric jsc fields. +system-routes test: /api/system/memory exposes jscHeap.extraMemorySize. + +## Measurement claim +None (diagnostics only) → goalplan c3 rationale: config/diagnostic-only. + diff --git a/devlog/_plan/260822_260822-bun14-followup-memory/020_watchdog_gc_relief.md b/devlog/_plan/260822_260822-bun14-followup-memory/020_watchdog_gc_relief.md new file mode 100644 index 0000000000..9ba054d4be --- /dev/null +++ b/devlog/_plan/260822_260822-bun14-followup-memory/020_watchdog_gc_relief.md @@ -0,0 +1,101 @@ +# 020 — GC relief: measurement-first evaluation, then gated production hook + +Depends on: 010 (diagnostics land first so the evaluation can read +extraMemorySize). THIS DOC LANDS NO PRODUCTION GC CALL BY ITSELF. + +## Prior-decision constraint (controlling) + +devlog/_fin/260731_macos_rss_retention/040_allocator_residual.md:139-161 bans +threshold/idle-triggered production `Bun.gc(true)` and defines the ONLY path +back: three fresh-process runs showing (a) ≥50% of post-load RSS growth gone by +60s after one GC, (b) repeatable across real workloads, (c) idle-only with +measured stop time and unchanged tail latency in a concurrent control, (d) +release-notes/API support on macOS. Written against Bun 1.3.x; Bun 1.4's +shared-allocator purge (000 C6) could flip the result — measure first. + +## Phase A (this unit): harness-only evaluation on Bun 1.4 + +### Child GC control channel (audit r2 finding 1) + +The measured proxy is a spawned child +(scripts/macos-rss-retention-harness.ts:626-638) that today only handles +SIGINT/SIGTERM (harness-child.ts:54-96) — no GC control exists. Add one: + +- scripts/macos-rss-retention-harness-child.ts: subscribe `process.on("SIGUSR2")`; + handler runs `const t0 = Bun.nanoseconds(); Bun.gc(true); const dur = + Bun.nanoseconds() - t0` and writes `{type:"gc", at:Date.now(), + durationMs:dur/1e6}` to stdout JSONL (same channel as "ready"). +- scripts/macos-rss-retention-harness.ts: after each load cell (outside the + latency-measurement window), `processHandle.kill("SIGUSR2")`, await the + `gc` event line (timestamped receipt), then take the +5s and +60s samples. +- GC duration evidence = the child-reported durationMs, not parent guesswork. +(SIGUSR2 is available on darwin/linux — this harness is darwin-targeted; +Windows is out of scope for it, matching the existing script name.) + +### Tail-latency control cells (audit r2 finding 2, r3 finding 1) + +The gate's criterion (c) needs a causally connected control WITHOUT +contaminating the RSS criterion (a). The two criteria use SEPARATE cell types: + +- RSS-retention cells: load stream → intervention (GC via SIGUSR2 with receipt, + or matched idle wait in the control arm) → process stays IDLE through the +5s + and +60s samples. No probe traffic; the +60s sample is pure post-GC idle + evidence for criterion (a). +- Latency cells (separate fresh-process runs): load stream → intervention → + identical POST-INTERVENTION probe stream in both arms; probe-stream p99 delta + (GC arm − control arm) ≤ max(5ms, 5%) is the oracle for criterion (c), with + the GC pause (child durationMs) reported explicitly. RSS numbers from these + cells are recorded but non-normative. + +Deliverable: numbers table in this unit (three fresh-process runs × matched +pairs, per 040 on macmini-cf and locally); verdict PASS/FAIL against the +260731 gate, criterion by criterion. + +## Phase B (conditional follow-up cycle, only on Phase-A PASS) + +- Idle gate: relief only when `getActiveTurnCount() === 0` + (src/server/lifecycle.ts:263 — existing export, no new seam needed). Defer + while busy; re-check next tick. +- Rate limit: OWN `lastReliefAt` (decoupled from lastWarnAt so warn cadence + never suppresses first relief), floor 30min. +- Config: restart-only startup configuration from the config file + (`memoryWatchdog: { gcRelief?: boolean; warnThresholdMb?: number }` in + OcxConfig). NOT in the /api/settings PUT allowlist; restart-only semantics + documented. warnThresholdMb validated at load: integer 256..65536, else + ignored+warn. +- Wiring chain (audit r2 finding 3 — all three layers named): + 1. src/server/index.ts:729 — `acquireServerBackgroundLifecycle(applyPolicy, + { memoryWatchdog: config.memoryWatchdog })`; + 2. src/server/background-lifecycle.ts:129-143 — + `acquireServerBackgroundLifecycle` gains the optional second param and + forwards it to `startProcessLoops(applyPolicy, opts)` (both the + first-owner branch and no change for the re-acquire branch: watchdog + options are first-owner-only, restart-only by definition); + 3. startProcessLoops passes `{ gcRelief, warnThresholdBytes, gc, isIdle }` + into startMemoryWatchdog. +- Test seam (audit r3 finding 2): StartServerDeps (src/server/index.ts:437) + gains an optional `memoryWatchdogDeps?: { gc?: () => void; sample?: () => + MemorySampleBase; now?: () => number; intervalMs?: number; isIdle?: () => + boolean }` forwarded through acquireServerBackgroundLifecycle alongside the + persisted config options into startMemoryWatchdog (deps override config- + derived defaults; production callers pass nothing). The startup integration + test injects gc spy + over-threshold sample + isIdle=true + short intervalMs + through this seam and asserts snapshot().gcRelief === true and the spy fired. + +- snapshot() exposes reliefCount, lastReliefAt, gcRelief. +- Windows caveat: mimalloc page scavenging disabled by design (#34181) — + relief mainly helps darwin/linux RSS. +- docs-site: troubleshooting page gains the new config keys (restart-only). + +## Tests (Phase B) +gcRelief on + above threshold + idle → gc called once; busy → deferred; +second tick within 30min → suppressed by lastReliefAt even when a warn fired +earlier; gc throwing → tick survives; gcRelief absent → never called; config +bounds validation; the startup integration test above. + +## Measurement claim +Phase A IS the measurement (040). Phase B lands only with that evidence +attached — goalplan c3 satisfied by construction. + + + diff --git a/devlog/_plan/260822_260822-bun14-followup-memory/030_smol_workers.md b/devlog/_plan/260822_260822-bun14-followup-memory/030_smol_workers.md new file mode 100644 index 0000000000..b1d3919b87 --- /dev/null +++ b/devlog/_plan/260822_260822-bun14-followup-memory/030_smol_workers.md @@ -0,0 +1,45 @@ +# 030 — smol workers: bounded JSC heap for storage/history batch workers + +Depends on: 000. Sibling PR (no shared files with 010/020) — but landing is +GATED on a local large-fixture A/B (audit finding 3). + +## Why + +history-job/restore-job/policy-job spawn short-lived Workers for batch work. +`smol: true` (probe-verified on bundled 1.4.0) selects JSC's Small heap growth +policy → lower peak RSS during storage jobs, at a GC-frequency cost. + +## Risk (audit finding 3 — must be measured before landing) + +These workers are NOT small-payload: policy cleanup materializes all archive +candidates (src/storage/policy.ts:347-379); cleanup snapshots full thread/log/ +memory/goal rows and serializes an aggregate backup +(src/storage/cleanup.ts:765-785,872-914,1171-1269); history reads complete +SQLite result sets with rollout buffers (src/codex/history-provider.ts:586-619, +709-732). smol is a growth-policy choice, not a payload bound — a large job +could GC-thrash or slow past the worker timeout. + +## Pre-landing gate: per-worker large-fixture A/B + +For each worker (history, restore, policy): build a large fixture (≥100MB +aggregate rows / large rollout set), run the job smol-off vs smol-on ×3, +record peak RSS (Subprocess/process sampling), elapsed wall time, completion +status. Acceptance to land each call site: completion success, elapsed within ++25% of baseline, peak RSS reduced. A worker failing the gate keeps its +full-size heap and the doc records the numbers — partial landing (subset of +the three call sites) is an acceptable outcome. + +## Changes (only for call sites that pass the gate) + +src/codex/history-job.ts:309, src/storage/restore-job.ts:170, +src/storage/policy-job.ts:303 — one-line `, { smol: true }` (pinned Bun 1.4 +types include smol; no cast needed per audit). + +## Tests +Existing worker suites stay green (smol changes GC policy, not messaging). +The A/B harness script + numbers are the landing evidence, committed into this +unit. + +## Measurement claim +Local A/B is the primary evidence (host-independent fixtures); macmini optional. + diff --git a/devlog/_plan/260822_260822-bun14-followup-memory/040_macmini_measurement.md b/devlog/_plan/260822_260822-bun14-followup-memory/040_macmini_measurement.md new file mode 100644 index 0000000000..6779d78e97 --- /dev/null +++ b/devlog/_plan/260822_260822-bun14-followup-memory/040_macmini_measurement.md @@ -0,0 +1,28 @@ +# 040 — macmini-cf live measurement protocol + +Depends on: 010 landed on a testable branch. Feeds 020 Phase A verdict. + +## Host facts (verified 2026-08-21) +ssh macmini-cf reachable (BatchMode OK), arm64, bun 1.3.14 installed → natural +1.3.14-vs-1.4.0 A/B host. zsh -lc PATH discipline. + +## Protocol +1. Install test build: `npm pack` locally → scp tarball → `npm i -g ` + on macmini-cf. Record ocx --version + bunVersion/bunRevision/bunRuntimeSource + from /api/system/memory. +2. Baseline: default config; drive SSE churn (harness waves: SSE-normal, + SSE-slow, SSE-abort, idle-recovery); sample /api/system/memory every 60s + ≥30min. Capture extraMemorySize (010). +3. GC evaluation (020 Phase A): matched no-GC/GC cell pairs with identical + concurrent request streams; child-side SIGUSR2 GC with reported durationMs; + +5s/+60s samples; three fresh-process runs; p99 latency delta ≤ max(5ms, 5%) + acceptance; evaluate the 260731 gate verbatim, criterion by criterion. +4. smol A/B (030): if remote numbers wanted beyond the local gate, trigger + storage jobs on both builds; record peak RSS delta. +5. Evidence: scalar-counter JSON only (watchdog privacy contract), committed + into this unit. + +## Acceptance mapping +goalplan c3: 020 carries macmini+local GC-gate numbers incl. latency pairs; +030 carries local A/B numbers; 010 records config/diagnostic-only rationale. +