2026-02-11 12:05:02 -08:00
# JavaScript REPL (`js_repl`)
`js_repl` runs JavaScript in a persistent Node-backed kernel with top-level `await` .
## Feature gate
`js_repl` is disabled by default and only appears when:
```toml
[features]
js_repl = true
```
2026-02-12 15:41:05 -08:00
`js_repl_tools_only` can be enabled to force direct model tool calls through `js_repl` :
```toml
[features]
js_repl = true
js_repl_tools_only = true
```
When enabled, direct model tool calls are restricted to `js_repl` and `js_repl_reset` ; other tools remain available via `await codex.tool(...)` inside js_repl.
2026-02-11 12:05:02 -08:00
## Node runtime
`js_repl` requires a Node version that meets or exceeds `codex-rs/node-version.txt` .
Runtime resolution order:
1. `CODEX_JS_REPL_NODE_PATH` environment variable
2. `js_repl_node_path` in config/profile
3. `node` discovered on `PATH`
You can configure an explicit runtime path:
```toml
js_repl_node_path = "/absolute/path/to/node"
```
2026-02-17 23:29:49 -08:00
## Module resolution
`js_repl` resolves **bare** specifiers (for example `await import("pkg")` ) using an ordered
2026-03-04 22:40:31 -08:00
search path. Local file imports are also supported for relative paths, absolute paths, and
`file://` URLs that point to ESM `.js` / `.mjs` files.
2026-02-17 23:29:49 -08:00
Module resolution proceeds in the following order:
1. `CODEX_JS_REPL_NODE_MODULE_DIRS` (PATH-delimited list)
2. `js_repl_node_module_dirs` in config/profile (array of absolute paths)
3. Thread working directory (cwd, always included as the last fallback)
For `CODEX_JS_REPL_NODE_MODULE_DIRS` and `js_repl_node_module_dirs` , module resolution is attempted in the order provided with earlier entries taking precedence.
2026-03-04 22:40:31 -08:00
Bare package imports always use this REPL-wide search path, even when they originate from an
imported local file. They are not resolved relative to the imported file's location.
2026-02-11 12:05:02 -08:00
## Usage
- `js_repl` is a freeform tool: send raw JavaScript source text.
- Optional first-line pragma:
- `// codex-js-repl: timeout_ms=15000`
- Top-level bindings persist across calls.
Persist initialized js_repl bindings after failed cells (#13482)
## Summary
- Change `js_repl` failed-cell persistence so later cells keep prior
bindings plus only the current-cell bindings whose initialization
definitely completed before the throw.
- Preserve initialized lexical bindings across failed cells via
module-namespace readability, including top-level destructuring that
partially succeeds before a later throw.
- Preserve hoisted `var` and `function` bindings only when execution
clearly reached their declaration site, and preserve direct top-level
pre-declaration `var` writes and updates through explicit write-site
markers.
- Preserve top-level `for...in` / `for...of` `var` bindings when the
loop body executes at least once, using a first-iteration guard to avoid
per-iteration bookkeeping overhead.
- Keep prior module state intact across link-time failures and
evaluation failures before the prelude runs, while still allowing failed
cells that already recreated prior bindings to persist updates to those
existing bindings.
- Hide internal commit hooks from user `js_repl` code after the prelude
aliases them, so snippets cannot spoof committed bindings by calling the
raw `import.meta` hooks directly.
- Add focused regression coverage for the supported failed-cell
behaviors and the intentionally unsupported boundaries.
- Update `js_repl` docs and generated instructions to describe the new,
narrower failed-cell persistence model.
## Motivation
We saw `js_repl` drop bindings that had already been initialized
successfully when a later statement in the same cell threw, for example:
const { context: liveContext, session } =
await initializeGoogleSheetsLiveForTab(tab);
// later statement throws
That was surprising in practice because successful earlier work
disappeared from the next cell.
This change makes failed-cell persistence more useful without trying to
model every possible partially executed JavaScript edge case. The
resulting behavior is narrower and easier to reason about:
- prior bindings are always preserved
- lexical bindings persist when their initialization completed before
the throw
- hoisted `var` / `function` bindings persist only when execution
clearly reached their declaration or a supported top-level `var` write
site
- failed cells that already recreated prior bindings can persist writes
to those existing bindings even if they introduce no new bindings
The detailed edge-case matrix stays in `docs/js_repl.md`. The
model-facing `project_doc` guidance is intentionally shorter and focused
on generation-relevant behavior.
## Supported Failed-Cell Behavior
- Prior bindings remain available after a failed cell.
- Initialized lexical bindings remain available after a failed cell.
- Top-level destructuring like `const { a, b } = ...` preserves names
whose initialization completed before a later throw.
- Hoisted `function` bindings persist when execution reached the
declaration statement before the throw.
- Direct top-level pre-declaration `var` writes and updates persist, for
example:
- `x = 1`
- `x += 1`
- `x++`
- short-circuiting logical assignments only persist when the write
branch actually runs
- Non-empty top-level `for...in` / `for...of` `var` loops persist their
loop bindings.
- Failed cells can persist updates to existing carried bindings after
the prelude has run, even when the cell commits no new bindings.
- Link failures and eval failures before the prelude do not poison
`@prev`.
## Intentionally Unsupported Failed-Cell Cases
- Hoisted function reads before the declaration, such as `foo(); ...;
function foo() {}`
- Aliasing or inference-based recovery from reads before declaration
- Nested writes inside already-instrumented assignment RHS expressions
- Destructuring-assignment recovery for hoisted `var`
- Partial `var` destructuring recovery
- Pre-declaration `undefined` reads for hoisted `var`
- Empty top-level `for...in` / `for...of` loop vars
- Nested or scope-sensitive pre-declaration `var` writes outside direct
top-level expression statements
2026-03-05 11:01:46 -08:00
- If a cell throws, prior bindings remain available, lexical bindings whose initialization completed before the throw stay available in later calls, and hoisted `var` / `function` bindings persist only when execution clearly reached their declaration or a supported write site.
- Supported hoisted-`var` failed-cell cases are direct top-level identifier writes and updates before the declaration (for example `x = 1` , `x += 1` , `x++` , `x &&= 1` ) and non-empty top-level `for...in` / `for...of` loops.
- Intentionally unsupported failed-cell cases include hoisted function reads before the declaration, aliasing or direct-IIFE-based inference, writes in nested blocks or other nested statement structure, nested writes inside already-instrumented assignment RHS expressions, destructuring-assignment recovery for hoisted `var` , partial `var` destructuring recovery, pre-declaration `undefined` reads, and empty top-level `for...in` / `for...of` loop vars.
2026-02-11 12:05:02 -08:00
- Top-level static import declarations (for example `import x from "pkg"` ) are currently unsupported; use dynamic imports with `await import("pkg")` .
2026-03-04 22:40:31 -08:00
- Imported local files must be ESM `.js` / `.mjs` files and run in the same REPL VM context as the calling cell.
- Static imports inside imported local files may only target other local `.js` / `.mjs` files via relative paths, absolute paths, or `file://` URLs. Bare package and builtin imports from local files must stay dynamic via `await import(...)` .
- `import.meta.resolve()` returns importable strings such as `file://...` , bare package names, and `node:fs` ; the returned value can be passed back to `await import(...)` .
- Local file modules reload between execs, so a later `await import("./file.js")` picks up edits and fixed failures. Top-level bindings you already created still persist until `js_repl_reset` .
2026-02-12 12:10:25 -08:00
- Use `js_repl_reset` to clear the kernel state.
## Helper APIs inside the kernel
`js_repl` exposes these globals:
- `codex.tmpDir` : per-session scratch directory path.
- `codex.tool(name, args?)` : executes a normal Codex tool call from inside `js_repl` (including shell tools like `shell` / `shell_command` when available).
2026-03-03 16:25:59 -08:00
- `codex.emitImage(imageLike)` : explicitly adds exactly one image to the outer `js_repl` function output.
2026-03-04 22:40:31 -08:00
- Imported local files run in the same VM context, so they can also access `codex.*` , the captured `console` , and Node-like `import.meta` helpers.
2026-02-26 10:12:28 -08:00
- Each `codex.tool(...)` call emits a bounded summary at `info` level from the `codex_core::tools::js_repl` logger. At `trace` level, the same path also logs the exact raw response object or error string seen by JavaScript.
2026-03-03 16:25:59 -08:00
- Nested `codex.tool(...)` outputs stay inside JavaScript unless you emit them explicitly.
2026-03-05 12:12:32 -08:00
- `codex.emitImage(...)` accepts a data URL, a single `input_image` item, an object like `{ bytes, mimeType }` , or a raw tool response object that contains exactly one image and no text.
2026-03-03 16:25:59 -08:00
- `codex.emitImage(...)` rejects mixed text-and-image content.
- Example of sharing an in-memory Playwright screenshot: `await codex.emitImage({ bytes: await page.screenshot({ type: "jpeg", quality: 85 }), mimeType: "image/jpeg" })` .
- Example of sharing a local image tool result: `await codex.emitImage(codex.tool("view_image", { path: "/absolute/path" }))` .
2026-02-12 12:10:25 -08:00
Avoid writing directly to `process.stdout` / `process.stderr` / `process.stdin` ; the kernel uses a JSON-line transport over stdio.
2026-02-11 12:05:02 -08:00
2026-02-26 10:12:28 -08:00
## Debug logging
Nested `codex.tool(...)` diagnostics are emitted through normal `tracing` output instead of rollout history.
- `info` level logs a bounded summary.
- `trace` level also logs the exact serialized response object or error string seen by JavaScript.
For `codex app-server` , these logs are written to the server process `stderr` .
Examples:
```sh
RUST_LOG=codex_core::tools::js_repl=info \
LOG_FORMAT=json \
codex app-server \
2> /tmp/codex-app-server.log
```
```sh
RUST_LOG=codex_core::tools::js_repl=trace \
LOG_FORMAT=json \
codex app-server \
2> /tmp/codex-app-server.log
```
In both cases, inspect `/tmp/codex-app-server.log` or whatever sink captures the process `stderr` .
2026-02-11 12:05:02 -08:00
## Vendored parser asset (`meriyah.umd.min.js`)
The kernel embeds a vendored Meriyah bundle at:
- `codex-rs/core/src/tools/js_repl/meriyah.umd.min.js`
Current source is `meriyah@7.0.0` from npm (`dist/meriyah.umd.min.js` ).
Licensing is tracked in:
- `third_party/meriyah/LICENSE`
- `NOTICE`
### How this file was sourced
From a clean temp directory:
```sh
tmp="$(mktemp -d)"
cd "$tmp"
npm pack meriyah@7 .0.0
tar -xzf meriyah-7.0.0.tgz
cp package/dist/meriyah.umd.min.js /path/to/repo/codex-rs/core/src/tools/js_repl/meriyah.umd.min.js
cp package/LICENSE.md /path/to/repo/third_party/meriyah/LICENSE
```
### How to update to a newer version
1. Replace `7.0.0` in the commands above with the target version.
2. Copy the new `dist/meriyah.umd.min.js` into `codex-rs/core/src/tools/js_repl/meriyah.umd.min.js` .
3. Copy the package license into `third_party/meriyah/LICENSE` .
4. Update the version string in the header comment at the top of `meriyah.umd.min.js` .
5. Update `NOTICE` if the upstream copyright notice changed.
6. Run the relevant `js_repl` tests.