Skip to content

Commit 8bc7aa8

Browse files
authored
Merge pull request #2479 from lidge-jun/dev
[WRONG BRANCH] release: promote dev to main (v2.32.0)
2 parents 6ae83b1 + c44e43f commit 8bc7aa8

300 files changed

Lines changed: 19750 additions & 2003 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

bin/ocx.mjs

Lines changed: 99 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -227,11 +227,90 @@ function runNpmSelfUpdate() {
227227
} catch { /* keep default */ }
228228
}
229229

230+
const launcher = fileURLToPath(import.meta.url);
231+
232+
function startProxyDirectly() {
233+
if (!existsSync(launcher)) {
234+
console.error("opencodex: cannot restart the proxy because the launcher is missing; reinstall opencodex manually.");
235+
return;
236+
}
237+
const env = { ...process.env };
238+
delete env.OCX_SERVICE;
239+
console.log(`Attempting to restart the proxy on port ${bakePort}.`);
240+
const child = spawn(process.execPath, [launcher, "start", "--port", String(bakePort)], {
241+
detached: true,
242+
stdio: "ignore",
243+
windowsHide: true,
244+
env,
245+
});
246+
child.on("error", error => {
247+
console.error(`opencodex: direct proxy restart failed: ${error.message}`);
248+
});
249+
child.unref();
250+
}
251+
252+
function refreshBackgroundServiceOrStartDirect() {
253+
const prevBake = process.env.OCX_BAKE_PORT;
254+
process.env.OCX_BAKE_PORT = String(bakePort);
255+
try {
256+
let svc = spawnSync(process.execPath, serviceRefreshArgs(), { stdio: "inherit", windowsHide: true });
257+
// `serviceWasInstalled` is inferred from service-state.json alone, which can be
258+
// STALE — present while the registration is gone. Repair refuses that case by
259+
// design, and its thrown Error is indistinguishable from any other failure at
260+
// this layer (plain Error, inherited stdio, generic exit status). So ask for
261+
// structured state instead of parsing the failure: install only when the
262+
// diagnostic says the service is genuinely absent. Installing after ANY repair
263+
// failure would resurrect the elevation prompt this change exists to avoid, and
264+
// could re-register a service the user just uninstalled.
265+
if (svc.status !== 0 && readServiceInstalledFromStatus(launcher) === false) {
266+
console.log("No registered service found — installing it instead.");
267+
svc = spawnSync(process.execPath, serviceInstallArgs(), { stdio: "inherit", windowsHide: true });
268+
}
269+
let needDirectStart = svc.status !== 0;
270+
if (!needDirectStart) {
271+
// Exit 0 can still leave stale/missing assets that never bring the proxy
272+
// back — match the GUI/CLI fallthrough so /healthz is not left dead.
273+
try {
274+
const st = spawnSync(process.execPath, [launcher, "status", "--json"], {
275+
encoding: "utf8",
276+
timeout: 20_000,
277+
windowsHide: true,
278+
});
279+
if (st.status === 0 && typeof st.stdout === "string" && st.stdout.trim()) {
280+
const parsed = JSON.parse(st.stdout);
281+
const proxyUp = parsed?.proxy?.running === true || parsed?.proxy?.health?.ok === true;
282+
const viable = parsed?.startup?.serviceViable === true;
283+
if (!proxyUp && !viable) needDirectStart = true;
284+
} else {
285+
// status failed or empty — fail closed to direct start (match CLI).
286+
needDirectStart = true;
287+
}
288+
} catch {
289+
needDirectStart = true;
290+
}
291+
}
292+
if (needDirectStart) {
293+
// A repair needs no elevation, but it can still fail — or exit 0 while leaving
294+
// a non-viable manager. Fall back to a direct detached proxy start so the
295+
// update never leaves the user without a running proxy.
296+
console.warn(
297+
svc.status === 0
298+
? "opencodex: service refresh left a non-viable manager — starting the proxy directly instead."
299+
: "opencodex: service refresh failed — starting the proxy directly instead.",
300+
);
301+
console.warn(" Run 'ocx service repair' to see why the background service could not restart.");
302+
startProxyDirectly();
303+
}
304+
} finally {
305+
if (prevBake === undefined) delete process.env.OCX_BAKE_PORT;
306+
else process.env.OCX_BAKE_PORT = prevBake;
307+
}
308+
}
309+
230310
// Never replace package files under a live proxy — stop it first (full `ocx stop`
231311
// semantics: graceful drain, service stop, native Codex restore). Gate on the service
232312
// and the runtime-port record too: a service-managed or orphaned proxy can be live
233313
// while ocx.pid is stale/missing.
234-
const launcher = fileURLToPath(import.meta.url);
235314
if (trayBeforeUpdate.stopBeforeReplacement) {
236315
console.log("⏹ Handing off the Windows tray before updating...");
237316
try {
@@ -249,6 +328,17 @@ function runNpmSelfUpdate() {
249328
}
250329
const hasRuntimeState =
251330
existsSync(join(configDir(), "ocx.pid")) || existsSync(join(configDir(), "runtime-port.json"));
331+
332+
function recoverStoppedRuntimeAfterFailure() {
333+
if (serviceWasInstalled) {
334+
console.warn("opencodex: update failed after stopping the proxy — restoring the previous background service.");
335+
refreshBackgroundServiceOrStartDirect();
336+
} else if (hasRuntimeState) {
337+
console.warn("opencodex: update failed after stopping the proxy — restarting the previous version directly.");
338+
startProxyDirectly();
339+
}
340+
}
341+
252342
if (serviceWasInstalled || hasRuntimeState) {
253343
console.log("⏹ Stopping the running proxy before updating...");
254344
const stopRes = spawnSync(process.execPath, [launcher, "stop"], { stdio: "inherit", windowsHide: true });
@@ -261,9 +351,9 @@ function runNpmSelfUpdate() {
261351
}
262352
if (historyRestoreIncomplete()) {
263353
console.warn(
264-
"opencodex: WARNING — Codex resume history was NOT restored (history DB locked; Codex app/IDE open?).\n" +
265-
" Routed threads stay hidden in the native Codex app until restored.\n" +
266-
" After the update: close the Codex app, then run 'ocx stop' once to restore.",
354+
"opencodex: WARNING — Codex resume-history metadata restore is incomplete (a backup manifest remains).\n" +
355+
" The DB may be busy or the manifest/target may need review; untracked routed history is intentionally unchanged.\n" +
356+
" After the update: close the Codex app, run 'ocx doctor', then run 'ocx stop' once to retry.",
267357
);
268358
}
269359
}
@@ -309,7 +399,8 @@ function runNpmSelfUpdate() {
309399
// it recreates the #1849 destruction path. Report and stop; the boot probe and the
310400
// recovery marker cover the swap-window states.
311401
console.error(`opencodex: transactional update failed unexpectedly (${error?.message ?? error}). ` +
312-
"The live install was not knowingly modified; run 'ocx update' again or reinstall with npm install -g.");
402+
`The live install was not knowingly modified; run 'ocx update' again or reinstall with ` +
403+
`npm install -g --allow-scripts=bun ${PKG}@${tag}.`);
313404
res = { status: 1 };
314405
}
315406
if (res.status === 0) {
@@ -329,77 +420,15 @@ function runNpmSelfUpdate() {
329420
// launcher so the new files write the baked paths and the service restarts.
330421
if (serviceWasInstalled) {
331422
console.log("Refreshing the background service with the updated files...");
332-
const prevBake = process.env.OCX_BAKE_PORT;
333-
process.env.OCX_BAKE_PORT = String(bakePort);
334-
try {
335-
let svc = spawnSync(process.execPath, serviceRefreshArgs(), { stdio: "inherit", windowsHide: true });
336-
// `serviceWasInstalled` is inferred from service-state.json alone, which can be
337-
// STALE — present while the registration is gone. Repair refuses that case by
338-
// design, and its thrown Error is indistinguishable from any other failure at
339-
// this layer (plain Error, inherited stdio, generic exit status). So ask for
340-
// structured state instead of parsing the failure: install only when the
341-
// diagnostic says the service is genuinely absent. Installing after ANY repair
342-
// failure would resurrect the elevation prompt this change exists to avoid, and
343-
// could re-register a service the user just uninstalled.
344-
if (svc.status !== 0 && readServiceInstalledFromStatus(launcher) === false) {
345-
console.log("No registered service found — installing it instead.");
346-
svc = spawnSync(process.execPath, serviceInstallArgs(), { stdio: "inherit", windowsHide: true });
347-
}
348-
let needDirectStart = svc.status !== 0;
349-
if (!needDirectStart) {
350-
// Exit 0 can still leave stale/missing assets that never bring the proxy
351-
// back — match the GUI/CLI fallthrough so /healthz is not left dead.
352-
try {
353-
const st = spawnSync(process.execPath, [launcher, "status", "--json"], {
354-
encoding: "utf8",
355-
timeout: 20_000,
356-
windowsHide: true,
357-
});
358-
if (st.status === 0 && typeof st.stdout === "string" && st.stdout.trim()) {
359-
const parsed = JSON.parse(st.stdout);
360-
const proxyUp = parsed?.proxy?.running === true || parsed?.proxy?.health?.ok === true;
361-
const viable = parsed?.startup?.serviceViable === true;
362-
if (!proxyUp && !viable) needDirectStart = true;
363-
} else {
364-
// status failed or empty — fail closed to direct start (match CLI).
365-
needDirectStart = true;
366-
}
367-
} catch {
368-
needDirectStart = true;
369-
}
370-
}
371-
if (needDirectStart) {
372-
// A repair needs no elevation, but it can still fail — or exit 0 while leaving
373-
// a non-viable manager. Fall back to a direct detached proxy start so the
374-
// update never leaves the user without a running proxy.
375-
console.warn(
376-
svc.status === 0
377-
? "opencodex: service refresh left a non-viable manager — starting the proxy directly instead."
378-
: "opencodex: service refresh failed — starting the proxy directly instead.",
379-
);
380-
console.warn(" Run 'ocx service repair' to see why the background service could not restart.");
381-
const env = { ...process.env };
382-
delete env.OCX_SERVICE;
383-
const child = spawn(process.execPath, [launcher, "start", "--port", String(bakePort)], {
384-
detached: true,
385-
stdio: "ignore",
386-
windowsHide: true,
387-
env,
388-
});
389-
child.unref();
390-
console.log(`Proxy starting on port ${bakePort}.`);
391-
}
392-
} finally {
393-
if (prevBake === undefined) delete process.env.OCX_BAKE_PORT;
394-
else process.env.OCX_BAKE_PORT = prevBake;
395-
}
423+
refreshBackgroundServiceOrStartDirect();
396424
} else {
397425
console.log("Restart the proxy: ocx start");
398426
}
399427
process.exit(0);
400428
}
401429
if (trayBeforeUpdate.restoreOnFailure) runTrayLifecycle(launcher, "start");
402-
console.error(`\nUpdate failed (npm exit ${res.status ?? "?"}). Try manually: npm install -g ${PKG}@${tag}`);
430+
recoverStoppedRuntimeAfterFailure();
431+
console.error(`\nUpdate failed (npm exit ${res.status ?? "?"}). Try manually: npm install -g --allow-scripts=bun ${PKG}@${tag}`);
403432
process.exit(1);
404433
}
405434

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# 000_plan — unit map
2+
3+
- 000_research.md — claim ledger + gap analysis
4+
- 010_memory_diagnostics.md — extraMemorySize observability (PR parent, base dev)
5+
- 020_watchdog_gc_relief.md — measurement-FIRST GC evaluation (Phase A harness),
6+
conditional idle-gated production hook (Phase B) per the 260731 gate
7+
- 030_smol_workers.md — smol:true gated on per-worker large-fixture A/B
8+
- 040_macmini_measurement.md — live measurement protocol (feeds 020 Phase A)
9+
10+
Stack shape: PR-A(010, base dev) → PR-B(020 Phase A harness + evaluation,
11+
base PR-A head) → conditional PR for Phase B only on gate PASS;
12+
PR-C(030, base dev, lands per-call-site with A/B evidence).
13+
One decade doc = one work-phase = one PABCD cycle (LOOP-UNIT-CHAIN-01).
14+
Audit round 1: FAIL (4 findings) → docs revised: 020 restructured
15+
measurement-first honoring 260731_macos_rss_retention/040_allocator_residual
16+
gate; 010 static-import sync seam; 030 pre-landing A/B gate; separate
17+
lastReliefAt. See ledger.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# 000 — Bun 1.4 follow-up memory patches: research and claim ledger
2+
3+
Date: 2026-08-22
4+
Unit: 260822_260822-bun14-followup-memory
5+
Question: from today's viewpoint (bundled Bun 1.4.0, released 2026-08-19), which
6+
ADDITIONAL memory patches are possible and worthwhile in opencodex?
7+
8+
## Method
9+
10+
Luna 5-lane discovery swarm (official releases / GitHub issues+PRs / JSC-runtime /
11+
community / server-SSE-proxy), then Tier-2 proof by the main agent via `gh api`
12+
against oven-sh/bun. App-side baseline re-audited against
13+
devlog/_fin/260813_bun_canary_dogfood/050_memory_patch_roadmap.md and current src/.
14+
15+
## Claim ledger (Tier-2 proven unless noted)
16+
17+
| # | Claim | Proof | Status |
18+
|---|---|---|---|
19+
| 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 |
20+
| 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 |
21+
| 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 |
22+
| 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 |
23+
| 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 |
24+
| 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) |
25+
| 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) |
26+
| 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) |
27+
| 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 |
28+
| 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 |
29+
| C11 | Medium post claims 1.4-era HTTP long-connection RSS still grew 280→340MB over 7 days; page returned 403. | unreachable | candidate — unverified |
30+
31+
## App-side baseline (what is already done — do not re-patch)
32+
33+
- 260813 roadmap patches #1#4 ALL landed since: native-main hardened-identity LRU
34+
(src/codex/native-main-claim.ts:25-33), installation-salt LRU
35+
(src/lab/subject/installation-salt.ts:7-17), mode-hint capability LRU
36+
(src/codex/features.ts:1097-1106), Lab ledger event-id process index REMOVED
37+
(no `eventIdIndexByLedger` in src/lab/ledger/store.ts).
38+
- `Bun.serve({ idleTimeout: 255 })` (src/server/index.ts:736) and per-request
39+
`server.timeout(req, 0)` for streaming (src/server/responses/fetch-helpers.ts:113)
40+
already implement the SSE-timeout guidance the swarm surfaced.
41+
- eager-relay vs legacy-tee runtime gate: src/lib/bun-stream-caps.ts
42+
(MIN_FIXED_BUN_VERSION="1.4.0").
43+
- Memory watchdog: warn-only ring sampler (src/server/memory-watchdog.ts), exposed at
44+
/api/system/memory with bun:jsc heapSize/heapCapacity/objectCount.
45+
- 36-store bounded-memory audit closed (devlog/_fin/260813…/050): only remaining
46+
investigation is model-cache generation tombstones — needs an authority-token
47+
redesign, NOT an eviction patch; excluded from this unit.
48+
49+
## Gap analysis → patch set for THIS unit
50+
51+
What Bun 1.4 newly makes possible, that opencodex does not use yet:
52+
53+
1. **Diagnostics gap** — /api/system/memory and the watchdog ignore
54+
`extraMemorySize` (JSC-visible native memory) and the watchdog samples carry no
55+
JSC data at all. On 1.4, extraMemorySize is the counter that moved most
56+
(external-memory reporting fixes #31422/#32653/#34142). → doc 010.
57+
2. **Reclaim gap** — nothing in the tree ever calls `Bun.gc`. On 1.4 a full
58+
`Bun.gc(true)` also purges mimalloc pages (C6) — the exact mitigation for the
59+
"heap shrinks, RSS stays" pattern (#27514) that JSC flags cannot deliver (C5).
60+
A config-gated, rate-limited watchdog relief hook is now worth having. → doc 020.
61+
3. **Worker heap gap** — history/restore/policy workers are short-lived batch jobs;
62+
`smol: true` (C8) bounds their JSC heap growth policy at a small perf cost,
63+
reducing peak RSS during storage jobs. → doc 030.
64+
4. **Proof gap** — every claim above is config/diagnostic-grade until measured.
65+
macmini-cf (arm64, bun 1.3.14 installed → good A/B host) runs the live
66+
measurement protocol. → doc 040.
67+
68+
Explicit non-goals: no Bun runtime patching/fork (upstream 1.4.0 already carries
69+
C2–C4); no JSC env-var "caps" (C5 proves them unreliable); no smol for the main
70+
proxy process (throughput cost, unmeasured); no model-cache tombstone work.

0 commit comments

Comments
 (0)