From 637a8f6a0b92bb9e6bab9036048f2b11949d29c4 Mon Sep 17 00:00:00 2001 From: Mohammad Dashti Date: Thu, 20 Aug 2026 01:35:57 -0700 Subject: [PATCH 1/9] Added codex-temporal: a Temporal-backed durable executor. One Codex turn is one activity, and the rollout under ~/.codex/sessions stays the record. The workflow holds the thread id Codex mints, so a later turn resumes the same thread and a crash re-drives the turn in flight instead of starting over. --- codex-temporal/.gitignore | 4 + codex-temporal/README.md | 75 + codex-temporal/inspect.mts | 14 + codex-temporal/package-lock.json | 3896 ++++++++++++++++++++++++++++++ codex-temporal/package.json | 23 + codex-temporal/src/activities.ts | 115 + codex-temporal/src/client.ts | 52 + codex-temporal/src/config.ts | 34 + codex-temporal/src/protocol.ts | 56 + codex-temporal/src/rollout.ts | 162 ++ codex-temporal/src/worker.ts | 30 + codex-temporal/src/workflow.ts | 88 + codex-temporal/state.mts | 3 + codex-temporal/submit.mts | 5 + codex-temporal/tsconfig.json | 21 + 15 files changed, 4578 insertions(+) create mode 100644 codex-temporal/.gitignore create mode 100644 codex-temporal/README.md create mode 100644 codex-temporal/inspect.mts create mode 100644 codex-temporal/package-lock.json create mode 100644 codex-temporal/package.json create mode 100644 codex-temporal/src/activities.ts create mode 100644 codex-temporal/src/client.ts create mode 100644 codex-temporal/src/config.ts create mode 100644 codex-temporal/src/protocol.ts create mode 100644 codex-temporal/src/rollout.ts create mode 100644 codex-temporal/src/worker.ts create mode 100644 codex-temporal/src/workflow.ts create mode 100644 codex-temporal/state.mts create mode 100644 codex-temporal/submit.mts create mode 100644 codex-temporal/tsconfig.json diff --git a/codex-temporal/.gitignore b/codex-temporal/.gitignore new file mode 100644 index 000000000000..dd8fe26f7f56 --- /dev/null +++ b/codex-temporal/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +*.log +.env diff --git a/codex-temporal/README.md b/codex-temporal/README.md new file mode 100644 index 000000000000..38576cc19239 --- /dev/null +++ b/codex-temporal/README.md @@ -0,0 +1,75 @@ +# codex-temporal + +A Temporal-backed durable executor for the [Codex CLI](https://github.com/openai/codex). Same pattern we proved on the OpenCode fork and on Pi: the agent's own loop runs under a durable executor, while the session record stays in the app's own log. + +Status: verified end to end against a live Codex built from this fork. Two turns on one thread work, and a worker killed mid-turn finishes that turn on a fresh worker without asking the same thing twice. + +## The idea + +Two parts of the state, two systems: + +- **Durable storage** stays with Codex. Codex already writes every turn to a rollout JSONL under `~/.codex/sessions`, and `codex exec resume ` reads it back. That file is the source of truth. We do not move it into Temporal. +- **Durable execution** comes from Temporal. A per-thread workflow drives Codex's turns and survives a crash: the turn re-runs on another worker and picks up from the rollout. + +One turn is one activity, and one turn is one `codex exec` process. That process boundary is what makes this the cleanest of the three integrations: there is no long-lived agent object to rebuild, so a crash is just a process that has to be run again. + +## What Codex already had + +Two things we had to add to Pi are already in Codex, which is worth saying plainly: + +- **A crash-truncated rollout is repaired on the way to the model.** `ensure_call_outputs_present` (`codex-rs/core/src/context_manager/normalize.rs`) synthesizes a `FunctionCallOutput` for any tool call whose output never landed, so the payload a resumed thread sends is valid. The synthetic outputs are not written back to the rollout. +- **A turn can run with no new user message.** `turn/start` with empty input is a supported, tested path in the app-server protocol. + +## What this fork adds + +The second one was not reachable from the shipping CLI: `codex exec resume ` always resolves a prompt, so a retry after a mid-turn crash could only ask the same thing twice. This fork exposes it. + +- `codex exec resume --continue` runs a turn with no new user message. Use it to finish a turn that stopped part way through. +- `Thread.continueTurn()` and `Thread.continueTurnStreamed()` in the TypeScript SDK do the same from code. + +Both are small: the CLI flag maps to the empty-input turn that already existed, and the SDK method reuses the turn collector that `run()` already had. + +## What is durable, and what is not + +- **Between turns: clean.** A worker dying between turns loses nothing. The next prompt drives on any worker, and Codex reads the rollout back. The workflow is what remembers the thread id, so the next turn resumes the same thread rather than starting a new one. +- **Mid-turn: the turn is carried on, not restarted.** On a retry, the activity looks for its own prompt in the rollout. Recorded and answered means the answer is already there and nothing needs to run. Recorded and unfinished means `continueTurn()`, so the prompt is not asked twice. +- **A first turn that dies before Codex minted a thread id** starts over, which is correct: nothing had been recorded yet. Once the id exists the activity heartbeats it, so a retry from that point resumes rather than opening a second thread. +- **Tool side effects are not rolled back.** A crash between a tool starting and its result landing leaves a tool call with no output, and Codex reports it to the model as `"aborted"`. For a coding agent that is optimistic: after a hard kill the command may well have run. Reporting the outcome as unknown, which is what we did on the Pi fork, is the safer default and is the natural next change here. + +## Running it + +``` +npm install +CODEX_PATH=../codex-rs/target/debug/codex npm run worker # in one shell +npx tsx submit.mts my-session "Reply with the single word BRAVO." +npx tsx state.mts my-session +npx tsx inspect.mts +``` + +The worker needs a Codex build from this fork, because it calls `--continue`. Build it with `cargo build -p codex-cli --bin codex` in `codex-rs` (set `CARGO_NET_GIT_FETCH_WITH_CLI=true` if a git dependency fails to authenticate). + +Env: `TEMPORAL_ADDRESS`, `TEMPORAL_NAMESPACE`, `CODEX_TEMPORAL_TASK_QUEUE`, `CODEX_SESSIONS_DIR`, `CODEX_IDLE_TIMEOUT`, `CODEX_PATH`, `CODEX_MODEL`, `CODEX_PROJECT_DIR`, `CODEX_SANDBOX`. + +`temporal workflow query --workflow-id codex-thread- --name threadState` reports the queue, the thread id, the turn in flight, and how the last turn ended. + +## Reproducing the crash test + +Submit a turn long enough to still be running a few seconds in, wait until the prompt is in the rollout with no turn completion after it, `pkill -9 -f "codex-temporal.*src/worker.ts"`, wait past the 30s heartbeat timeout, then start a fresh worker. The activity comes back on attempt 2, the turn finishes, and the rollout holds exactly one copy of the prompt. + +The observed run: before the kill, 1 prompt and 0 turn completions; after recovery, 1 prompt and 1 turn completion, with the answer running to the end. + +## Layout + +- `src/config.ts` — Temporal + Codex wiring from env. +- `src/protocol.ts` — workflow id, signal and query names, shared types. +- `src/rollout.ts` — reading Codex's rollout: find it by thread id, and ask what it says about one prompt. +- `src/activities.ts` — `runTurn`: drives one Codex turn through the SDK. +- `src/workflow.ts` — `codexThread`: per-thread durable executor (submit prompt, drive, interrupt, idle-retire). +- `src/worker.ts` — worker hosting the workflow and the activity. +- `src/client.ts` — submit a prompt, read thread state, interrupt. + +## Known gaps + +- **No step level.** A turn is the smallest durable unit here. Codex has no "run one model call and its tools, then stop" surface, so a crash re-drives from wherever the rollout left off rather than from the last step. On the Pi fork we added stepping; the same change here is a core change, not a CLI change. +- **A settled tool call is reported as aborted, not unknown.** See above. +- **Tool execution needs a release build.** Codex routes shell tools through a code-mode host that embeds V8, and there is no prebuilt V8 archive for `aarch64-apple-darwin` in this version, so a source build cannot run tool calls. The crash test above therefore uses a long generation rather than a tool call. diff --git a/codex-temporal/inspect.mts b/codex-temporal/inspect.mts new file mode 100644 index 000000000000..3974316bb5c9 --- /dev/null +++ b/codex-temporal/inspect.mts @@ -0,0 +1,14 @@ +// Summarize a Codex rollout by thread id: how many of our prompts it holds, the tool-call balance, +// and the last answer. The tool balance is what a crash shows up in. +import { fromEnv } from "./src/config.js"; +import { findRollout, summarize } from "./src/rollout.js"; + +const threadId = process.argv[2]; +const cfg = fromEnv(); +const path = await findRollout(cfg.sessionsDir, threadId); +if (!path) { + console.error(`no rollout for thread ${threadId} under ${cfg.sessionsDir}`); + process.exit(1); +} +console.log(JSON.stringify({ path, ...(await summarize(path)) })); +process.exit(0); diff --git a/codex-temporal/package-lock.json b/codex-temporal/package-lock.json new file mode 100644 index 000000000000..2b40330f7ff7 --- /dev/null +++ b/codex-temporal/package-lock.json @@ -0,0 +1,3896 @@ +{ + "name": "codex-temporal", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "codex-temporal", + "version": "0.0.0", + "dependencies": { + "@temporalio/activity": "^1.11.0", + "@temporalio/client": "^1.11.0", + "@temporalio/worker": "^1.11.0", + "@temporalio/workflow": "^1.11.0" + }, + "devDependencies": { + "@modelcontextprotocol/sdk": "^1.0.0", + "tsx": "^4.19.0", + "typescript": "^5.6.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", + "integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", + "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@hono/node-server": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.1.tgz", + "integrity": "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/buffers": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz", + "integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/codegen": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", + "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-core": { + "version": "4.68.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.68.1.tgz", + "integrity": "sha512-V5oZ4Gt9WJKyQef0n9cAd0N9qjSkIBm3E4MYsgNIWBk5aINCDPKxMPo1i29rBxqiT4Ixf1epklqV9VJMKIxwlw==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.68.1", + "@jsonjoy.com/fs-node-utils": "4.68.1", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-fsa": { + "version": "4.68.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.68.1.tgz", + "integrity": "sha512-HCG72UioncuO7Gw09XNVG+S85e3cq2hrUC/mexBrsWsa3mI7eePkkqWie3uVYbtsb64OR9YGQs5SqaufDRYBcg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.68.1", + "@jsonjoy.com/fs-node-builtins": "4.68.1", + "@jsonjoy.com/fs-node-utils": "4.68.1", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node": { + "version": "4.68.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.68.1.tgz", + "integrity": "sha512-R5D9mWtqdURzcOWj1vdXr3APCwX0xchtFT+kmW7fXLNDifWdDrnh26jSID8pdnUfFBxTyfHtFtTL/NWKzIH7kQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.68.1", + "@jsonjoy.com/fs-node-builtins": "4.68.1", + "@jsonjoy.com/fs-node-utils": "4.68.1", + "@jsonjoy.com/fs-print": "4.68.1", + "@jsonjoy.com/fs-snapshot": "4.68.1", + "glob-to-regex.js": "^1.0.0", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-builtins": { + "version": "4.68.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.68.1.tgz", + "integrity": "sha512-HK1BTksysokNZxNspqDH0yPaqN9YgR/AYIlYiIaU2Ys4BOk5CdybI7r6BgiZuiiPiV8n4sK/kZdice7Znpy2Kw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-to-fsa": { + "version": "4.68.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.68.1.tgz", + "integrity": "sha512-lpKmU4X9e/oh8GIuAI7EXaS5QiLNM3KD15CkdhfS6PYmrGvoJqKQcyEfnLgnnaGslh/PFUMYSIZBCf2ejJGw8g==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-fsa": "4.68.1", + "@jsonjoy.com/fs-node-builtins": "4.68.1", + "@jsonjoy.com/fs-node-utils": "4.68.1" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-utils": { + "version": "4.68.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.68.1.tgz", + "integrity": "sha512-/GxfW1DWm9SCdkfbvqevLO/P5duobQfmKkHXxdMIDbcZMQeAgooAstIfZhkXpATzq9QbCQsnoWFM/dGHdZfndw==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.68.1", + "glob-to-regex.js": "^1.0.1" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-print": { + "version": "4.68.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.68.1.tgz", + "integrity": "sha512-oGeZOGPYKK9v1CgeVeEDsLomH1lCnslSpqUN5GmPzrmAVGQlsmsdcXNA2O4lV8Y4xkuSuynx2ITBkUHJVaTbow==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-utils": "4.68.1", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot": { + "version": "4.68.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.68.1.tgz", + "integrity": "sha512-XZfP0FDZN32bbc4t2bZN2qRrYHg5AktJnzk22HRoKGK4BprrbNRH2k5ceSNS/kupKYcofCs+O841+xaAbjnxwQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^17.65.0", + "@jsonjoy.com/fs-node-utils": "4.68.1", + "@jsonjoy.com/json-pack": "^17.65.0", + "@jsonjoy.com/util": "^17.65.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/base64": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.67.0.tgz", + "integrity": "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/codegen": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz", + "integrity": "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pack": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz", + "integrity": "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "17.67.0", + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0", + "@jsonjoy.com/json-pointer": "17.67.0", + "@jsonjoy.com/util": "17.67.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pointer": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.67.0.tgz", + "integrity": "sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/util": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/util": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.67.0.tgz", + "integrity": "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", + "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "^1.1.2", + "@jsonjoy.com/buffers": "^1.2.0", + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/json-pointer": "^1.0.2", + "@jsonjoy.com/util": "^1.9.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pointer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", + "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/util": "^1.9.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", + "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", + "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9 || ^2.0.5", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause" + }, + "node_modules/@swc/core": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.16.1.tgz", + "integrity": "sha512-nUaeu91O5QZKrQdaDCHd402ogUIoNOOjpkZNq0UomWK0G6gDaGmLhvddF1/3BXf5O8aLyo6ZPY/aMDWvaJQ/hg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.28" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.16.1", + "@swc/core-darwin-x64": "1.16.1", + "@swc/core-linux-arm-gnueabihf": "1.16.1", + "@swc/core-linux-arm64-gnu": "1.16.1", + "@swc/core-linux-arm64-musl": "1.16.1", + "@swc/core-linux-ppc64-gnu": "1.16.1", + "@swc/core-linux-s390x-gnu": "1.16.1", + "@swc/core-linux-x64-gnu": "1.16.1", + "@swc/core-linux-x64-musl": "1.16.1", + "@swc/core-win32-arm64-msvc": "1.16.1", + "@swc/core-win32-ia32-msvc": "1.16.1", + "@swc/core-win32-x64-msvc": "1.16.1" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.16.1.tgz", + "integrity": "sha512-zlJblJ8ncErD43lKdxjbUaUskJQf+LxiPXYcWXD8/8ZMV+7uuAT+CwjciLXpyZBd5Pq/S726bMpeeAwSeL1hhg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.16.1.tgz", + "integrity": "sha512-IN0BmPWb0YAh/17mmlWB/HDBtTw2MfuW4hulf/tQAgTQBRH17l+z499bNJLK6LizSjqs0P7V+jU38Zj+vJC1DA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.16.1.tgz", + "integrity": "sha512-EYgrx2YOCQ2Twz2S793kqNjPkpvYVUPzzR95bIb7by+VQcyaai4lZZ2iz/tZvcFVKSNcN3/JTKwx+aBn2ZL52A==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.16.1.tgz", + "integrity": "sha512-moyKm0YZlHdHohzm1YwgAyesqnE853rO0REMfJLFAova51wF9BNi+3ZW2PeS7Vqvn6HeJuepLpAHbBdZctxpHA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.16.1.tgz", + "integrity": "sha512-kKGBO9wdapiSzuf5ZzZ2fYtlu1BNSYtIIUxvH1ir/gcelTOREEHGDCLTDFx/2Knf878nU11A40z7LxwasEFxqA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.16.1.tgz", + "integrity": "sha512-nZ6qahtLxC3PM54cWOQZHxt4lTCF/3J4LIoWWzz6v7A+rLs8Dx54anYQf7mH3eIi8KlNpgKci/ie8ZSqFN8O7A==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.16.1.tgz", + "integrity": "sha512-4ji5PNzhYq193Z4/4xUaSoNJza6iCkDJSzhetrbB6KOYxsr+kxtQr8ePWhMJUiMt6JUWtXaZ1PYT8FhtED+nGA==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.16.1.tgz", + "integrity": "sha512-VJQxqrisHV+B394IgrOu8YsIIXZgffnf5tO+yc9Z/hoUpuZEvuQTjWwlnpZdpyD+0nx6LTD1/3k646JYm43yJA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.16.1.tgz", + "integrity": "sha512-r9oV1mwxxsIGcLV1IQ/tw76MW3doatKze1QFWuC+a7QqJUkhY/bKTSVk6NpKKUGm2LDsE33Va8VqSClfA7vSiQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.16.1.tgz", + "integrity": "sha512-6huNRessoBLxWEqBm5zJXyCQ27TO7anvkdiuQ5MDO4CJni0nOXEqKtV9RllQ2TdyENKKsUMXVnIfW2hIXx/R5Q==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.16.1.tgz", + "integrity": "sha512-OVKJFUzphrGmsh+BGtcZDesx0YryV7/Yvy5XGgTqnrZfjnyfcr5uaqYQugCckdIlupc5Vs3XtDjRAj12z4ZPlw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.16.1.tgz", + "integrity": "sha512-Bt+VIhWYCGk4urklnkkteLUOeLv1VxigwTCeB/xC6rBZxY6IIKdDwCJf6on3E3SUGsIqmQS6QqtuJQc1VxF4Aw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.28", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.28.tgz", + "integrity": "sha512-V6Mnml8v09QALx6K0elJ7o9K/MkVDtW3t6L+7Ou/JcWtb3xwId2AH4FeOceySd2JaO87IMw4+6vSZxLm34LPbw==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@temporalio/activity": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@temporalio/activity/-/activity-1.22.0.tgz", + "integrity": "sha512-hrPeELIMloFWVrwmDfG1lh3uFNkaYp1ZHpcpWmJniTeSyxqCqLdil4lBOv11FByb6HeKxE6rsvOOUohulyVhRg==", + "license": "MIT", + "dependencies": { + "@temporalio/client": "1.22.0", + "@temporalio/common": "1.22.0" + }, + "engines": { + "node": ">= 20.3.0" + } + }, + "node_modules/@temporalio/client": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@temporalio/client/-/client-1.22.0.tgz", + "integrity": "sha512-b02ty0uDly6/ZRKlbavs5GjXVTry/GB4tkq0v1NH4H7x+nTo/OK0u5Ms8Nni8D+aar3xff9lzIpXNwu/Dp3P3w==", + "license": "MIT", + "dependencies": { + "@grpc/grpc-js": "^1.12.4", + "@temporalio/common": "1.22.0", + "@temporalio/proto": "1.22.0", + "abort-controller": "^3.0.0", + "long": "^5.2.3", + "nexus-rpc": "^0.0.2", + "uuid": "^11.1.0" + }, + "engines": { + "node": ">= 20.3.0" + } + }, + "node_modules/@temporalio/common": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@temporalio/common/-/common-1.22.0.tgz", + "integrity": "sha512-1NpQpo/y6XU1ULbo/bXgBEhl6qQnOeB79NrsSJdjI38/hfXPom2IYGJcIxOFfsQICQ0zePEKzB9Yyo31KNpIxA==", + "license": "MIT", + "dependencies": { + "@temporalio/proto": "1.22.0", + "long": "^5.2.3", + "ms": "3.0.0-canary.1", + "nexus-rpc": "^0.0.2", + "proto3-json-serializer": "^2.0.0" + }, + "engines": { + "node": ">= 20.3.0" + } + }, + "node_modules/@temporalio/core-bridge": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@temporalio/core-bridge/-/core-bridge-1.22.0.tgz", + "integrity": "sha512-mJ5agqtfW+GBPYl+ivYLrBpKJ3oFOw8ogcoizNrxYCzwJeprGFiEgHDb/jzyvECR4HAy5s9zQFJYNyTruXXFMw==", + "license": "MIT", + "dependencies": { + "@grpc/grpc-js": "^1.12.4", + "@temporalio/common": "1.22.0" + }, + "engines": { + "node": ">= 20.3.0" + } + }, + "node_modules/@temporalio/nexus": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@temporalio/nexus/-/nexus-1.22.0.tgz", + "integrity": "sha512-4S1ZZdSNjrO9IUyhWxweIidvNn1X/ihEl+P2fOn/Ky38YRx9G+8wtV6g6lqG8zUrtEv//uqFw+HsRMziE2y4tg==", + "license": "MIT", + "dependencies": { + "@temporalio/client": "1.22.0", + "@temporalio/common": "1.22.0", + "@temporalio/proto": "1.22.0", + "long": "^5.2.3", + "nexus-rpc": "^0.0.2" + }, + "engines": { + "node": ">= 20.3.0" + } + }, + "node_modules/@temporalio/proto": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@temporalio/proto/-/proto-1.22.0.tgz", + "integrity": "sha512-X7NVa0Z6HK3l9irZR48FKwZ9w9YXetCdF2z2KCIRr0W7Kul64Wt9o5hwvSXShAtrZuCEEHH8WO/ffHTXxJieLg==", + "license": "MIT", + "dependencies": { + "long": "^5.2.3", + "protobufjs": "^7.6.4" + }, + "engines": { + "node": ">= 20.3.0" + } + }, + "node_modules/@temporalio/worker": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@temporalio/worker/-/worker-1.22.0.tgz", + "integrity": "sha512-axRHhUsovFlqDtXZxUnhRLV7WmC6qOMUCij13lw4glT5yRH62k8wIZqcT3naEfvLSzoDxjt2xM7me3VvTRSaow==", + "license": "MIT", + "dependencies": { + "@grpc/grpc-js": "^1.12.4", + "@swc/core": "^1.3.102", + "@temporalio/activity": "1.22.0", + "@temporalio/client": "1.22.0", + "@temporalio/common": "1.22.0", + "@temporalio/core-bridge": "1.22.0", + "@temporalio/nexus": "1.22.0", + "@temporalio/proto": "1.22.0", + "@temporalio/workflow": "1.22.0", + "heap-js": "^2.6.0", + "memfs": "^4.6.0", + "nexus-rpc": "^0.0.2", + "protobufjs": "^7.6.4", + "rxjs": "^7.8.1", + "source-map": "^0.7.4", + "source-map-loader": "^5.0.0", + "supports-color": "^8.1.1", + "swc-loader": "^0.2.3", + "unionfs": "^4.5.1", + "webpack": "^5.108.4" + }, + "engines": { + "node": ">= 20.3.0" + } + }, + "node_modules/@temporalio/workflow": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@temporalio/workflow/-/workflow-1.22.0.tgz", + "integrity": "sha512-31Ax529+TvfH2RCexOhiCyM3lARJJoVzrnRUS1gX+jl+mka+pRRnEUx4B572mZz0oOlCCxbiztHZF72wwwWZDA==", + "license": "MIT", + "dependencies": { + "@temporalio/common": "1.22.0", + "@temporalio/proto": "1.22.0", + "nexus-rpc": "^0.0.2" + }, + "engines": { + "node": ">= 20.3.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", + "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "license": "Apache-2.0" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.15", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.15.tgz", + "integrity": "sha512-FwMjJJ7HnyZpWe+oWxegG0fezZyBZUagI5LZEoO3GCbtbKNwRfMH9Ue5d5v01PNePBy1QSfPSDTTeVL0Hb9EzA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.411", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.411.tgz", + "integrity": "sha512-gglkxzokjHfawpGxq75XdBV2/l3BAPzrsMs70qgaZdTW5rpV1tC4MdgJVP9fN126bODA4ZJQkn1wryEzJyQXIg==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.6.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.2.tgz", + "integrity": "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-monkey": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", + "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", + "license": "Unlicense" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-to-regex.js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", + "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/heap-js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/heap-js/-/heap-js-2.7.1.tgz", + "integrity": "sha512-EQfezRg0NCZGNlhlDR3Evrw1FVL2G3LhU7EgPoxufQKruNBSYA8MiRPHeWbU+36o+Fhel0wMwM+sLEiBAlNLJA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/hono": { + "version": "4.13.3", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.3.tgz", + "integrity": "sha512-r8AO2mYHoLxSHkgafNeC/BXyb2vWRxD3jem4Ts+ptav8oTG5FIRifAjuJEmZI4bSvvc2ns0GxmIYiZnHqN3mMw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "license": "MIT", + "engines": { + "node": ">=10.18" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz", + "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jose": { + "version": "6.2.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.9.tgz", + "integrity": "sha512-XrchZOFZUl/T3vTwRe8XK+cJrGtMF4th1ARnDfwbBXFKThGhlsxEE4Zu03AD/bjJSt/9jT/mxrOCkJWOg77aPA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/memfs": { + "version": "4.68.1", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.68.1.tgz", + "integrity": "sha512-OD+IDRUvIxu3QHL+nFm9gdyugInD27FDJ+sl4B5QgomPHXMlbw+GP918P8VNKu2FkNlVeqBkpzkwROpamVifRw==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.68.1", + "@jsonjoy.com/fs-fsa": "4.68.1", + "@jsonjoy.com/fs-node": "4.68.1", + "@jsonjoy.com/fs-node-builtins": "4.68.1", + "@jsonjoy.com/fs-node-to-fsa": "4.68.1", + "@jsonjoy.com/fs-node-utils": "4.68.1", + "@jsonjoy.com/fs-print": "4.68.1", + "@jsonjoy.com/fs-snapshot": "4.68.1", + "@jsonjoy.com/json-pack": "^1.11.0", + "@jsonjoy.com/util": "^1.9.0", + "glob-to-regex.js": "^1.0.1", + "thingies": "^2.5.0", + "tree-dump": "^1.0.3", + "tslib": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimizer-webpack-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-DoeAZz8Q1C1znwsUzej1fdoi4jCf7/+Em27ouLqfK/+3m8G+D7yDhUwrc3CNhjSzGUN1kn7Iv4sWmjflQHenpw==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/ms": { + "version": "3.0.0-canary.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-3.0.0-canary.1.tgz", + "integrity": "sha512-kh8ARjh8rMN7Du2igDRO9QJnqCb2xYTJxyQYK7vJJS4TvLLmsbyhiKpSW+t+y26gyOyMd0riphX0GeWKU3ky5g==", + "license": "MIT", + "engines": { + "node": ">=12.13" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/nexus-rpc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/nexus-rpc/-/nexus-rpc-0.0.2.tgz", + "integrity": "sha512-IWjIExdVYlmwXuzHdY/Q3lXCv1gbqoAXPazQhy2w4Xgtgha3H0OOujEESVPQcFUFMWm+pAk2gKnb57g8S41JZg==", + "license": "MIT", + "engines": { + "node": ">= 20.0.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proto3-json-serializer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", + "integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==", + "license": "Apache-2.0", + "dependencies": { + "protobufjs": "^7.2.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/protobufjs": { + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", + "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.72.1" + } + }, + "node_modules/source-map-loader/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/swc-loader": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/swc-loader/-/swc-loader-0.2.7.tgz", + "integrity": "sha512-nwYWw3Fh9ame3Rtm7StS9SBLpHRRnYcK7bnpF3UKZmesAK0gw2/ADvlURFAINmPvKtDLzp+GBiP9yLoEjg6S9w==", + "license": "MIT", + "dependencies": { + "@swc/counter": "^0.1.3" + }, + "peerDependencies": { + "@swc/core": "^1.2.147", + "webpack": ">=2" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.50.0.tgz", + "integrity": "sha512-CN9BVxWhgS/hRxtUMjtC2uRWSTcSfQFHMDWma6sKKfIivCD91sM+FOPfvwoaRMqCSrUpe1nv3jDamd9eEQ4y+w==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/thingies": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.6.1.tgz", + "integrity": "sha512-cV/CMGTK3M4MlnJ/0At6ismOw/A0EEniDNScajjz/Br3c1sqE72YD01rGpPTKwd27wAxI5Pr+6+0w8yofzFRYw==", + "license": "MIT", + "engines": { + "node": ">=10.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "^2" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-dump": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", + "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "peer": true + }, + "node_modules/tsx": { + "version": "4.23.12", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", + "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "license": "MIT" + }, + "node_modules/unionfs": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/unionfs/-/unionfs-4.6.0.tgz", + "integrity": "sha512-fJAy3gTHjFi5S3TP5EGdjs/OUMFFvI/ady3T8qVuZfkv8Qi8prV/Q8BuFEgODJslhZTT2z2qdD2lGdee9qjEnA==", + "dependencies": { + "fs-monkey": "^1.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uuid": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/watchpack": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", + "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.109.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.109.2.tgz", + "integrity": "sha512-U9/cvLzxObKNEZ9+TtdqrHM5/9z3lgl2c+c4BzbqGxFQvQvBAq87yql5A8pQ+rrMbS496MZJeF5enVBndIy2hw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.24.4", + "es-module-lexer": "^2.1.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "graceful-fs": "^4.2.11", + "mime-db": "^1.54.0", + "minimizer-webpack-plugin": "^5.6.1", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "watchpack": "^2.5.2", + "webpack-sources": "^3.5.1" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.1.tgz", + "integrity": "sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/codex-temporal/package.json b/codex-temporal/package.json new file mode 100644 index 000000000000..142cf8d169c8 --- /dev/null +++ b/codex-temporal/package.json @@ -0,0 +1,23 @@ +{ + "name": "codex-temporal", + "version": "0.0.0", + "private": true, + "description": "Temporal-backed durable executor for the Codex CLI", + "type": "module", + "scripts": { + "typecheck": "tsc --noEmit", + "worker": "tsx src/worker.ts", + "submit": "tsx submit.mts" + }, + "dependencies": { + "@temporalio/activity": "^1.11.0", + "@temporalio/client": "^1.11.0", + "@temporalio/worker": "^1.11.0", + "@temporalio/workflow": "^1.11.0" + }, + "devDependencies": { + "@modelcontextprotocol/sdk": "^1.0.0", + "tsx": "^4.19.0", + "typescript": "^5.6.0" + } +} diff --git a/codex-temporal/src/activities.ts b/codex-temporal/src/activities.ts new file mode 100644 index 000000000000..d68fb49cba57 --- /dev/null +++ b/codex-temporal/src/activities.ts @@ -0,0 +1,115 @@ +// The durable step body: drive one Codex turn through the SDK, with Codex's rollout JSONL as the +// log. Runs inside a Temporal activity, so a worker crash re-runs this one turn; it re-opens the +// thread by id. Node builtins and the Codex SDK are fine here (not workflow code). + +import { Context } from "@temporalio/activity"; +import { Codex } from "@openai/codex-sdk"; +import type { Config } from "./config.js"; +import type { RunTurnInput, RunTurnResult } from "./protocol.js"; +import { findRollout, inspectPrompt } from "./rollout.js"; + +// The prompt carries a zero-width marker with its promptId, so a re-driven activity can tell +// whether this exact prompt was already recorded. It also tells our prompts apart from the +// context Codex injects as user messages of its own. +const marker = (promptId: string) => `​[codex-temporal:${promptId}]`; + +const heartbeatEvery = (ms: number, details: () => unknown) => { + const timer = setInterval(() => { + try { + Context.current().heartbeat(details()); + } catch { + // outside an activity context (a unit test); ignore + } + }, ms); + timer.unref?.(); + return () => clearInterval(timer); +}; + +const beat = (details: unknown) => { + try { + Context.current().heartbeat(details); + } catch { + // outside an activity context; ignore + } +}; + +// The last heartbeat of the previous attempt. Codex mints the thread id mid-turn, so this is how +// a retry of a first turn learns the id the workflow never got to hear about. +const threadIdFromLastAttempt = (): string | undefined => { + try { + const details = Context.current().info.heartbeatDetails; + const last = Array.isArray(details) ? details[details.length - 1] : details; + return typeof last === "string" && last.length > 0 ? last : undefined; + } catch { + return undefined; + } +}; + +export function makeActivities(cfg: Config) { + const codex = new Codex({ codexPathOverride: cfg.codexPath }); + + const threadOptions = { + workingDirectory: cfg.projectDir, + skipGitRepoCheck: true, + sandboxMode: cfg.sandboxMode, + ...(cfg.model ? { model: cfg.model } : {}), + } as const; + + async function runTurn(input: RunTurnInput): Promise { + let threadId = input.threadId ?? threadIdFromLastAttempt(); + const stop = heartbeatEvery(3000, () => threadId); + + try { + if (threadId) { + const rollout = await findRollout(cfg.sessionsDir, threadId); + const state = rollout ? await inspectPrompt(rollout, marker(input.promptId)) : undefined; + + if (state?.answered) { + // A retry that landed after the turn finished but before its result reached Temporal. + // The answer is already in the log, so there is nothing left to run. + return { threadId, finalResponse: state.lastAgentMessage, ran: false }; + } + if (state?.recorded) { + // The prompt is recorded but the turn never finished. Carry it on rather than asking + // the same thing twice; Codex fills in a tool call whose output never landed. + const turn = await codex.resumeThread(threadId, threadOptions).continueTurn(); + return { + threadId, + finalResponse: turn.finalResponse || state.lastAgentMessage, + ran: true, + }; + } + } + + const thread = threadId + ? codex.resumeThread(threadId, threadOptions) + : codex.startThread(threadOptions); + const { events } = await thread.runStreamed(`${input.text}${marker(input.promptId)}`); + + let finalResponse = ""; + for await (const event of events) { + if (event.type === "thread.started") { + // Heartbeat the id the moment Codex mints it, so a crash from here on resumes this + // thread instead of starting a second one. + threadId = event.thread_id; + beat(threadId); + } else if (event.type === "item.completed" && event.item.type === "agent_message") { + finalResponse = event.item.text; + } else if (event.type === "turn.failed") { + throw new Error(event.error.message); + } + } + + if (!threadId) { + throw new Error("codex ran a turn without reporting a thread id"); + } + return { threadId, finalResponse, ran: true }; + } finally { + stop(); + } + } + + return { runTurn }; +} + +export type Activities = ReturnType; diff --git a/codex-temporal/src/client.ts b/codex-temporal/src/client.ts new file mode 100644 index 000000000000..48d8710bbce3 --- /dev/null +++ b/codex-temporal/src/client.ts @@ -0,0 +1,52 @@ +// Client helpers: submit a prompt to a thread (start the workflow if idle, then signal), read what +// the thread is doing, and interrupt it. The prompt rides a signal-with-start, so a first prompt +// starts the per-thread workflow and later prompts coalesce into the running one. + +import { randomUUID } from "node:crypto"; +import { Client, Connection } from "@temporalio/client"; +import { fromEnv } from "./config.js"; +import { QUERIES, SIGNALS, WORKFLOW_TYPE, workflowId } from "./protocol.js"; +import type { PromptInput, ThreadOptions, ThreadState } from "./protocol.js"; + +export async function connect() { + const cfg = fromEnv(); + const connection = await Connection.connect({ address: cfg.address }); + const client = new Client({ connection, namespace: cfg.namespace }); + return { cfg, client, connection }; +} + +export async function submitPrompt(sessionId: string, text: string, promptId = randomUUID()) { + const { cfg, client, connection } = await connect(); + const prompt: PromptInput = { promptId, text }; + const options: ThreadOptions = { idleTimeout: cfg.idleTimeout }; + try { + await client.workflow.signalWithStart(WORKFLOW_TYPE, { + taskQueue: cfg.taskQueue, + workflowId: workflowId(sessionId), + args: [sessionId, options], + signal: SIGNALS.submitPrompt, + signalArgs: [prompt], + }); + } finally { + await connection.close(); + } + return promptId; +} + +export async function threadState(sessionId: string): Promise { + const { client, connection } = await connect(); + try { + return await client.workflow.getHandle(workflowId(sessionId)).query(QUERIES.threadState); + } finally { + await connection.close(); + } +} + +export async function interrupt(sessionId: string) { + const { client, connection } = await connect(); + try { + await client.workflow.getHandle(workflowId(sessionId)).signal(SIGNALS.interrupt); + } finally { + await connection.close(); + } +} diff --git a/codex-temporal/src/config.ts b/codex-temporal/src/config.ts new file mode 100644 index 000000000000..7ce757d9e314 --- /dev/null +++ b/codex-temporal/src/config.ts @@ -0,0 +1,34 @@ +// Temporal + Codex wiring, read from env. The workflow never reads this (workflow code must stay +// deterministic); the client and the worker do. + +export interface Config { + readonly address: string; + readonly namespace: string; + readonly taskQueue: string; + // Where Codex keeps its rollout logs. That directory is the durable record, so a fleet points + // it at shared storage. + readonly sessionsDir: string; + readonly idleTimeout: string; + // Which codex binary to drive. Defaults to whatever the SDK finds; point it at a fork build to + // drive that instead. + readonly codexPath?: string; + readonly model?: string; + readonly projectDir: string; + // Codex's own sandbox setting for the turn. Left to the caller because a durable executor + // running unattended is exactly where you want to be deliberate about it. + readonly sandboxMode: "read-only" | "workspace-write" | "danger-full-access"; +} + +export function fromEnv(): Config { + return { + address: process.env.TEMPORAL_ADDRESS ?? "127.0.0.1:7233", + namespace: process.env.TEMPORAL_NAMESPACE ?? "default", + taskQueue: process.env.CODEX_TEMPORAL_TASK_QUEUE ?? "codex-thread", + sessionsDir: process.env.CODEX_SESSIONS_DIR ?? `${process.env.HOME}/.codex/sessions`, + idleTimeout: process.env.CODEX_IDLE_TIMEOUT ?? "5 minutes", + codexPath: process.env.CODEX_PATH, + model: process.env.CODEX_MODEL, + projectDir: process.env.CODEX_PROJECT_DIR ?? process.cwd(), + sandboxMode: (process.env.CODEX_SANDBOX as Config["sandboxMode"]) ?? "workspace-write", + }; +} diff --git a/codex-temporal/src/protocol.ts b/codex-temporal/src/protocol.ts new file mode 100644 index 000000000000..fd65b06cdcc4 --- /dev/null +++ b/codex-temporal/src/protocol.ts @@ -0,0 +1,56 @@ +// Names and shapes shared by the workflow, the client, and (type-only) the activities. +// Keep this free of the Codex SDK and Node builtins: the workflow bundles it into the sandbox. + +export const WORKFLOW_TYPE = "codexThread"; +export const WORKFLOW_ID_PREFIX = "codex-thread-"; + +export const workflowId = (sessionId: string) => `${WORKFLOW_ID_PREFIX}${sessionId}`; + +export const SIGNALS = { + submitPrompt: "submitPrompt", + interrupt: "interrupt", +} as const; + +export const QUERIES = { + threadState: "threadState", +} as const; + +export interface PromptInput { + // Deterministic id for this prompt, so a re-driven activity can tell whether it already ran. + readonly promptId: string; + readonly text: string; +} + +export interface RunTurnInput extends PromptInput { + readonly sessionId: string; + // Codex mints the thread id on the first turn, so it is absent then and known after. The + // workflow is what remembers it, which is why it rides in the input. + readonly threadId?: string; +} + +export interface RunTurnResult { + // Codex's id for the thread this turn ran in. The workflow keeps it for later turns. + readonly threadId: string; + readonly finalResponse: string; + // Whether this call drove a turn, or found the prompt already answered and returned the answer. + readonly ran: boolean; +} + +// What the thread is doing, for anyone watching from outside. The conversation itself is in +// Codex's rollout file, not here. +export interface ThreadState { + readonly queued: number; + readonly threadId?: string; + readonly running?: { readonly promptId: string }; + readonly finished?: { + readonly promptId: string; + readonly outcome: "answered" | "interrupted"; + readonly finalResponse: string; + }; +} + +export interface ThreadOptions { + // How long the workflow stays alive with no work before it retires. The next prompt starts a + // fresh run, which rebuilds nothing: Codex's rollout already holds the conversation. + readonly idleTimeout?: string; +} diff --git a/codex-temporal/src/rollout.ts b/codex-temporal/src/rollout.ts new file mode 100644 index 000000000000..93fae282f564 --- /dev/null +++ b/codex-temporal/src/rollout.ts @@ -0,0 +1,162 @@ +// Reading Codex's rollout log. That file is the durable record of a thread, so it is also the +// only honest way to ask "was this prompt already recorded?" after a crash. + +import { readFile, readdir, stat } from "node:fs/promises"; +import { join } from "node:path"; + +interface Line { + readonly type?: string; + readonly payload?: { + readonly type?: string; + readonly role?: string; + readonly content?: unknown; + readonly call_id?: string; + }; +} + +// Codex reaches its tools by several item shapes: the classic function call, the shell call, and +// the custom tool call that code mode uses. +const TOOL_CALLS = new Set(["function_call", "local_shell_call", "custom_tool_call", "tool_search_call"]); +const TOOL_OUTPUTS = new Set([ + "function_call_output", + "custom_tool_call_output", + "tool_search_output", +]); + +const textOf = (content: unknown): string => { + if (typeof content === "string") return content; + if (!Array.isArray(content)) return ""; + return content.map((part) => (part as { text?: string } | null)?.text ?? "").join(""); +}; + +/** Codex writes rollouts under sessions/YYYY/MM/DD, so the thread id has to be searched for. */ +export async function findRollout(sessionsDir: string, threadId: string): Promise { + const suffix = `-${threadId}.jsonl`; + const walk = async (dir: string): Promise => { + let entries: string[]; + try { + entries = await readdir(dir); + } catch { + return undefined; + } + for (const entry of entries) { + const path = join(dir, entry); + if (entry.endsWith(suffix)) return path; + const info = await stat(path).catch(() => undefined); + if (info?.isDirectory()) { + const found = await walk(path); + if (found) return found; + } + } + return undefined; + }; + return walk(sessionsDir); +} + +async function lines(path: string): Promise { + const raw = await readFile(path, "utf8").catch(() => ""); + return raw + .split("\n") + .filter((line) => line.trim().length > 0) + .flatMap((line) => { + try { + return [JSON.parse(line) as Line]; + } catch { + // A crash can leave the last line half-written. Everything before it still counts. + return []; + } + }); +} + +export interface RolloutSummary { + readonly userMessages: number; + readonly agentMessages: number; + readonly toolCalls: number; + readonly toolOutputs: number; + readonly lastAgentMessage: string; +} + +export async function summarize(path: string): Promise { + let userMessages = 0; + let agentMessages = 0; + let toolCalls = 0; + let toolOutputs = 0; + let lastAgentMessage = ""; + + for (const line of await lines(path)) { + if (line.type !== "response_item") continue; + const payload = line.payload; + if (!payload) continue; + if (payload.type === "message" && payload.role === "user") userMessages++; + if (payload.type === "message" && payload.role === "assistant") { + agentMessages++; + const text = textOf(payload.content).trim(); + if (text) lastAgentMessage = text; + } + if (TOOL_CALLS.has(payload.type ?? "")) toolCalls++; + if (TOOL_OUTPUTS.has(payload.type ?? "")) toolOutputs++; + } + + return { userMessages, agentMessages, toolCalls, toolOutputs, lastAgentMessage }; +} + +// Codex renamed these; a rollout written by an older build uses the task_* spelling. +const TURN_DONE = new Set(["turn_complete", "task_complete"]); + +export interface PromptState { + /** Whether a user message carrying this marker is in the rollout at all. */ + readonly recorded: boolean; + /** Whether a turn finished after it, so the answer is already in the log. */ + readonly answered: boolean; + readonly lastAgentMessage: string; +} + +/** What the rollout says about one prompt: never recorded, recorded and answered, or cut short. */ +export async function inspectPrompt(path: string, marker: string): Promise { + let recorded = false; + let answered = false; + let lastAgentMessage = ""; + + for (const line of await lines(path)) { + const payload = line.payload; + if (!payload) continue; + + if ( + !recorded && + line.type === "response_item" && + payload.type === "message" && + payload.role === "user" && + textOf(payload.content).includes(marker) + ) { + recorded = true; + continue; + } + if (!recorded) continue; + + if (line.type === "event_msg" && payload.type && TURN_DONE.has(payload.type)) { + answered = true; + } + if (line.type === "response_item" && payload.type === "message" && payload.role === "assistant") { + const text = textOf(payload.content).trim(); + if (text) lastAgentMessage = text; + } + } + + return { recorded, answered, lastAgentMessage }; +} + +/** Whether the rollout ends with a tool call whose output never landed: a crash mid-turn. */ +export async function hasUnsettledToolCall(path: string): Promise { + const calls = new Set(); + const outputs = new Set(); + for (const line of await lines(path)) { + if (line.type !== "response_item") continue; + const payload = line.payload; + const id = payload?.call_id; + if (!id) continue; + if (TOOL_CALLS.has(payload?.type ?? "")) calls.add(id); + if (TOOL_OUTPUTS.has(payload?.type ?? "")) outputs.add(id); + } + for (const id of calls) if (!outputs.has(id)) return true; + return false; +} diff --git a/codex-temporal/src/worker.ts b/codex-temporal/src/worker.ts new file mode 100644 index 000000000000..6239d91d51e2 --- /dev/null +++ b/codex-temporal/src/worker.ts @@ -0,0 +1,30 @@ +// The worker: hosts the codexThread workflow and the runTurn activity. Run one or many; they pull +// the same task queue, so any worker can drive any thread from the shared sessions directory. + +import { fileURLToPath } from "node:url"; +import { NativeConnection, Worker } from "@temporalio/worker"; +import { fromEnv } from "./config.js"; +import { makeActivities } from "./activities.js"; + +async function main() { + const cfg = fromEnv(); + const connection = await NativeConnection.connect({ address: cfg.address }); + + const worker = await Worker.create({ + connection, + namespace: cfg.namespace, + taskQueue: cfg.taskQueue, + workflowsPath: fileURLToPath(new URL("./workflow.ts", import.meta.url)), + activities: makeActivities(cfg), + }); + + console.log(`codex-temporal worker on ${cfg.address} / ${cfg.namespace} / ${cfg.taskQueue}`); + console.log(`codex: ${cfg.codexPath ?? "(from PATH)"} project: ${cfg.projectDir}`); + console.log(`sessions: ${cfg.sessionsDir} sandbox: ${cfg.sandboxMode}`); + await worker.run(); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/codex-temporal/src/workflow.ts b/codex-temporal/src/workflow.ts new file mode 100644 index 000000000000..ac5eff240893 --- /dev/null +++ b/codex-temporal/src/workflow.ts @@ -0,0 +1,88 @@ +// The per-thread durable executor. One workflow per Codex thread. It owns the small control state +// (the pending-prompt queue, and the thread id Codex minted); the conversation lives in Codex's +// rollout JSONL, which the runTurn activity reads and writes. A crash re-drives the turn in +// flight on another worker. +// +// Sandbox-safe: only @temporalio/workflow and type-only protocol imports. No Codex SDK, no Node. + +import { + proxyActivities, + defineSignal, + defineQuery, + setHandler, + condition, + CancellationScope, + isCancellation, +} from "@temporalio/workflow"; +import { QUERIES, SIGNALS } from "./protocol.js"; +import type { + PromptInput, + RunTurnInput, + RunTurnResult, + ThreadOptions, + ThreadState, +} from "./protocol.js"; + +const { runTurn } = proxyActivities<{ + runTurn(input: RunTurnInput): Promise; +}>({ + // A turn is a whole Codex run (many model calls and tools), so give it room; the heartbeat is + // the real liveness bound and re-drives within seconds of a worker death. + startToCloseTimeout: "1 hour", + heartbeatTimeout: "30 seconds", + retry: { maximumAttempts: 100 }, +}); + +export const submitPrompt = defineSignal<[PromptInput]>(SIGNALS.submitPrompt); +export const interrupt = defineSignal<[]>(SIGNALS.interrupt); +export const threadState = defineQuery(QUERIES.threadState); + +export async function codexThread(sessionId: string, options?: ThreadOptions): Promise { + const idleTimeout = options?.idleTimeout ?? "5 minutes"; + const queue: PromptInput[] = []; + let current: CancellationScope | undefined; + // Codex mints this on the first turn. Keeping it here is what makes later turns resume the same + // thread: the workflow is the durable memory, not the worker. + let threadId: string | undefined; + let running: ThreadState["running"]; + let finished: ThreadState["finished"]; + + setHandler(submitPrompt, (p) => { + queue.push(p); + }); + setHandler(interrupt, () => { + current?.cancel(); + }); + setHandler(threadState, () => ({ queued: queue.length, threadId, running, finished })); + + for (;;) { + const woke = await condition(() => queue.length > 0, idleTimeout); + if (!woke && queue.length === 0) return; // idle: retire; the next prompt starts a fresh run + + const prompt = queue.shift()!; + let outcome: NonNullable["outcome"] = "interrupted"; + let finalResponse = ""; + try { + await CancellationScope.cancellable(async () => { + current = CancellationScope.current(); + running = { promptId: prompt.promptId }; + const input: RunTurnInput = { sessionId, threadId, ...prompt }; + const result = await runTurn(input); + threadId = result.threadId; + finalResponse = result.finalResponse; + outcome = "answered"; + }); + } catch (err) { + // An interrupt cancels the in-flight turn; the thread keeps serving later prompts. A real + // run error is already recorded in the rollout, so we log-and-continue rather than fail the + // whole thread. (Surfacing typed errors to the caller is a follow-up.) + if (!isCancellation(err)) { + // TODO: classify and, for genuine failures, decide retry vs surface. For now, continue. + } + } finally { + current = undefined; + running = undefined; + finished = { promptId: prompt.promptId, outcome, finalResponse }; + } + } +} diff --git a/codex-temporal/state.mts b/codex-temporal/state.mts new file mode 100644 index 000000000000..9b3d7a4f6452 --- /dev/null +++ b/codex-temporal/state.mts @@ -0,0 +1,3 @@ +import { threadState } from "./src/client.js"; +console.log(JSON.stringify(await threadState(process.argv[2]), null, 2)); +process.exit(0); diff --git a/codex-temporal/submit.mts b/codex-temporal/submit.mts new file mode 100644 index 000000000000..9e1a665e1d38 --- /dev/null +++ b/codex-temporal/submit.mts @@ -0,0 +1,5 @@ +import { submitPrompt } from "./src/client.js"; +const [, , sid, text] = process.argv; +const id = await submitPrompt(sid, text); +console.log("promptId", id); +process.exit(0); diff --git a/codex-temporal/tsconfig.json b/codex-temporal/tsconfig.json new file mode 100644 index 000000000000..a06b93bb15f8 --- /dev/null +++ b/codex-temporal/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "lib": ["ES2022"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "noEmit": true, + "types": ["node"], + "baseUrl": ".", + "paths": { + "@openai/codex-sdk": ["../sdk/typescript/src/index.ts"], + "@modelcontextprotocol/sdk/types.js": [ + "./node_modules/@modelcontextprotocol/sdk/dist/esm/types.d.ts" + ] + } + }, + "include": ["src/**/*.ts"] +} From 6d3818607b28ad666f3250bc45ae5ec15f66c993 Mon Sep 17 00:00:00 2001 From: Mohammad Dashti Date: Thu, 20 Aug 2026 02:17:01 -0700 Subject: [PATCH 2/9] Recorded the tool-crash test and what it turned up. --- codex-temporal/README.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/codex-temporal/README.md b/codex-temporal/README.md index 38576cc19239..7ec1b597c4c2 100644 --- a/codex-temporal/README.md +++ b/codex-temporal/README.md @@ -2,7 +2,7 @@ A Temporal-backed durable executor for the [Codex CLI](https://github.com/openai/codex). Same pattern we proved on the OpenCode fork and on Pi: the agent's own loop runs under a durable executor, while the session record stays in the app's own log. -Status: verified end to end against a live Codex built from this fork. Two turns on one thread work, and a worker killed mid-turn finishes that turn on a fresh worker without asking the same thing twice. +Status: verified end to end against a live Codex built from this fork. Two turns on one thread work, and a worker killed mid-tool-call finishes that turn on a fresh worker: no duplicate prompt, and the tool that had already completed does not run again. ## The idea @@ -22,19 +22,26 @@ Two things we had to add to Pi are already in Codex, which is worth saying plain ## What this fork adds -The second one was not reachable from the shipping CLI: `codex exec resume ` always resolves a prompt, so a retry after a mid-turn crash could only ask the same thing twice. This fork exposes it. +**Reaching the empty-input turn.** It was not available from the shipping CLI: `codex exec resume ` always resolves a prompt, so a retry after a mid-turn crash could only ask the same thing twice. - `codex exec resume --continue` runs a turn with no new user message. Use it to finish a turn that stopped part way through. - `Thread.continueTurn()` and `Thread.continueTurnStreamed()` in the TypeScript SDK do the same from code. Both are small: the CLI flag maps to the empty-input turn that already existed, and the SDK method reuses the turn collector that `run()` already had. +**Making a crash-truncated thread resumable at all.** `ensure_call_outputs_present` filled a missing tool output quietly for a function call, but called `error_or_panic` for a custom tool call or a local shell call. Code mode dispatches custom tool calls, so that is the ordinary case: kill Codex during a tool and the thread could not be resumed. A debug build panicked the runtime worker, and the process then sat holding the thread's writer lock, which made every later resume fail with `already has an active writer`. A release build logged an error instead and carried on. + +A gap there is expected, not a defect, so all three now log it the same way. The recovery code was already sitting directly after the panic, which is the tell. + +**Saying what actually happened.** The synthesized output used to read `aborted`. After a hard kill that is optimistic: the command may well have run. It now says the outcome is unknown and the state should be checked before repeating the call. This does not touch the interrupt path, which writes its own richer output (`Wall time: N seconds` plus `aborted by user`); the placeholder is only reached when nothing was recorded at all. + ## What is durable, and what is not - **Between turns: clean.** A worker dying between turns loses nothing. The next prompt drives on any worker, and Codex reads the rollout back. The workflow is what remembers the thread id, so the next turn resumes the same thread rather than starting a new one. - **Mid-turn: the turn is carried on, not restarted.** On a retry, the activity looks for its own prompt in the rollout. Recorded and answered means the answer is already there and nothing needs to run. Recorded and unfinished means `continueTurn()`, so the prompt is not asked twice. - **A first turn that dies before Codex minted a thread id** starts over, which is correct: nothing had been recorded yet. Once the id exists the activity heartbeats it, so a retry from that point resumes rather than opening a second thread. -- **Tool side effects are not rolled back.** A crash between a tool starting and its result landing leaves a tool call with no output, and Codex reports it to the model as `"aborted"`. For a coding agent that is optimistic: after a hard kill the command may well have run. Reporting the outcome as unknown, which is what we did on the Pi fork, is the safer default and is the natural next change here. +- **Tool side effects are not rolled back, and are not repeated either.** A crash between a tool starting and its result landing leaves a tool call with no output. The model is told the outcome is unknown and decides what to do, which is the safe default for a coding agent. Tools that had already completed keep their recorded results and do not run again. +- **The rollout stays honest about the gap.** Codex does not write the synthesized output back, so a thread that survived a crash keeps one more tool call than tool outputs on disk forever. That is the record telling the truth, not a defect. ## Running it @@ -54,9 +61,9 @@ Env: `TEMPORAL_ADDRESS`, `TEMPORAL_NAMESPACE`, `CODEX_TEMPORAL_TASK_QUEUE`, `COD ## Reproducing the crash test -Submit a turn long enough to still be running a few seconds in, wait until the prompt is in the rollout with no turn completion after it, `pkill -9 -f "codex-temporal.*src/worker.ts"`, wait past the 30s heartbeat timeout, then start a fresh worker. The activity comes back on attempt 2, the turn finishes, and the rollout holds exactly one copy of the prompt. +Ask for two shell commands one at a time, the second one slow (`echo first >> a.txt`, then `sleep 30`). Wait until the rollout has more tool calls than tool outputs, so the first tool is settled and the second is in flight. Then `pkill -9 -f "codex-temporal.*src/worker.ts"`, wait past the 30s heartbeat timeout, and start a fresh worker. -The observed run: before the kill, 1 prompt and 0 turn completions; after recovery, 1 prompt and 1 turn completion, with the answer running to the end. +The observed run. Before the kill: 1 prompt, 0 turn completions, 2 tool calls, 1 tool output, and `a.txt` with one line. After recovery: 1 prompt (not asked twice), 1 turn completion, the answer `DELTA`, and `a.txt` still with **one** line, so the completed tool did not run again. The activity comes back on attempt 2. ## Layout @@ -72,4 +79,5 @@ The observed run: before the kill, 1 prompt and 0 turn completions; after recove - **No step level.** A turn is the smallest durable unit here. Codex has no "run one model call and its tools, then stop" surface, so a crash re-drives from wherever the rollout left off rather than from the last step. On the Pi fork we added stepping; the same change here is a core change, not a CLI change. - **A settled tool call is reported as aborted, not unknown.** See above. -- **Tool execution needs a release build.** Codex routes shell tools through a code-mode host that embeds V8, and there is no prebuilt V8 archive for `aarch64-apple-darwin` in this version, so a source build cannot run tool calls. The crash test above therefore uses a long generation rather than a tool call. +- **A source build cannot compile the code-mode host.** Codex routes shell tools through a host that embeds V8, and the rusty_v8 prebuilt for `aarch64-apple-darwin` is a 404 in this version. Copying the prebuilt host out of the `@openai/codex` npm platform package into `codex-rs/target/debug/codex-code-mode-host` works and is what the crash test above ran against. +- **A killed process can leave a thread locked.** The writer lock is an advisory `flock`, so the OS releases it when the process dies. It only blocks a resume while a process still holds it, which is why the panic above was so damaging: the panicking process stayed alive. From ab02e4f582dfccc1f511a6022fcd441888542f3b Mon Sep 17 00:00:00 2001 From: Mohammad Dashti Date: Thu, 20 Aug 2026 02:30:55 -0700 Subject: [PATCH 3/9] Stopped reporting a hung turn as healthy. The heartbeat ran on a timer, so a codex that stopped producing events without dying looked healthy forever and the workflow waited behind it. Temporal recovers from a crash on its own; a hang is the shape that needs help. The activity now heartbeats on progress, kills the child once the gap passes CODEX_STALL_TIMEOUT_MS, and says so, which also frees the thread's lock for the next attempt. Activity cancellation kills the child too, so an interrupt no longer leaves codex running against the thread. --- codex-temporal/README.md | 14 ++++++ codex-temporal/src/activities.ts | 76 +++++++++++++++++++++++++++++--- codex-temporal/src/config.ts | 4 ++ 3 files changed, 89 insertions(+), 5 deletions(-) diff --git a/codex-temporal/README.md b/codex-temporal/README.md index 7ec1b597c4c2..1803d1aaa0a1 100644 --- a/codex-temporal/README.md +++ b/codex-temporal/README.md @@ -33,8 +33,20 @@ Both are small: the CLI flag maps to the empty-input turn that already existed, A gap there is expected, not a defect, so all three now log it the same way. The recovery code was already sitting directly after the panic, which is the tell. +**Not leaving a child behind.** The SDK aborted a turn by sending the child a polite signal and trusting it to exit. A child that ignores it keeps running, and codex holds a writer lock on the thread for as long as it lives, so every later resume of that thread fails with `already has an active writer`. An aborted turn now escalates to `SIGKILL` after a grace period. + **Saying what actually happened.** The synthesized output used to read `aborted`. After a hard kill that is optimistic: the command may well have run. It now says the outcome is unknown and the state should be checked before repeating the call. This does not touch the interrupt path, which writes its own richer output (`Wall time: N seconds` plus `aborted by user`); the placeholder is only reached when nothing was recorded at all. +## A hang is worse than a crash + +Temporal handles a crash: the heartbeat stops, the activity times out, another worker picks the turn up. A hang is the dangerous shape, because a heartbeat on a timer reports a wedged process as healthy forever and the workflow waits behind it. + +So the activity heartbeats on progress, not on the clock. Every event from codex marks progress; if the gap grows past `CODEX_STALL_TIMEOUT_MS` (10 minutes by default, long enough for a slow tool), the child is killed and the activity fails with what happened. Temporal then re-drives the turn, and because the child is gone the thread's lock is free for the next attempt to take. + +The same abort is wired to activity cancellation, so interrupting a turn stops codex rather than leaving it running against the thread. + +Observed against a stand-in that reports a thread and then ignores both events and `SIGTERM`: the activity failed with `codex produced no events for 18s and was killed; the turn will be carried on by the next attempt`, and moved to attempt 2. + ## What is durable, and what is not - **Between turns: clean.** A worker dying between turns loses nothing. The next prompt drives on any worker, and Codex reads the rollout back. The workflow is what remembers the thread id, so the next turn resumes the same thread rather than starting a new one. @@ -81,3 +93,5 @@ The observed run. Before the kill: 1 prompt, 0 turn completions, 2 tool calls, 1 - **A settled tool call is reported as aborted, not unknown.** See above. - **A source build cannot compile the code-mode host.** Codex routes shell tools through a host that embeds V8, and the rusty_v8 prebuilt for `aarch64-apple-darwin` is a 404 in this version. Copying the prebuilt host out of the `@openai/codex` npm platform package into `codex-rs/target/debug/codex-code-mode-host` works and is what the crash test above ran against. - **A killed process can leave a thread locked.** The writer lock is an advisory `flock`, so the OS releases it when the process dies. It only blocks a resume while a process still holds it, which is why the panic above was so damaging: the panicking process stayed alive. +- **Killing the worker itself with `SIGKILL` can leave one codex behind.** The escalation runs in the worker, so a worker that is denied the chance to clean up cannot do it. The orphan holds its thread's lock until it is reaped, which blocks that one thread's retries. A worker that shuts down normally does not have this problem. +- **`codex exec` still waits forever if a turn task dies without saying so.** The panic that used to cause that is fixed, but the wait loop only ends on an event or a closed stream, so any other silent death of the turn task would hang it. The stall timeout above is the reason that is now survivable rather than fatal. diff --git a/codex-temporal/src/activities.ts b/codex-temporal/src/activities.ts index d68fb49cba57..6108251e0482 100644 --- a/codex-temporal/src/activities.ts +++ b/codex-temporal/src/activities.ts @@ -13,14 +13,40 @@ import { findRollout, inspectPrompt } from "./rollout.js"; // context Codex injects as user messages of its own. const marker = (promptId: string) => `​[codex-temporal:${promptId}]`; -const heartbeatEvery = (ms: number, details: () => unknown) => { +const HEARTBEAT_MS = 3000; + +/** + * Heartbeat until the turn goes quiet. + * + * A turn that stops producing events is not progress, however healthy the worker is. Codex can + * hang without dying, and a heartbeat on a timer would report that as healthy forever, pinning + * the workflow. So the caller kills the child once the gap grows too large, which frees the + * thread's writer lock and lets Temporal re-drive the turn somewhere else. + * `progressedAt` is read on every beat, so the caller marks + * progress by updating it; `onStall` is called once when the gap grows too large. + */ +const heartbeatWhileProgressing = ( + details: () => unknown, + progressedAt: () => number, + stallTimeoutMs: number, + onStall: () => void, +) => { + let stalled = false; const timer = setInterval(() => { + if (!stalled && Date.now() - progressedAt() > stallTimeoutMs) { + stalled = true; + onStall(); + return; + } + if (stalled) { + return; + } try { Context.current().heartbeat(details()); } catch { // outside an activity context (a unit test); ignore } - }, ms); + }, HEARTBEAT_MS); timer.unref?.(); return () => clearInterval(timer); }; @@ -33,6 +59,15 @@ const beat = (details: unknown) => { } }; +// Undefined outside an activity context (a unit test), so callers treat it as optional. +const cancellationSignal = (): AbortSignal | undefined => { + try { + return Context.current().cancellationSignal; + } catch { + return undefined; + } +}; + // The last heartbeat of the previous attempt. Codex mints the thread id mid-turn, so this is how // a retry of a first turn learns the id the workflow never got to hear about. const threadIdFromLastAttempt = (): string | undefined => { @@ -57,7 +92,25 @@ export function makeActivities(cfg: Config) { async function runTurn(input: RunTurnInput): Promise { let threadId = input.threadId ?? threadIdFromLastAttempt(); - const stop = heartbeatEvery(3000, () => threadId); + let progressedAt = Date.now(); + + // Kills the codex child: on a stall, and when the workflow interrupts the turn. Without it an + // interrupt would cancel the activity and leave codex running against the same thread. + const child = new AbortController(); + let stalledFor = 0; + const cancellation = cancellationSignal(); + cancellation?.addEventListener("abort", () => child.abort(), { once: true }); + + const stop = heartbeatWhileProgressing( + () => threadId, + () => progressedAt, + cfg.stallTimeoutMs, + () => { + stalledFor = Date.now() - progressedAt; + child.abort(); + }, + ); + const turnOptions = { signal: child.signal }; try { if (threadId) { @@ -72,7 +125,7 @@ export function makeActivities(cfg: Config) { if (state?.recorded) { // The prompt is recorded but the turn never finished. Carry it on rather than asking // the same thing twice; Codex fills in a tool call whose output never landed. - const turn = await codex.resumeThread(threadId, threadOptions).continueTurn(); + const turn = await codex.resumeThread(threadId, threadOptions).continueTurn(turnOptions); return { threadId, finalResponse: turn.finalResponse || state.lastAgentMessage, @@ -84,10 +137,14 @@ export function makeActivities(cfg: Config) { const thread = threadId ? codex.resumeThread(threadId, threadOptions) : codex.startThread(threadOptions); - const { events } = await thread.runStreamed(`${input.text}${marker(input.promptId)}`); + const { events } = await thread.runStreamed( + `${input.text}${marker(input.promptId)}`, + turnOptions, + ); let finalResponse = ""; for await (const event of events) { + progressedAt = Date.now(); if (event.type === "thread.started") { // Heartbeat the id the moment Codex mints it, so a crash from here on resumes this // thread instead of starting a second one. @@ -104,6 +161,15 @@ export function makeActivities(cfg: Config) { throw new Error("codex ran a turn without reporting a thread id"); } return { threadId, finalResponse, ran: true }; + } catch (err) { + if (stalledFor > 0) { + throw new Error( + `codex produced no events for ${Math.round(stalledFor / 1000)}s and was killed; ` + + `the turn will be carried on by the next attempt`, + { cause: err }, + ); + } + throw err; } finally { stop(); } diff --git a/codex-temporal/src/config.ts b/codex-temporal/src/config.ts index 7ce757d9e314..237a1bbb2922 100644 --- a/codex-temporal/src/config.ts +++ b/codex-temporal/src/config.ts @@ -17,6 +17,9 @@ export interface Config { // Codex's own sandbox setting for the turn. Left to the caller because a durable executor // running unattended is exactly where you want to be deliberate about it. readonly sandboxMode: "read-only" | "workspace-write" | "danger-full-access"; + // How long a turn may produce no events before its codex is killed and the turn re-driven. + // Long enough for a slow tool, short enough that a hung codex does not pin the workflow. + readonly stallTimeoutMs: number; } export function fromEnv(): Config { @@ -30,5 +33,6 @@ export function fromEnv(): Config { model: process.env.CODEX_MODEL, projectDir: process.env.CODEX_PROJECT_DIR ?? process.cwd(), sandboxMode: (process.env.CODEX_SANDBOX as Config["sandboxMode"]) ?? "workspace-write", + stallTimeoutMs: Number(process.env.CODEX_STALL_TIMEOUT_MS ?? 10 * 60_000), }; } From f5878f331156d7ce5e73b9b9a7189750c7c4d1b5 Mon Sep 17 00:00:00 2001 From: Mohammad Dashti Date: Thu, 20 Aug 2026 02:42:25 -0700 Subject: [PATCH 4/9] Reaped a codex that outlived its worker. A worker killed outright leaves its codex holding the thread's writer lock, and every later attempt fails with "already has an active writer" until that process finishes. A retry now kills the holder first, but only when it has been reparented, which happens exactly when the worker that spawned it is gone. A codex driven by another live worker fails that test and is left alone. Worth noting a codex mid-stream dies on its own when the pipe closes, so this is the quiet case: the same hang the stall timeout covers, seen from the other side. --- codex-temporal/README.md | 5 +- codex-temporal/src/activities.ts | 24 +++++++++ codex-temporal/src/config.ts | 6 ++- codex-temporal/src/orphans.ts | 83 ++++++++++++++++++++++++++++++++ 4 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 codex-temporal/src/orphans.ts diff --git a/codex-temporal/README.md b/codex-temporal/README.md index 1803d1aaa0a1..df5fd55fb839 100644 --- a/codex-temporal/README.md +++ b/codex-temporal/README.md @@ -45,8 +45,12 @@ So the activity heartbeats on progress, not on the clock. Every event from codex The same abort is wired to activity cancellation, so interrupting a turn stops codex rather than leaving it running against the thread. +A worker killed outright cannot run any of that, so its codex can outlive it. A codex that is mid-stream dies on its own, because writing to the closed pipe kills it; a quiet one, which is the hung case again, keeps running and keeps the thread's writer lock. Every later attempt would then fail with `already has an active writer` until it finishes. So a retry reaps first: it looks for a process holding that thread's lock file and kills it, but only if the process has been reparented, which happens exactly when the worker that spawned it is gone. A codex being driven by another live worker fails that test and is left alone; the attempt fails saying so, rather than fighting over the thread. + Observed against a stand-in that reports a thread and then ignores both events and `SIGTERM`: the activity failed with `codex produced no events for 18s and was killed; the turn will be carried on by the next attempt`, and moved to attempt 2. +Reaping observed against a real thread held by a detached process: a resume failed with `already has an active writer`, the reaper reported `{"killed":[14195],"liveHolders":[]}`, and the same resume then ran to completion. A holder with a live parent came back as `{"killed":[],"liveHolders":[15127]}` and was left running. + ## What is durable, and what is not - **Between turns: clean.** A worker dying between turns loses nothing. The next prompt drives on any worker, and Codex reads the rollout back. The workflow is what remembers the thread id, so the next turn resumes the same thread rather than starting a new one. @@ -93,5 +97,4 @@ The observed run. Before the kill: 1 prompt, 0 turn completions, 2 tool calls, 1 - **A settled tool call is reported as aborted, not unknown.** See above. - **A source build cannot compile the code-mode host.** Codex routes shell tools through a host that embeds V8, and the rusty_v8 prebuilt for `aarch64-apple-darwin` is a 404 in this version. Copying the prebuilt host out of the `@openai/codex` npm platform package into `codex-rs/target/debug/codex-code-mode-host` works and is what the crash test above ran against. - **A killed process can leave a thread locked.** The writer lock is an advisory `flock`, so the OS releases it when the process dies. It only blocks a resume while a process still holds it, which is why the panic above was so damaging: the panicking process stayed alive. -- **Killing the worker itself with `SIGKILL` can leave one codex behind.** The escalation runs in the worker, so a worker that is denied the chance to clean up cannot do it. The orphan holds its thread's lock until it is reaped, which blocks that one thread's retries. A worker that shuts down normally does not have this problem. - **`codex exec` still waits forever if a turn task dies without saying so.** The panic that used to cause that is fixed, but the wait loop only ends on an event or a closed stream, so any other silent death of the turn task would hang it. The stall timeout above is the reason that is now survivable rather than fatal. diff --git a/codex-temporal/src/activities.ts b/codex-temporal/src/activities.ts index 6108251e0482..38b14e47fcb1 100644 --- a/codex-temporal/src/activities.ts +++ b/codex-temporal/src/activities.ts @@ -7,6 +7,7 @@ import { Codex } from "@openai/codex-sdk"; import type { Config } from "./config.js"; import type { RunTurnInput, RunTurnResult } from "./protocol.js"; import { findRollout, inspectPrompt } from "./rollout.js"; +import { reapOrphanedWriter } from "./orphans.js"; // The prompt carries a zero-width marker with its promptId, so a re-driven activity can tell // whether this exact prompt was already recorded. It also tells our prompts apart from the @@ -68,6 +69,14 @@ const cancellationSignal = (): AbortSignal | undefined => { } }; +const attempt = (): number => { + try { + return Context.current().info.attempt; + } catch { + return 1; + } +}; + // The last heartbeat of the previous attempt. Codex mints the thread id mid-turn, so this is how // a retry of a first turn learns the id the workflow never got to hear about. const threadIdFromLastAttempt = (): string | undefined => { @@ -113,6 +122,21 @@ export function makeActivities(cfg: Config) { const turnOptions = { signal: child.signal }; try { + if (threadId && attempt() > 1) { + // A worker killed outright cannot clean up after itself, so its codex may still be + // holding this thread and would block the resume below. + const { killed, liveHolders } = await reapOrphanedWriter(cfg.codexHome, threadId); + if (killed.length > 0) { + console.log(`reaped orphaned codex ${killed.join(", ")} holding thread ${threadId}`); + } + if (liveHolders.length > 0) { + throw new Error( + `thread ${threadId} is held by a live codex (${liveHolders.join(", ")}); ` + + `an earlier attempt is still running somewhere`, + ); + } + } + if (threadId) { const rollout = await findRollout(cfg.sessionsDir, threadId); const state = rollout ? await inspectPrompt(rollout, marker(input.promptId)) : undefined; diff --git a/codex-temporal/src/config.ts b/codex-temporal/src/config.ts index 237a1bbb2922..3f9e6e7cd80a 100644 --- a/codex-temporal/src/config.ts +++ b/codex-temporal/src/config.ts @@ -5,6 +5,8 @@ export interface Config { readonly address: string; readonly namespace: string; readonly taskQueue: string; + // Codex's home. Holds the rollout logs and the per-thread writer locks. + readonly codexHome: string; // Where Codex keeps its rollout logs. That directory is the durable record, so a fleet points // it at shared storage. readonly sessionsDir: string; @@ -23,11 +25,13 @@ export interface Config { } export function fromEnv(): Config { + const codexHome = process.env.CODEX_HOME ?? `${process.env.HOME}/.codex`; return { + codexHome, address: process.env.TEMPORAL_ADDRESS ?? "127.0.0.1:7233", namespace: process.env.TEMPORAL_NAMESPACE ?? "default", taskQueue: process.env.CODEX_TEMPORAL_TASK_QUEUE ?? "codex-thread", - sessionsDir: process.env.CODEX_SESSIONS_DIR ?? `${process.env.HOME}/.codex/sessions`, + sessionsDir: process.env.CODEX_SESSIONS_DIR ?? `${codexHome}/sessions`, idleTimeout: process.env.CODEX_IDLE_TIMEOUT ?? "5 minutes", codexPath: process.env.CODEX_PATH, model: process.env.CODEX_MODEL, diff --git a/codex-temporal/src/orphans.ts b/codex-temporal/src/orphans.ts new file mode 100644 index 000000000000..30bd86fd690e --- /dev/null +++ b/codex-temporal/src/orphans.ts @@ -0,0 +1,83 @@ +// Reaping a codex that outlived its worker. +// +// Codex holds an advisory lock on a thread for as long as it runs, so a worker killed outright +// leaves a child still holding it and the next attempt cannot resume that thread. The orphan does +// finish eventually and free the lock, but a retry should not have to wait for it. +// +// The test for "safe to kill" is narrow on purpose: the process must hold this thread's lock file +// AND have been reparented, which only happens once the worker that spawned it is gone. A codex +// being driven by another live worker fails that test and is left alone. + +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import { join } from "node:path"; + +const run = promisify(execFile); + +const LOCK_DIR = "thread-writer-locks"; +const ORPHAN_PPID = 1; +const FREED_TIMEOUT_MS = 10_000; + +const lockPath = (codexHome: string, threadId: string) => + join(codexHome, LOCK_DIR, `${threadId}.lock`); + +/** Pids holding the lock file, or an empty list when nothing does (or lsof is unavailable). */ +async function holders(path: string): Promise { + try { + const { stdout } = await run("lsof", ["-t", path]); + return stdout + .split("\n") + .map((line) => Number.parseInt(line.trim(), 10)) + .filter((pid) => Number.isInteger(pid) && pid > 0); + } catch { + // lsof exits non-zero when no process holds the file, which is the common case. + return []; + } +} + +async function parentOf(pid: number): Promise { + try { + const { stdout } = await run("ps", ["-o", "ppid=", "-p", String(pid)]); + const ppid = Number.parseInt(stdout.trim(), 10); + return Number.isInteger(ppid) ? ppid : undefined; + } catch { + return undefined; + } +} + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +export interface ReapResult { + readonly killed: number[]; + /** Held by something still parented, so it is somebody else's live turn. Left alone. */ + readonly liveHolders: number[]; +} + +/** Kill any orphaned codex holding this thread's lock, and wait for the lock to come free. */ +export async function reapOrphanedWriter(codexHome: string, threadId: string): Promise { + const path = lockPath(codexHome, threadId); + const killed: number[] = []; + const liveHolders: number[] = []; + + for (const pid of await holders(path)) { + if ((await parentOf(pid)) !== ORPHAN_PPID) { + liveHolders.push(pid); + continue; + } + try { + process.kill(pid, "SIGKILL"); + killed.push(pid); + } catch { + // Gone between the two calls, which is the outcome we wanted anyway. + } + } + + if (killed.length > 0) { + const deadline = Date.now() + FREED_TIMEOUT_MS; + while (Date.now() < deadline && (await holders(path)).length > 0) { + await sleep(200); + } + } + + return { killed, liveHolders }; +} From 7c28d47d16b0bffabcc56d4efc58af96823eb9e8 Mon Sep 17 00:00:00 2001 From: Mohammad Dashti Date: Thu, 20 Aug 2026 11:45:07 -0700 Subject: [PATCH 5/9] Reported how far a turn has got. A turn is the durable unit, so a step was invisible from outside without reading the rollout. The heartbeat now carries the items codex has finished and the last one's type, which is what `temporal workflow describe` shows. --- codex-temporal/README.md | 6 +++++- codex-temporal/src/activities.ts | 31 ++++++++++++++++++++++++++----- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/codex-temporal/README.md b/codex-temporal/README.md index df5fd55fb839..82a5a073051a 100644 --- a/codex-temporal/README.md +++ b/codex-temporal/README.md @@ -75,6 +75,8 @@ Env: `TEMPORAL_ADDRESS`, `TEMPORAL_NAMESPACE`, `CODEX_TEMPORAL_TASK_QUEUE`, `COD `temporal workflow query --workflow-id codex-thread- --name threadState` reports the queue, the thread id, the turn in flight, and how the last turn ended. +`temporal workflow describe` shows how far the running turn has got, because the heartbeat carries it: `[{"items":6,"lastItem":"command_execution","threadId":"..."}]`. Heartbeats are throttled against the heartbeat timeout, so it moves in jumps rather than on every item. + ## Reproducing the crash test Ask for two shell commands one at a time, the second one slow (`echo first >> a.txt`, then `sleep 30`). Wait until the rollout has more tool calls than tool outputs, so the first tool is settled and the second is in flight. Then `pkill -9 -f "codex-temporal.*src/worker.ts"`, wait past the 30s heartbeat timeout, and start a fresh worker. @@ -93,7 +95,9 @@ The observed run. Before the kill: 1 prompt, 0 turn completions, 2 tool calls, 1 ## Known gaps -- **No step level.** A turn is the smallest durable unit here. Codex has no "run one model call and its tools, then stop" surface, so a crash re-drives from wherever the rollout left off rather than from the last step. On the Pi fork we added stepping; the same change here is a core change, not a CLI change. +- **No step level, and it costs less than it sounds.** A turn is the smallest durable unit here, but that is not the same gap it was on the Pi fork. Codex records each item to the rollout as it happens, and a resumed turn picks up from there, so the work lost to a crash is already about one step: the tool-crash test above finished the turn with the completed tool's side effect intact and not repeated. What a step-per-activity design would add is control and visibility, not saved work, and the visibility half is covered by the progress on the heartbeat. + + The remaining half is not cheap. `run_turn` does have a step loop, so stopping after one step is a small change on its own, but a turn that stops early is not a completed turn, and this protocol has no way to say that: the app-server would emit `turn/completed` for something still mid-conversation. Making it honest means a new terminal turn status threaded through the app-server, both SDKs, the rollout projection that reconstructs turns (`build_turns_from_rollout_items`, which fork and rollback depend on), and the TUI. That is a large, invasive change for control this executor does not currently need, so it is deliberately not built. - **A settled tool call is reported as aborted, not unknown.** See above. - **A source build cannot compile the code-mode host.** Codex routes shell tools through a host that embeds V8, and the rusty_v8 prebuilt for `aarch64-apple-darwin` is a 404 in this version. Copying the prebuilt host out of the `@openai/codex` npm platform package into `codex-rs/target/debug/codex-code-mode-host` works and is what the crash test above ran against. - **A killed process can leave a thread locked.** The writer lock is an advisory `flock`, so the OS releases it when the process dies. It only blocks a resume while a process still holds it, which is why the panic above was so damaging: the panicking process stayed alive. diff --git a/codex-temporal/src/activities.ts b/codex-temporal/src/activities.ts index 38b14e47fcb1..2d0ffb9f584e 100644 --- a/codex-temporal/src/activities.ts +++ b/codex-temporal/src/activities.ts @@ -77,13 +77,27 @@ const attempt = (): number => { } }; +// What a turn reports while it runs. It rides the heartbeat, so `temporal workflow describe` +// shows which step a turn is on without anyone reading the rollout. A turn is the durable unit +// here, so this is how a step is visible at all. +interface TurnProgress { + readonly threadId?: string; + // Items codex has finished in this turn: roughly, the steps taken. + readonly items: number; + readonly lastItem?: string; +} + // The last heartbeat of the previous attempt. Codex mints the thread id mid-turn, so this is how // a retry of a first turn learns the id the workflow never got to hear about. const threadIdFromLastAttempt = (): string | undefined => { try { const details = Context.current().info.heartbeatDetails; const last = Array.isArray(details) ? details[details.length - 1] : details; - return typeof last === "string" && last.length > 0 ? last : undefined; + if (typeof last === "string") { + return last.length > 0 ? last : undefined; + } + const threadId = (last as TurnProgress | undefined)?.threadId; + return typeof threadId === "string" && threadId.length > 0 ? threadId : undefined; } catch { return undefined; } @@ -102,6 +116,9 @@ export function makeActivities(cfg: Config) { async function runTurn(input: RunTurnInput): Promise { let threadId = input.threadId ?? threadIdFromLastAttempt(); let progressedAt = Date.now(); + const progress = (): TurnProgress => ({ threadId, items, lastItem }); + let items = 0; + let lastItem: string | undefined; // Kills the codex child: on a stall, and when the workflow interrupts the turn. Without it an // interrupt would cancel the activity and leave codex running against the same thread. @@ -111,7 +128,7 @@ export function makeActivities(cfg: Config) { cancellation?.addEventListener("abort", () => child.abort(), { once: true }); const stop = heartbeatWhileProgressing( - () => threadId, + progress, () => progressedAt, cfg.stallTimeoutMs, () => { @@ -173,9 +190,13 @@ export function makeActivities(cfg: Config) { // Heartbeat the id the moment Codex mints it, so a crash from here on resumes this // thread instead of starting a second one. threadId = event.thread_id; - beat(threadId); - } else if (event.type === "item.completed" && event.item.type === "agent_message") { - finalResponse = event.item.text; + beat(progress()); + } else if (event.type === "item.completed") { + items++; + lastItem = event.item.type; + if (event.item.type === "agent_message") { + finalResponse = event.item.text; + } } else if (event.type === "turn.failed") { throw new Error(event.error.message); } From 26b817280c5a54393aa44e3bfb9795276ffcb2a1 Mon Sep 17 00:00:00 2001 From: Mohammad Dashti Date: Thu, 20 Aug 2026 12:16:06 -0700 Subject: [PATCH 6/9] Skipped npm lockfiles in the spelling check. The integrity hashes in a package-lock.json read as misspelled words, and any npm lockfile will trip on that sooner or later. pnpm-lock.yaml was already skipped through the *-lock.yaml pattern; npm's name matches neither of the existing ones. --- .codespellrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.codespellrc b/.codespellrc index 838b7e874e46..a607b36eef8e 100644 --- a/.codespellrc +++ b/.codespellrc @@ -1,6 +1,6 @@ [codespell] # Ref: https://github.com/codespell-project/codespell#using-a-config-file -skip = .git*,vendor,*-lock.yaml,*.lock,.codespellrc,*test.ts,*.jsonl,frame*.txt,*.snap,*.snap.new +skip = .git*,vendor,*-lock.yaml,*.lock,package-lock.json,.codespellrc,*test.ts,*.jsonl,frame*.txt,*.snap,*.snap.new check-hidden = true ignore-regex = ^\s*"image/\S+": ".*|\b(afterAll)\b ignore-words-list = ratatui,ser,iTerm,iterm2,iterm,te,TE,PASE,SEH From 196aeeb972a0e3380c11922f94e45c09234f1e5a Mon Sep 17 00:00:00 2001 From: Mohammad Dashti Date: Thu, 20 Aug 2026 15:20:29 -0700 Subject: [PATCH 7/9] Added dev scripts to bring the stack up and crash it. The two crash tests are the point of this package, and reproducing them by hand took a page of instructions and a broad pkill that took down other people's workers. Ports and state sit off the defaults now, and every process these scripts start is tracked by pid. --- codex-temporal/dev/ask.sh | 24 ++++++ codex-temporal/dev/attempt.py | 19 +++++ codex-temporal/dev/common.sh | 131 +++++++++++++++++++++++++++++ codex-temporal/dev/count-prompt.py | 41 +++++++++ codex-temporal/dev/crash-tool.sh | 78 +++++++++++++++++ codex-temporal/dev/crash-worker.sh | 58 +++++++++++++ codex-temporal/dev/find-rollout.py | 27 ++++++ codex-temporal/dev/finished.py | 16 ++++ codex-temporal/dev/inspect.sh | 14 +++ codex-temporal/dev/setup.sh | 29 +++++++ codex-temporal/dev/state.sh | 11 +++ codex-temporal/dev/stop.sh | 21 +++++ codex-temporal/dev/temporal.sh | 22 +++++ codex-temporal/dev/worker.sh | 12 +++ 14 files changed, 503 insertions(+) create mode 100755 codex-temporal/dev/ask.sh create mode 100644 codex-temporal/dev/attempt.py create mode 100755 codex-temporal/dev/common.sh create mode 100644 codex-temporal/dev/count-prompt.py create mode 100755 codex-temporal/dev/crash-tool.sh create mode 100755 codex-temporal/dev/crash-worker.sh create mode 100644 codex-temporal/dev/find-rollout.py create mode 100644 codex-temporal/dev/finished.py create mode 100755 codex-temporal/dev/inspect.sh create mode 100755 codex-temporal/dev/setup.sh create mode 100755 codex-temporal/dev/state.sh create mode 100755 codex-temporal/dev/stop.sh create mode 100755 codex-temporal/dev/temporal.sh create mode 100755 codex-temporal/dev/worker.sh diff --git a/codex-temporal/dev/ask.sh b/codex-temporal/dev/ask.sh new file mode 100755 index 000000000000..49e578e92093 --- /dev/null +++ b/codex-temporal/dev/ask.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Submit a prompt to a thread and wait for the answer. +# dev/ask.sh "Reply with the single word BRAVO." +# dev/ask.sh my-session "and what did you just say?" +set -euo pipefail +# shellcheck source=./common.sh +source "$(cd -- "$(dirname -- "$0")" && pwd)/common.sh" + +if [ $# -eq 1 ]; then session=dev; text=$1; else session=${1:?session}; text=${2:?prompt}; fi +require_temporal +timeout=${ASK_TIMEOUT:-600} + +promptId=$(tsx submit.mts "$session" "$text" | awk '{print $2}') +say "session $session prompt $promptId" + +deadline=$((SECONDS + timeout)) +while [ "$SECONDS" -lt "$deadline" ]; do + if state=$(tsx state.mts "$session" 2>/dev/null); then + remember_thread "$session" "$(printf '%s' "$state" | json_get threadId)" + if printf '%s' "$state" | python3 "$DEV_DIR/finished.py" "$promptId"; then exit 0; fi + fi + sleep 2 +done +die "no answer in ${timeout}s for prompt $promptId. Worker log: $LOG_DIR/worker.log" diff --git a/codex-temporal/dev/attempt.py b/codex-temporal/dev/attempt.py new file mode 100644 index 000000000000..8b2b8339429c --- /dev/null +++ b/codex-temporal/dev/attempt.py @@ -0,0 +1,19 @@ +"""Print the attempt the activity was on when it last started. + +Temporal does not add a history event per retry: the ActivityTaskStarted event carries the attempt +of the attempt that finished. So a 2 here is the proof that a turn was re-driven, and it is the one +claim a crash test cannot fake by finishing early. +""" + +import json +import sys + +history = json.load(sys.stdin) +events = history.get("events") or history.get("history", {}).get("events", []) + +attempt = 0 +for event in events: + started = event.get("activityTaskStartedEventAttributes") + if started: + attempt = started.get("attempt", 1) +print(attempt) diff --git a/codex-temporal/dev/common.sh b/codex-temporal/dev/common.sh new file mode 100755 index 000000000000..c8000dbb2715 --- /dev/null +++ b/codex-temporal/dev/common.sh @@ -0,0 +1,131 @@ +# Shared wiring for the dev stack. Sourced, never run. +# +# Two rules hold this together. Ports and state live off the defaults, so this cannot disturb a +# Temporal or a Codex you already run. And every process we start is tracked by pid, so nothing +# here ever matches on a command line: a broad pkill would take down another session's worker. + +# shellcheck shell=bash + +DEV_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +PKG_DIR=$(cd -- "$DEV_DIR/.." && pwd) +REPO_DIR=$(cd -- "$PKG_DIR/.." && pwd) + +STATE_DIR=${CODEX_TEMPORAL_DEV_DIR:-/tmp/codex-temporal-dev} +PID_DIR="$STATE_DIR/pids" +LOG_DIR="$STATE_DIR/logs" +THREAD_DIR="$STATE_DIR/threads" + +TEMPORAL_PORT=${TEMPORAL_PORT:-7244} +TEMPORAL_UI_PORT=${TEMPORAL_UI_PORT:-8244} + +export TEMPORAL_ADDRESS=${TEMPORAL_ADDRESS:-127.0.0.1:$TEMPORAL_PORT} +export TEMPORAL_NAMESPACE=${TEMPORAL_NAMESPACE:-default} +export CODEX_TEMPORAL_TASK_QUEUE=${CODEX_TEMPORAL_TASK_QUEUE:-codex-thread-dev} + +# The worker needs a build from this fork, because it calls `--continue`. +export CODEX_PATH=${CODEX_PATH:-$REPO_DIR/codex-rs/target/debug/codex} +# Tools run here, so keep them out of the repo. +export CODEX_PROJECT_DIR=${CODEX_PROJECT_DIR:-$STATE_DIR/work} +export CODEX_SANDBOX=${CODEX_SANDBOX:-workspace-write} + +mkdir -p "$PID_DIR" "$LOG_DIR" "$THREAD_DIR" "$CODEX_PROJECT_DIR" + +say() { printf '%s\n' "$*" >&2; } +die() { say "$*"; exit 1; } + +port_taken() { lsof -ti "tcp:$1" -sTCP:LISTEN >/dev/null 2>&1; } + +record_pid() { printf '%s\n' "$2" >"$PID_DIR/$1.pid"; } +read_pid() { cat "$PID_DIR/$1.pid" 2>/dev/null || true; } +alive() { [ -n "${1:-}" ] && kill -0 "$1" 2>/dev/null; } + +# Depth-first so children are named before their parent. Only ever called on a pid we recorded, +# which is what keeps another session's tree out of reach. +descendants() { + local kid + for kid in $(pgrep -P "$1" 2>/dev/null); do + descendants "$kid" + printf '%s\n' "$kid" + done +} + +# Stop a recorded process and its children. `tsx` runs the worker in a child, so the pid we own is +# not always the node process doing the work. +stop_tree() { + local name=$1 sig=${2:--TERM} pid kid + pid=$(read_pid "$name") + if alive "$pid"; then + for kid in $(descendants "$pid"); do kill "$sig" "$kid" 2>/dev/null || true; done + kill "$sig" "$pid" 2>/dev/null || true + local i + for i in $(seq 40); do alive "$pid" || break; sleep 0.25; done + fi + rm -f "$PID_DIR/$name.pid" +} + +require_temporal() { + port_taken "$TEMPORAL_PORT" || die "no Temporal on $TEMPORAL_ADDRESS. Run dev/temporal.sh first." +} + +start_worker() { + stop_tree worker -KILL + : >"$LOG_DIR/worker.log" + "$DEV_DIR/worker.sh" >>"$LOG_DIR/worker.log" 2>&1 & + record_pid worker $! + local i + for i in $(seq 60); do + grep -q 'codex-temporal worker on' "$LOG_DIR/worker.log" 2>/dev/null && return 0 + sleep 0.5 + done + say "worker did not come up; last lines:"; tail -5 "$LOG_DIR/worker.log" >&2 + return 1 +} + +tsx() { (cd "$PKG_DIR" && ./node_modules/.bin/tsx "$@"); } + +# The thread id is Codex's, and it is the only way into the rollout once no worker is left to +# answer a query. Cache it the moment we see one. +remember_thread() { + if [ -n "${2:-}" ] && [ "$2" != null ]; then printf '%s\n' "$2" >"$THREAD_DIR/$1"; fi + return 0 +} +recall_thread() { cat "$THREAD_DIR/$1" 2>/dev/null; } + +# Find the rollout for a prompt while the turn is still running. Poll for the file rather than the +# workflow's thread id: the workflow only learns that from the activity's result, so it arrives too +# late to kill anything mid-turn. +wait_rollout() { + local token=$1 tries=${2:-120} out + local i + for i in $(seq "$tries"); do + if out=$(python3 "$DEV_DIR/find-rollout.py" "${CODEX_SESSIONS_DIR:-${CODEX_HOME:-$HOME/.codex}/sessions}" "$token" 2>/dev/null); then + printf '%s\n' "$out" + return 0 + fi + sleep 1 + done + return 1 +} + +# Refuse to kill anything unless the turn is genuinely in flight. A turn that already finished makes +# every later assertion pass for the wrong reason, which is the trap this whole check exists for. +turn_in_flight() { + local session=$1 promptId=$2 state + state=$(tsx state.mts "$session" 2>/dev/null) || return 1 + printf '%s' "$state" | python3 -c ' +import json, sys +state = json.load(sys.stdin) +prompt = sys.argv[1] +running = (state.get("running") or {}).get("promptId") +done = (state.get("finished") or {}).get("promptId") +sys.exit(0 if running == prompt and done != prompt else 1) +' "$promptId" +} + +# A 2 here means Temporal re-drove the turn. It is the one claim an early-finishing turn cannot fake. +activity_attempt() { + temporal workflow show --workflow-id "codex-thread-$1" --output json 2>/dev/null \ + | python3 "$DEV_DIR/attempt.py" +} + +json_get() { python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get(sys.argv[1]) if d.get(sys.argv[1]) is not None else "")' "$1"; } diff --git a/codex-temporal/dev/count-prompt.py b/codex-temporal/dev/count-prompt.py new file mode 100644 index 000000000000..969dede2a3d9 --- /dev/null +++ b/codex-temporal/dev/count-prompt.py @@ -0,0 +1,41 @@ +"""Count the user messages in a rollout that carry a token, and report the tool-call balance. + +Counting *all* user messages would lie: Codex injects its own context as user-role messages, and it +does so again on a resume. A token we put in the prompt text is the only thing that tells our own +prompt apart, which is what makes "was it asked twice?" answerable. +""" + +import json +import sys + +path, token = sys.argv[1], sys.argv[2] + +TOOL_CALLS = {"function_call", "custom_tool_call", "local_shell_call"} +TOOL_OUTPUTS = {"function_call_output", "custom_tool_call_output"} + +prompts = calls = outputs = 0 +last_agent = "" + +with open(path, encoding="utf-8") as fh: + for line in fh: + try: + entry = json.loads(line) + except ValueError: + continue + if entry.get("type") != "response_item": + continue + payload = entry.get("payload") or {} + kind = payload.get("type") + if kind == "message" and payload.get("role") == "user": + if token in json.dumps(payload.get("content")): + prompts += 1 + elif kind == "message" and payload.get("role") == "assistant": + text = json.dumps(payload.get("content")) + if text: + last_agent = text + if kind in TOOL_CALLS: + calls += 1 + if kind in TOOL_OUTPUTS: + outputs += 1 + +print(json.dumps({"prompts": prompts, "toolCalls": calls, "toolOutputs": outputs, "lastAgent": last_agent[-200:]})) diff --git a/codex-temporal/dev/crash-tool.sh b/codex-temporal/dev/crash-tool.sh new file mode 100755 index 000000000000..95ab78456c44 --- /dev/null +++ b/codex-temporal/dev/crash-tool.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# The test worth running: kill the worker with one tool call settled and another in flight. +# +# It asserts the thing a durable executor has to get right. The finished tool keeps its recorded +# result and does not run again, so `a.txt` still holds one line afterwards. That needs the +# missing-output fix as well: without it, resuming a thread with a dangling custom tool call panics +# and the process sits on the thread's writer lock. +set -euo pipefail +# shellcheck source=./common.sh +source "$(cd -- "$(dirname -- "$0")" && pwd)/common.sh" + +require_temporal +host="$(dirname "$CODEX_PATH")/codex-code-mode-host" +[ -x "$host" ] || die "no codex-code-mode-host at $host, so tool calls cannot run. See dev/setup.sh." + +session=crash-tool-$$ +token=CRASHT-$$ +work=$CODEX_PROJECT_DIR +rm -f "$work/a.txt" +say "== session $session work dir $work" + +start_worker +promptId=$(tsx submit.mts "$session" \ + "Use the shell, one command at a time and in this order. First run: echo first >> a.txt +Wait for it to finish, then run: sleep 90 +When both are done, reply with the single word DELTA. ($token)" | awk '{print $2}') +say "prompt $promptId" + +found=$(wait_rollout "$token") || die "no rollout for $token in 120s. Worker log: $LOG_DIR/worker.log" +path=$(printf '%s' "$found" | json_get path) +thread=$(printf '%s' "$found" | json_get threadId) +remember_thread "$session" "$thread" +say "thread $thread" + +# More tool calls than outputs means the first one settled and the second is still running. That is +# the only window where this test proves anything. +say "waiting for a tool call to be in flight" +for _ in $(seq 240); do + now=$(python3 "$DEV_DIR/count-prompt.py" "$path" "$token") + calls=$(printf '%s' "$now" | json_get toolCalls) + outs=$(printf '%s' "$now" | json_get toolOutputs) + if [ "${calls:-0}" -gt "${outs:-0}" ]; then break; fi + sleep 1 +done + +before=$(python3 "$DEV_DIR/count-prompt.py" "$path" "$token") +lines_before=$(wc -l <"$work/a.txt" 2>/dev/null | tr -d ' ' || echo 0) +say "before the kill: $before a.txt lines: $lines_before" +[ "$(printf '%s' "$before" | json_get toolCalls)" -gt "$(printf '%s' "$before" | json_get toolOutputs)" ] \ + || die "no tool call in flight, so the kill would prove nothing. Try again." +turn_in_flight "$session" "$promptId" || die "the turn is not in flight any more. Try again." + +say "SIGKILL worker $(read_pid worker)" +stop_tree worker -KILL + +say "waiting 35s for the heartbeat timeout to lapse" +sleep 35 +start_worker + +for _ in $(seq 250); do + state=$(tsx state.mts "$session" 2>/dev/null) || { sleep 2; continue; } + if printf '%s' "$state" | python3 "$DEV_DIR/finished.py" "$promptId"; then break; fi + sleep 2 +done + +after=$(python3 "$DEV_DIR/count-prompt.py" "$path" "$token") +lines_after=$(wc -l <"$work/a.txt" 2>/dev/null | tr -d ' ' || echo 0) +attempt=$(activity_attempt "$session") +say "after recovery: $after a.txt lines: $lines_after" +say "activity attempt: $attempt" + +fail=0 +[ "$(printf '%s' "$after" | json_get prompts)" = 1 ] || { say "FAIL: the prompt was asked more than once"; fail=1; } +[ "${lines_after:-0}" -eq 1 ] || { say "FAIL: a.txt has $lines_after lines, so a settled tool ran again"; fail=1; } +[ "${attempt:-0}" -ge 2 ] || { say "FAIL: activity finished on attempt $attempt, so the turn was never re-driven"; fail=1; } +printf '%s' "$after" | grep -q DELTA || { say "FAIL: the turn did not finish"; fail=1; } +if [ "$fail" = 0 ]; then say "PASS: one prompt, the settled tool did not re-run, re-driven on attempt $attempt."; fi +exit "$fail" diff --git a/codex-temporal/dev/crash-worker.sh b/codex-temporal/dev/crash-worker.sh new file mode 100755 index 000000000000..470bfc5dbda4 --- /dev/null +++ b/codex-temporal/dev/crash-worker.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Kill the worker in the middle of a turn and show the turn finish on a fresh one. +# +# No tool call here, so this isolates two claims: the prompt is not asked twice, and Temporal +# re-drove the turn. The second one needs the attempt number, because a turn that quietly finished +# before the kill would satisfy everything else. +set -euo pipefail +# shellcheck source=./common.sh +source "$(cd -- "$(dirname -- "$0")" && pwd)/common.sh" + +require_temporal +session=crash-worker-$$ +token=CRASHW-$$ +say "== session $session" + +start_worker +promptId=$(tsx submit.mts "$session" \ + "Count from 1 to 2000, one number per line, then reply with the single word CHARLIE. ($token)" \ + | awk '{print $2}') +say "prompt $promptId" + +found=$(wait_rollout "$token") || die "no rollout for $token in 120s. Worker log: $LOG_DIR/worker.log" +path=$(printf '%s' "$found" | json_get path) +thread=$(printf '%s' "$found" | json_get threadId) +remember_thread "$session" "$thread" +say "thread $thread" + +turn_in_flight "$session" "$promptId" \ + || die "the turn is not in flight any more, so a kill would prove nothing. Try a longer prompt." + +before=$(python3 "$DEV_DIR/count-prompt.py" "$path" "$token") +say "before the kill: $before" +say "SIGKILL worker $(read_pid worker)" +stop_tree worker -KILL + +# The activity has a 30s heartbeat timeout, so nothing can be re-driven before it lapses. +say "waiting 35s for the heartbeat timeout to lapse" +sleep 35 +start_worker + +for _ in $(seq 200); do + state=$(tsx state.mts "$session" 2>/dev/null) || { sleep 2; continue; } + if printf '%s' "$state" | python3 "$DEV_DIR/finished.py" "$promptId"; then break; fi + sleep 2 +done + +after=$(python3 "$DEV_DIR/count-prompt.py" "$path" "$token") +attempt=$(activity_attempt "$session") +say "after recovery: $after" +say "activity attempt: $attempt" + +fail=0 +[ "$(printf '%s' "$before" | json_get prompts)" = 1 ] || { say "FAIL: prompt not recorded once before the kill"; fail=1; } +[ "$(printf '%s' "$after" | json_get prompts)" = 1 ] || { say "FAIL: the prompt was asked more than once"; fail=1; } +[ "${attempt:-0}" -ge 2 ] || { say "FAIL: activity finished on attempt $attempt, so the turn was never re-driven"; fail=1; } +printf '%s' "$after" | grep -q CHARLIE || { say "FAIL: no answer in the rollout"; fail=1; } +if [ "$fail" = 0 ]; then say "PASS: one prompt, re-driven on attempt $attempt, turn finished."; fi +exit "$fail" diff --git a/codex-temporal/dev/find-rollout.py b/codex-temporal/dev/find-rollout.py new file mode 100644 index 000000000000..356af3c7a671 --- /dev/null +++ b/codex-temporal/dev/find-rollout.py @@ -0,0 +1,27 @@ +"""Find the rollout a prompt landed in, by scanning for a token in the prompt text. + +The workflow only learns the thread id from the activity's result, so asking it mid-turn tells you +nothing. Codex writes the rollout as the turn happens, and names the file after the thread, so the +sessions directory answers the question while the turn is still running, and after the worker dies. +""" + +import json +import pathlib +import re +import sys + +sessions, token = pathlib.Path(sys.argv[1]), sys.argv[2] + +candidates = sorted(sessions.rglob("rollout-*.jsonl"), key=lambda p: p.stat().st_mtime, reverse=True) +for path in candidates[:200]: + try: + if token not in path.read_text(encoding="utf-8", errors="ignore"): + continue + except OSError: + continue + # rollout--.jsonl, and the timestamp is dashed too, so anchor on the uuid. + match = re.search(r"([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$", path.stem) + print(json.dumps({"path": str(path), "threadId": match.group(1) if match else ""})) + sys.exit(0) + +sys.exit(1) diff --git a/codex-temporal/dev/finished.py b/codex-temporal/dev/finished.py new file mode 100644 index 000000000000..35df1d76e919 --- /dev/null +++ b/codex-temporal/dev/finished.py @@ -0,0 +1,16 @@ +"""Exit 0 and print the answer once the thread reports our prompt finished. + +Reads a threadState query result on stdin. Keying on the prompt id matters: a session is reused +across turns, so a stale `finished` from the previous turn is the easy thing to mistake for ours. +""" + +import json +import sys + +state = json.load(sys.stdin) +finished = state.get("finished") or {} +if finished.get("promptId") != sys.argv[1]: + sys.exit(1) + +print(finished.get("outcome", "")) +print(finished.get("finalResponse", "")) diff --git a/codex-temporal/dev/inspect.sh b/codex-temporal/dev/inspect.sh new file mode 100755 index 000000000000..61462e7ebb5a --- /dev/null +++ b/codex-temporal/dev/inspect.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# Summarize the thread's rollout: prompts, tool-call balance, last answer. +# +# This reads Codex's own log off disk, so it works with no worker and no Temporal running. That is +# the point: after killing a worker, the rollout is the only honest witness left. +set -euo pipefail +# shellcheck source=./common.sh +source "$(cd -- "$(dirname -- "$0")" && pwd)/common.sh" + +arg=${1:-dev} +thread=$(recall_thread "$arg" || true) +[ -n "$thread" ] || thread=$arg # accept a thread id directly + +tsx inspect.mts "$thread" diff --git a/codex-temporal/dev/setup.sh b/codex-temporal/dev/setup.sh new file mode 100755 index 000000000000..12b8f0f82397 --- /dev/null +++ b/codex-temporal/dev/setup.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Install deps and check the things whose absence gives a confusing failure later. +set -euo pipefail +# shellcheck source=./common.sh +source "$(cd -- "$(dirname -- "$0")" && pwd)/common.sh" + +(cd "$PKG_DIR" && npm install --silent) + +command -v temporal >/dev/null || die "no temporal CLI on PATH. See https://docs.temporal.io/cli" +command -v python3 >/dev/null || die "no python3 on PATH; the dev scripts read rollouts with it" + +[ -x "$CODEX_PATH" ] || die "no codex build at $CODEX_PATH +Build it: cd $REPO_DIR/codex-rs && CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build -p codex-cli --bin codex" + +"$CODEX_PATH" exec resume --help 2>/dev/null | grep -q -- --continue \ + || die "$CODEX_PATH has no 'exec resume --continue'. That build is not from this fork." + +# Codex routes shell tools through a host that embeds V8, and the rusty_v8 prebuilt for this target +# is a 404, so a source build cannot produce it. The prebuilt from npm works with a fork binary. +host="$(dirname "$CODEX_PATH")/codex-code-mode-host" +if [ ! -x "$host" ]; then + say "warning: no codex-code-mode-host next to the binary, so tool calls will not run." + say " Copy it out of the @openai/codex npm platform package to $host," + say " or skip dev/crash-tool.sh, which needs tools." +fi + +[ -f "${CODEX_HOME:-$HOME/.codex}/auth.json" ] || say "warning: no Codex auth found; run '$CODEX_PATH login' first." + +say "ok. state dir $STATE_DIR, task queue $CODEX_TEMPORAL_TASK_QUEUE, Temporal $TEMPORAL_ADDRESS" diff --git a/codex-temporal/dev/state.sh b/codex-temporal/dev/state.sh new file mode 100755 index 000000000000..0c65504a819f --- /dev/null +++ b/codex-temporal/dev/state.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# What the thread is doing, from the workflow. Needs a live worker, because a query needs a poller. +set -euo pipefail +# shellcheck source=./common.sh +source "$(cd -- "$(dirname -- "$0")" && pwd)/common.sh" + +session=${1:-dev} +require_temporal +state=$(tsx state.mts "$session") +remember_thread "$session" "$(printf '%s' "$state" | json_get threadId)" +printf '%s\n' "$state" diff --git a/codex-temporal/dev/stop.sh b/codex-temporal/dev/stop.sh new file mode 100755 index 000000000000..7ce8b5607b90 --- /dev/null +++ b/codex-temporal/dev/stop.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Stop what these scripts started, and nothing else. Only recorded pids are touched, so another +# session's Temporal or worker on this machine is left alone. +set -euo pipefail +# shellcheck source=./common.sh +source "$(cd -- "$(dirname -- "$0")" && pwd)/common.sh" + +for name in worker temporal; do + pid=$(read_pid "$name") + if alive "$pid"; then + say "stopping $name ($pid)" + stop_tree "$name" + else + rm -f "$PID_DIR/$name.pid" + fi +done + +for port in "$TEMPORAL_PORT" "$TEMPORAL_UI_PORT"; do + if port_taken "$port"; then say "note: port $port is still in use, by something we did not start."; fi +done +say "done." diff --git a/codex-temporal/dev/temporal.sh b/codex-temporal/dev/temporal.sh new file mode 100755 index 000000000000..0e4c0d0197e6 --- /dev/null +++ b/codex-temporal/dev/temporal.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# A local Temporal server for this stack only. Run it in its own shell and leave it. +set -euo pipefail +# shellcheck source=./common.sh +source "$(cd -- "$(dirname -- "$0")" && pwd)/common.sh" + +if port_taken "$TEMPORAL_PORT"; then + die "port $TEMPORAL_PORT is taken. If that is your own Temporal, use it as is, or move this one: + TEMPORAL_PORT=7245 TEMPORAL_UI_PORT=8245 dev/temporal.sh" +fi + +# A file db, so workflows survive restarting the server. That matters here: the whole point is +# showing that work outlives a process. +say "Temporal on $TEMPORAL_ADDRESS, UI on http://127.0.0.1:$TEMPORAL_UI_PORT, db $STATE_DIR/temporal.db" + +# Record before exec, so the pid stays right after this shell is replaced. +record_pid temporal $$ +exec temporal server start-dev \ + --port "$TEMPORAL_PORT" \ + --ui-port "$TEMPORAL_UI_PORT" \ + --db-filename "$STATE_DIR/temporal.db" \ + --log-level warn diff --git a/codex-temporal/dev/worker.sh b/codex-temporal/dev/worker.sh new file mode 100755 index 000000000000..54d6745a90cc --- /dev/null +++ b/codex-temporal/dev/worker.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# The durable executor. Run as many as you like; they all pull the same task queue. +set -euo pipefail +# shellcheck source=./common.sh +source "$(cd -- "$(dirname -- "$0")" && pwd)/common.sh" + +require_temporal +[ -x "$CODEX_PATH" ] || die "no codex build at $CODEX_PATH. Run dev/setup.sh." + +record_pid worker $$ +cd "$PKG_DIR" +exec ./node_modules/.bin/tsx src/worker.ts From 801bc4c7aa84d5993f9ab42bfb284d9c51678357 Mon Sep 17 00:00:00 2001 From: Mohammad Dashti Date: Thu, 20 Aug 2026 15:20:29 -0700 Subject: [PATCH 8/9] Squared the README with what this branch carries. The gaps section still listed the aborted wording and the exec hang as open, and this branch now sits on the fixes for both. --- codex-temporal/README.md | 43 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/codex-temporal/README.md b/codex-temporal/README.md index 82a5a073051a..d607a40d4692 100644 --- a/codex-temporal/README.md +++ b/codex-temporal/README.md @@ -33,6 +33,8 @@ Both are small: the CLI flag maps to the empty-input turn that already existed, A gap there is expected, not a defect, so all three now log it the same way. The recovery code was already sitting directly after the panic, which is the tell. +**Ending a turn whose task died.** A panic inside the turn task unwound past both the finish hook and the waiters, so a client waited for a terminal event that never came and codex stayed alive holding the writer lock. The task now runs under `catch_unwind`, and a panic becomes a fatal task error that flows through the same finish hook as any other failure. That is the general guard behind the specific fix above. + **Not leaving a child behind.** The SDK aborted a turn by sending the child a polite signal and trusting it to exit. A child that ignores it keeps running, and codex holds a writer lock on the thread for as long as it lives, so every later resume of that thread fails with `already has an active writer`. An aborted turn now escalates to `SIGKILL` after a grace period. **Saying what actually happened.** The synthesized output used to read `aborted`. After a hard kill that is optimistic: the command may well have run. It now says the outcome is unknown and the state should be checked before repeating the call. This does not touch the interrupt path, which writes its own richer output (`Wall time: N seconds` plus `aborted by user`); the placeholder is only reached when nothing was recorded at all. @@ -61,6 +63,26 @@ Reaping observed against a real thread held by a detached process: a resume fail ## Running it +`dev/` brings the whole stack up. It runs off the default ports and tracks every process by pid, so +it cannot disturb, or be disturbed by, a Temporal or a Codex already running on the machine. + +``` +dev/setup.sh # once: deps, and a preflight that explains itself + +dev/temporal.sh # shell 1: Temporal on 127.0.0.1:7244, UI on 8244 +dev/worker.sh # shell 2: the durable executor + +dev/ask.sh "Reply with the single word BRAVO." # submit and wait for the answer +dev/ask.sh smoke "What word did you just say?" # same session twice: turn 2 resumes the thread +dev/state.sh smoke # what the thread is doing +dev/inspect.sh smoke # what the rollout says, worker or no worker +dev/stop.sh # stops only what dev/ started +``` + +`TEMPORAL_PORT=7245 TEMPORAL_UI_PORT=8245` moves it if those ports are taken. + +By hand, if you would rather: + ``` npm install CODEX_PATH=../codex-rs/target/debug/codex npm run worker # in one shell @@ -79,9 +101,23 @@ Env: `TEMPORAL_ADDRESS`, `TEMPORAL_NAMESPACE`, `CODEX_TEMPORAL_TASK_QUEUE`, `COD ## Reproducing the crash test -Ask for two shell commands one at a time, the second one slow (`echo first >> a.txt`, then `sleep 30`). Wait until the rollout has more tool calls than tool outputs, so the first tool is settled and the second is in flight. Then `pkill -9 -f "codex-temporal.*src/worker.ts"`, wait past the 30s heartbeat timeout, and start a fresh worker. +``` +dev/crash-worker.sh # killed during a long generation +dev/crash-tool.sh # killed with one tool settled and one in flight +``` + +Both print PASS or FAIL and exit non-zero on failure, and both kill the worker by recorded pid, so nothing else on the machine is at risk. + +Each one refuses to conclude anything from a turn that already finished, and that guard earns its place. The first version of these scripts waited for the workflow to report a thread id, which only arrives with the activity's *result*, so the kill always landed after the turn was over and every assertion passed for the wrong reason. They now find the rollout by a token in the prompt text, which Codex writes while the turn runs, and they check the activity attempt afterwards. An attempt of 2 is the one claim an early-finishing turn cannot fake. + +`dev/crash-tool.sh` is the one worth watching. It asks for two shell commands, the second slow, and waits until the rollout holds more tool calls than tool outputs before killing the worker. + +``` +before the kill: 1 prompt, 2 tool calls, 1 tool output, a.txt 1 line +after recovery: 1 prompt, 6 tool calls, 6 tool outputs, a.txt 1 line, answer DELTA, attempt 2 +``` -The observed run. Before the kill: 1 prompt, 0 turn completions, 2 tool calls, 1 tool output, and `a.txt` with one line. After recovery: 1 prompt (not asked twice), 1 turn completion, the answer `DELTA`, and `a.txt` still with **one** line, so the completed tool did not run again. The activity comes back on attempt 2. +`a.txt` still holding one line is the assertion that matters: the settled tool kept its recorded result and did not run again. The tool count rising to 6 is the unknown-outcome wording working as intended. The model was told the interrupted call's outcome was unknown, so it went and checked the state rather than repeating the command. ## Layout @@ -98,7 +134,6 @@ The observed run. Before the kill: 1 prompt, 0 turn completions, 2 tool calls, 1 - **No step level, and it costs less than it sounds.** A turn is the smallest durable unit here, but that is not the same gap it was on the Pi fork. Codex records each item to the rollout as it happens, and a resumed turn picks up from there, so the work lost to a crash is already about one step: the tool-crash test above finished the turn with the completed tool's side effect intact and not repeated. What a step-per-activity design would add is control and visibility, not saved work, and the visibility half is covered by the progress on the heartbeat. The remaining half is not cheap. `run_turn` does have a step loop, so stopping after one step is a small change on its own, but a turn that stops early is not a completed turn, and this protocol has no way to say that: the app-server would emit `turn/completed` for something still mid-conversation. Making it honest means a new terminal turn status threaded through the app-server, both SDKs, the rollout projection that reconstructs turns (`build_turns_from_rollout_items`, which fork and rollback depend on), and the TUI. That is a large, invasive change for control this executor does not currently need, so it is deliberately not built. -- **A settled tool call is reported as aborted, not unknown.** See above. - **A source build cannot compile the code-mode host.** Codex routes shell tools through a host that embeds V8, and the rusty_v8 prebuilt for `aarch64-apple-darwin` is a 404 in this version. Copying the prebuilt host out of the `@openai/codex` npm platform package into `codex-rs/target/debug/codex-code-mode-host` works and is what the crash test above ran against. - **A killed process can leave a thread locked.** The writer lock is an advisory `flock`, so the OS releases it when the process dies. It only blocks a resume while a process still holds it, which is why the panic above was so damaging: the panicking process stayed alive. -- **`codex exec` still waits forever if a turn task dies without saying so.** The panic that used to cause that is fixed, but the wait loop only ends on an event or a closed stream, so any other silent death of the turn task would hang it. The stall timeout above is the reason that is now survivable rather than fatal. +- **`codex exec` can still wait on a turn task that dies without saying so.** A panic is now turned into a turn failure, which is the cause that showed up in practice. The wait loop still only ends on an event or a closed stream, so any other silent death of the turn task would hang it, and the stall timeout above is what makes that survivable rather than fatal. From 62a9e1cf6ed87847717e5f9744451858499600d2 Mon Sep 17 00:00:00 2001 From: Mohammad Dashti Date: Thu, 20 Aug 2026 16:00:44 -0700 Subject: [PATCH 9/9] Bounded a turn's retries and said why one failed. A turn retried a hundred times with no classification, so a codex that could never run burned a hundred model calls and the thread reported the result as an interrupt. Attempts default to five, the cap and the turn timeout come from env, and a failure no retry can fix stops on the first one. --- codex-temporal/README.md | 10 ++++- codex-temporal/package.json | 3 +- codex-temporal/src/activities.ts | 3 +- codex-temporal/src/client.ts | 6 ++- codex-temporal/src/config.ts | 4 ++ codex-temporal/src/errors.ts | 59 ++++++++++++++++++++++++++++++ codex-temporal/src/protocol.ts | 11 +++++- codex-temporal/src/worker.ts | 11 ++++++ codex-temporal/src/workflow.ts | 51 +++++++++++++++++--------- codex-temporal/test/errors.test.ts | 53 +++++++++++++++++++++++++++ codex-temporal/tsconfig.json | 2 +- 11 files changed, 190 insertions(+), 23 deletions(-) create mode 100644 codex-temporal/src/errors.ts create mode 100644 codex-temporal/test/errors.test.ts diff --git a/codex-temporal/README.md b/codex-temporal/README.md index d607a40d4692..afcba13ed6ad 100644 --- a/codex-temporal/README.md +++ b/codex-temporal/README.md @@ -59,6 +59,8 @@ Reaping observed against a real thread held by a detached process: a resume fail - **Mid-turn: the turn is carried on, not restarted.** On a retry, the activity looks for its own prompt in the rollout. Recorded and answered means the answer is already there and nothing needs to run. Recorded and unfinished means `continueTurn()`, so the prompt is not asked twice. - **A first turn that dies before Codex minted a thread id** starts over, which is correct: nothing had been recorded yet. Once the id exists the activity heartbeats it, so a retry from that point resumes rather than opening a second thread. - **Tool side effects are not rolled back, and are not repeated either.** A crash between a tool starting and its result landing leaves a tool call with no output. The model is told the outcome is unknown and decides what to do, which is the safe default for a coding agent. Tools that had already completed keep their recorded results and do not run again. +- **One thread belongs to one machine.** The reaper works through `lsof` on the local `thread-writer-locks` file, `ps`, and a local kill, and Codex's writer lock is an advisory `flock`. None of that coordinates two workers on different hosts, even with a shared sessions directory, so one thread's turns must not be spread across hosts. Scale by running more threads, not by spreading one. +- **A turn that cannot run says so.** Attempts per turn default to 5, and a failure no retry can fix, such as a `CODEX_PATH` that is not executable, is non-retryable and stops on the first one. The worker also checks that path on startup rather than once per attempt. Either way the thread stays up and `threadState` reports `outcome: "failed"` with the message, instead of reporting it as an interrupt. - **The rollout stays honest about the gap.** Codex does not write the synthesized output back, so a thread that survived a crash keeps one more tool call than tool outputs on disk forever. That is the record telling the truth, not a defect. ## Running it @@ -93,7 +95,13 @@ npx tsx inspect.mts The worker needs a Codex build from this fork, because it calls `--continue`. Build it with `cargo build -p codex-cli --bin codex` in `codex-rs` (set `CARGO_NET_GIT_FETCH_WITH_CLI=true` if a git dependency fails to authenticate). -Env: `TEMPORAL_ADDRESS`, `TEMPORAL_NAMESPACE`, `CODEX_TEMPORAL_TASK_QUEUE`, `CODEX_SESSIONS_DIR`, `CODEX_IDLE_TIMEOUT`, `CODEX_PATH`, `CODEX_MODEL`, `CODEX_PROJECT_DIR`, `CODEX_SANDBOX`. +Env: `TEMPORAL_ADDRESS`, `TEMPORAL_NAMESPACE`, `CODEX_TEMPORAL_TASK_QUEUE`, `CODEX_SESSIONS_DIR`, `CODEX_IDLE_TIMEOUT`, `CODEX_TURN_TIMEOUT`, `CODEX_MAX_ATTEMPTS`, `CODEX_STALL_TIMEOUT_MS`, `CODEX_PATH`, `CODEX_MODEL`, `CODEX_PROJECT_DIR`, `CODEX_SANDBOX`. + +`CODEX_TURN_TIMEOUT` and `CODEX_MAX_ATTEMPTS` reach the workflow as start arguments, so they are fixed for the life of a thread. Changing them affects the next thread, not one already running. + +A worker serves one project: the activity passes `CODEX_PROJECT_DIR` as the turn's working directory, and a prompt carries no directory of its own. So a second project means a second worker on its own task queue. + +`npm test` covers the failure classifier. The rest is verified end to end, through `dev/`. `temporal workflow query --workflow-id codex-thread- --name threadState` reports the queue, the thread id, the turn in flight, and how the last turn ended. diff --git a/codex-temporal/package.json b/codex-temporal/package.json index 142cf8d169c8..0e120373eea8 100644 --- a/codex-temporal/package.json +++ b/codex-temporal/package.json @@ -7,7 +7,8 @@ "scripts": { "typecheck": "tsc --noEmit", "worker": "tsx src/worker.ts", - "submit": "tsx submit.mts" + "submit": "tsx submit.mts", + "test": "tsx --test test/*.test.ts" }, "dependencies": { "@temporalio/activity": "^1.11.0", diff --git a/codex-temporal/src/activities.ts b/codex-temporal/src/activities.ts index 2d0ffb9f584e..b093c70964c7 100644 --- a/codex-temporal/src/activities.ts +++ b/codex-temporal/src/activities.ts @@ -8,6 +8,7 @@ import type { Config } from "./config.js"; import type { RunTurnInput, RunTurnResult } from "./protocol.js"; import { findRollout, inspectPrompt } from "./rollout.js"; import { reapOrphanedWriter } from "./orphans.js"; +import { asActivityFailure } from "./errors.js"; // The prompt carries a zero-width marker with its promptId, so a re-driven activity can tell // whether this exact prompt was already recorded. It also tells our prompts apart from the @@ -214,7 +215,7 @@ export function makeActivities(cfg: Config) { { cause: err }, ); } - throw err; + throw asActivityFailure(err); } finally { stop(); } diff --git a/codex-temporal/src/client.ts b/codex-temporal/src/client.ts index 48d8710bbce3..cb8acf1cc7dc 100644 --- a/codex-temporal/src/client.ts +++ b/codex-temporal/src/client.ts @@ -18,7 +18,11 @@ export async function connect() { export async function submitPrompt(sessionId: string, text: string, promptId = randomUUID()) { const { cfg, client, connection } = await connect(); const prompt: PromptInput = { promptId, text }; - const options: ThreadOptions = { idleTimeout: cfg.idleTimeout }; + const options: ThreadOptions = { + idleTimeout: cfg.idleTimeout, + turnTimeout: cfg.turnTimeout, + maxAttempts: cfg.maxAttempts, + }; try { await client.workflow.signalWithStart(WORKFLOW_TYPE, { taskQueue: cfg.taskQueue, diff --git a/codex-temporal/src/config.ts b/codex-temporal/src/config.ts index 3f9e6e7cd80a..3406746c0213 100644 --- a/codex-temporal/src/config.ts +++ b/codex-temporal/src/config.ts @@ -11,6 +11,8 @@ export interface Config { // it at shared storage. readonly sessionsDir: string; readonly idleTimeout: string; + readonly turnTimeout: string; + readonly maxAttempts: number; // Which codex binary to drive. Defaults to whatever the SDK finds; point it at a fork build to // drive that instead. readonly codexPath?: string; @@ -33,6 +35,8 @@ export function fromEnv(): Config { taskQueue: process.env.CODEX_TEMPORAL_TASK_QUEUE ?? "codex-thread", sessionsDir: process.env.CODEX_SESSIONS_DIR ?? `${codexHome}/sessions`, idleTimeout: process.env.CODEX_IDLE_TIMEOUT ?? "5 minutes", + turnTimeout: process.env.CODEX_TURN_TIMEOUT ?? "1 hour", + maxAttempts: Number(process.env.CODEX_MAX_ATTEMPTS ?? 5), codexPath: process.env.CODEX_PATH, model: process.env.CODEX_MODEL, projectDir: process.env.CODEX_PROJECT_DIR ?? process.cwd(), diff --git a/codex-temporal/src/errors.ts b/codex-temporal/src/errors.ts new file mode 100644 index 000000000000..3558bfbcf052 --- /dev/null +++ b/codex-temporal/src/errors.ts @@ -0,0 +1,59 @@ +// Deciding which failures are worth another attempt. +// +// A retry helps when the cause was the process dying or the turn stalling: the rollout is intact +// and another worker carries the turn on. It helps with nothing when the codex binary is missing or +// unusable, because every attempt fails the same way and the real reason ends up buried under a +// pile of identical errors. + +import { ApplicationFailure } from "@temporalio/activity"; + +export const SPAWN_FAILURE = "CodexNotRunnable"; + +const SPAWN_CODES = new Set(["ENOENT", "EACCES", "EPERM", "ENOTDIR"]); +const SPAWN_TEXT = /\bENOENT\b|\bEACCES\b|\bEPERM\b|\bENOTDIR\b|\bspawn\b .*\bfailed\b/i; + +const codeOf = (err: unknown): string | undefined => { + const code = (err as { code?: unknown } | null)?.code; + return typeof code === "string" ? code : undefined; +}; + +/** Walk the cause chain, because a spawn error usually arrives wrapped by the SDK. */ +const chain = (err: unknown): unknown[] => { + const seen: unknown[] = []; + let current = err; + while (current && seen.length < 8 && !seen.includes(current)) { + seen.push(current); + current = (current as { cause?: unknown }).cause; + } + return seen; +}; + +export const messageOf = (err: unknown): string => { + const deepest = chain(err).at(-1); + if (deepest instanceof Error && deepest.message) return deepest.message; + if (err instanceof Error) return err.message; + return String(err); +}; + +/** Whether this failure would fail the same way on every later attempt. */ +export function isSpawnFailure(err: unknown): boolean { + return chain(err).some((link) => { + const code = codeOf(link); + if (code && SPAWN_CODES.has(code)) return true; + return link instanceof Error && SPAWN_TEXT.test(link.message); + }); +} + +/** + * The error to throw out of the activity: unchanged when a retry could help, and non-retryable + * when nothing about waiting or moving worker would change the outcome. + */ +export function asActivityFailure(err: unknown): unknown { + if (!isSpawnFailure(err)) return err; + return ApplicationFailure.create({ + message: `codex could not be run: ${messageOf(err)}`, + type: SPAWN_FAILURE, + nonRetryable: true, + cause: err instanceof Error ? err : undefined, + }); +} diff --git a/codex-temporal/src/protocol.ts b/codex-temporal/src/protocol.ts index fd65b06cdcc4..4ebabf87d364 100644 --- a/codex-temporal/src/protocol.ts +++ b/codex-temporal/src/protocol.ts @@ -44,8 +44,11 @@ export interface ThreadState { readonly running?: { readonly promptId: string }; readonly finished?: { readonly promptId: string; - readonly outcome: "answered" | "interrupted"; + // `failed` is its own outcome because reporting a genuine failure as an interrupt hides the + // reason, and the reason is the only thing worth having when a turn will not run. + readonly outcome: "answered" | "interrupted" | "failed"; readonly finalResponse: string; + readonly error?: string; }; } @@ -53,4 +56,10 @@ export interface ThreadOptions { // How long the workflow stays alive with no work before it retires. The next prompt starts a // fresh run, which rebuilds nothing: Codex's rollout already holds the conversation. readonly idleTimeout?: string; + // How long one turn may take. A large refactor turn can outlast a small value and be killed + // mid-work, so this is the knob to raise for real work rather than a limit to live with. + readonly turnTimeout?: string; + // Attempts per turn. Workflow options are fixed when the workflow starts, so changing this only + // affects the next thread, not one already running. + readonly maxAttempts?: number; } diff --git a/codex-temporal/src/worker.ts b/codex-temporal/src/worker.ts index 6239d91d51e2..bd4ca7a6948b 100644 --- a/codex-temporal/src/worker.ts +++ b/codex-temporal/src/worker.ts @@ -2,12 +2,22 @@ // the same task queue, so any worker can drive any thread from the shared sessions directory. import { fileURLToPath } from "node:url"; +import { access, constants } from "node:fs/promises"; import { NativeConnection, Worker } from "@temporalio/worker"; import { fromEnv } from "./config.js"; import { makeActivities } from "./activities.js"; async function main() { const cfg = fromEnv(); + + // Checked here rather than left to the first turn: a worker that cannot run codex should say so + // on startup, not once per attempt inside somebody's workflow. + if (cfg.codexPath) { + await access(cfg.codexPath, constants.X_OK).catch(() => { + throw new Error(`CODEX_PATH is not an executable file: ${cfg.codexPath}`); + }); + } + const connection = await NativeConnection.connect({ address: cfg.address }); const worker = await Worker.create({ @@ -21,6 +31,7 @@ async function main() { console.log(`codex-temporal worker on ${cfg.address} / ${cfg.namespace} / ${cfg.taskQueue}`); console.log(`codex: ${cfg.codexPath ?? "(from PATH)"} project: ${cfg.projectDir}`); console.log(`sessions: ${cfg.sessionsDir} sandbox: ${cfg.sandboxMode}`); + console.log(`turn timeout: ${cfg.turnTimeout} attempts per turn: ${cfg.maxAttempts}`); await worker.run(); } diff --git a/codex-temporal/src/workflow.ts b/codex-temporal/src/workflow.ts index ac5eff240893..c6178664f910 100644 --- a/codex-temporal/src/workflow.ts +++ b/codex-temporal/src/workflow.ts @@ -23,22 +23,23 @@ import type { ThreadState, } from "./protocol.js"; -const { runTurn } = proxyActivities<{ - runTurn(input: RunTurnInput): Promise; -}>({ - // A turn is a whole Codex run (many model calls and tools), so give it room; the heartbeat is - // the real liveness bound and re-drives within seconds of a worker death. - startToCloseTimeout: "1 hour", - heartbeatTimeout: "30 seconds", - retry: { maximumAttempts: 100 }, -}); - export const submitPrompt = defineSignal<[PromptInput]>(SIGNALS.submitPrompt); export const interrupt = defineSignal<[]>(SIGNALS.interrupt); export const threadState = defineQuery(QUERIES.threadState); export async function codexThread(sessionId: string, options?: ThreadOptions): Promise { const idleTimeout = options?.idleTimeout ?? "5 minutes"; + const { runTurn } = proxyActivities<{ + runTurn(input: RunTurnInput): Promise; + }>({ + // A turn is a whole Codex run (many model calls and tools), so give it room; the heartbeat is + // the real liveness bound and re-drives within seconds of a worker death. + startToCloseTimeout: options?.turnTimeout ?? "1 hour", + heartbeatTimeout: "30 seconds", + // Few, not many. Every attempt of a turn that cannot succeed costs a model call, and the + // activity marks the failures that no retry can help as non-retryable anyway. + retry: { maximumAttempts: options?.maxAttempts ?? 5 }, + }); const queue: PromptInput[] = []; let current: CancellationScope | undefined; // Codex mints this on the first turn. Keeping it here is what makes later turns resume the same @@ -60,8 +61,9 @@ export async function codexThread(sessionId: string, options?: ThreadOptions): P if (!woke && queue.length === 0) return; // idle: retire; the next prompt starts a fresh run const prompt = queue.shift()!; - let outcome: NonNullable["outcome"] = "interrupted"; + let outcome: NonNullable["outcome"] = "failed"; let finalResponse = ""; + let error: string | undefined; try { await CancellationScope.cancellable(async () => { current = CancellationScope.current(); @@ -73,16 +75,31 @@ export async function codexThread(sessionId: string, options?: ThreadOptions): P outcome = "answered"; }); } catch (err) { - // An interrupt cancels the in-flight turn; the thread keeps serving later prompts. A real - // run error is already recorded in the rollout, so we log-and-continue rather than fail the - // whole thread. (Surfacing typed errors to the caller is a follow-up.) - if (!isCancellation(err)) { - // TODO: classify and, for genuine failures, decide retry vs surface. For now, continue. + // An interrupt cancels the in-flight turn; the thread keeps serving later prompts. A turn + // that ran out of attempts is a different thing, and it says why: the thread stays up so a + // later prompt can still work, but nobody has to read the worker log to find out what broke. + if (isCancellation(err)) { + outcome = "interrupted"; + } else { + error = failureText(err); } } finally { current = undefined; running = undefined; - finished = { promptId: prompt.promptId, outcome, finalResponse }; + finished = { promptId: prompt.promptId, outcome, finalResponse, error }; } } } + +// Temporal wraps an activity failure, so the message worth reporting is on the cause. Walking the +// chain keeps this readable in a query rather than "Activity task failed". +function failureText(err: unknown): string { + let current: unknown = err; + let text = ""; + for (let depth = 0; current && depth < 8; depth++) { + const message = (current as { message?: unknown }).message; + if (typeof message === "string" && message.length > 0) text = message; + current = (current as { cause?: unknown }).cause; + } + return text || String(err); +} diff --git a/codex-temporal/test/errors.test.ts b/codex-temporal/test/errors.test.ts new file mode 100644 index 000000000000..48051ed89965 --- /dev/null +++ b/codex-temporal/test/errors.test.ts @@ -0,0 +1,53 @@ +// The classifier decides whether a turn gets another attempt, so the cases that matter are the two +// ways it can be wrong: retrying something that will never work, and giving up on something a +// second worker would have finished. + +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { ApplicationFailure } from "@temporalio/activity"; +import { asActivityFailure, isSpawnFailure, messageOf, SPAWN_FAILURE } from "../src/errors.js"; + +const withCode = (message: string, code: string) => Object.assign(new Error(message), { code }); + +test("a missing codex binary is not worth retrying", () => { + assert.equal(isSpawnFailure(withCode("spawn /nope/codex ENOENT", "ENOENT")), true); + assert.equal(isSpawnFailure(withCode("permission problem", "EACCES")), true); +}); + +test("a spawn failure wrapped by the SDK is still recognised", () => { + const wrapped = new Error("failed to start codex", { cause: withCode("boom", "ENOENT") }); + assert.equal(isSpawnFailure(wrapped), true); +}); + +test("an ordinary turn failure stays retryable", () => { + // The model reporting a failed tool, or a rate limit, is exactly what a retry is for. + assert.equal(isSpawnFailure(new Error("turn failed: file not found in workspace")), false); + assert.equal(isSpawnFailure(new Error("429 rate limit exceeded")), false); + assert.equal(isSpawnFailure(new Error("codex produced no events for 18s and was killed")), false); +}); + +test("a retryable error is passed through untouched", () => { + const err = new Error("429 rate limit exceeded"); + assert.equal(asActivityFailure(err), err); +}); + +test("a spawn failure becomes a non-retryable failure that names the cause", () => { + const failure = asActivityFailure(withCode("spawn /nope/codex ENOENT", "ENOENT")); + assert.ok(failure instanceof ApplicationFailure); + assert.equal(failure.nonRetryable, true); + assert.equal(failure.type, SPAWN_FAILURE); + assert.match(failure.message, /ENOENT/); +}); + +test("the reported message comes from the deepest cause", () => { + const wrapped = new Error("outer", { cause: new Error("inner", { cause: new Error("root") }) }); + assert.equal(messageOf(wrapped), "root"); +}); + +test("a cause cycle does not hang the walk", () => { + const a = new Error("a"); + const b = new Error("b", { cause: a }); + (a as { cause?: unknown }).cause = b; + assert.equal(isSpawnFailure(a), false); + assert.ok(messageOf(a).length > 0); +}); diff --git a/codex-temporal/tsconfig.json b/codex-temporal/tsconfig.json index a06b93bb15f8..345aa357b380 100644 --- a/codex-temporal/tsconfig.json +++ b/codex-temporal/tsconfig.json @@ -17,5 +17,5 @@ ] } }, - "include": ["src/**/*.ts"] + "include": ["src/**/*.ts", "test/**/*.ts"] }