chore: sync with upstream 2026-06-20 - #38
Conversation
## Summary - Extract session replay/history behavior from `SessionDO` into `SessionEventStream` - Centralize EventRow projection for WebSocket replay/history and HTTP event listing - Re-export event list response types from shared instead of maintaining local duplicates ## Why `SessionDO` was carrying Session Event Stream replay and paginated history projection inline. Moving this into a focused class reduces Durable Object surface area and gives the event stream behavior its own testable interface without adding callback-heavy dependencies. ## Validation - `npm test -w @open-inspect/control-plane -- src/session/event-stream.test.ts src/session/services/message.service.test.ts src/session/http/handlers/messages.handler.test.ts` - `npm run build -w @open-inspect/shared` - `npm run test:integration -w @open-inspect/control-plane -- test/integration/websocket-client.test.ts` - `npm run typecheck -w @open-inspect/control-plane` - `npx prettier --check packages/control-plane/src/session/durable-object.ts packages/control-plane/src/session/event-stream.ts packages/control-plane/src/session/event-stream.test.ts packages/control-plane/src/session/http/handlers/messages.handler.ts packages/control-plane/src/session/http/handlers/messages.handler.test.ts packages/control-plane/src/session/services/message.service.ts packages/control-plane/src/session/services/message.service.test.ts packages/control-plane/src/types.ts` - `npx eslint packages/control-plane/src/session/durable-object.ts packages/control-plane/src/session/event-stream.ts packages/control-plane/src/session/event-stream.test.ts packages/control-plane/src/session/http/handlers/messages.handler.ts packages/control-plane/src/session/http/handlers/messages.handler.test.ts packages/control-plane/src/session/services/message.service.ts packages/control-plane/src/session/services/message.service.test.ts packages/control-plane/src/types.ts` - `git diff --check` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * Refactored session event handling and replay data sourcing to improve modularity and code organization. * Consolidated event response types to the shared package for better reusability. * **Tests** * Added comprehensive test suite for session event stream functionality. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…#789) ## Problem The scheduler's recovery sweep was timing out against D1, which takes the whole tick down with it: ``` D1DatabaseSessionAlwaysPrimary._sendOrThrow (cloudflare-internal:d1-api) AutomationStore.getOrphanedStartingRuns SchedulerDO.recoverySweep SchedulerDO.handleTick ``` `recoverySweep()` runs at the top of every tick, before any overdue automations are processed, and is not wrapped in try/catch — so when its first query times out the entire tick aborts and **no scheduled automations run at all**. ## Root cause Both recovery-sweep queries filter `automation_runs` by a single literal status: - `getOrphanedStartingRuns`: `WHERE status = 'starting' AND created_at < ?` - `getTimedOutRunningRuns`: `WHERE status = 'running' AND started_at IS NOT NULL AND started_at < ?` The only candidate index was: ```sql CREATE INDEX idx_runs_active_status ON automation_runs (status, created_at) WHERE status IN ('starting', 'running'); ``` SQLite (and therefore D1) only uses a partial index when it can prove the query's `WHERE` implies the index predicate, and it "does not do algebra" — it cannot prove `status = 'starting'` implies `status IN ('starting','running')` (`IN` is represented as its own node type, not OR-connected terms). So the index was **never used** and both sweeps fell back to a full table `SCAN`. `automation_runs` is append-only (no retention), so scan cost grows with total history until it exceeds D1's query time limit — the classic "worked for months, then started timing out as data grew." `EXPLAIN QUERY PLAN` on the exact production query confirms the scan: ``` SELECT * FROM automation_runs WHERE status = 'starting' AND created_at < ?; `--SCAN automation_runs -- idx_runs_active_status not used ``` ## Fix Replace the unused index with bare-equality **per-status partial indexes** that the planner matches verbatim (migration `0024`): ```sql DROP INDEX IF EXISTS idx_runs_active_status; CREATE INDEX idx_runs_orphan_sweep ON automation_runs (created_at) WHERE status = 'starting'; CREATE INDEX idx_runs_timeout_sweep ON automation_runs (started_at) WHERE status = 'running'; ``` After the migration: ``` SELECT * FROM automation_runs WHERE status = 'starting' AND created_at < ?; `--SEARCH automation_runs USING INDEX idx_runs_orphan_sweep (created_at<?) ``` Because the indexes are partial, they only ever contain the small *active* subset (terminal rows are excluded), so they stay tiny and fast regardless of how large the history grows — index size tracks the live working set, not total history. (On a 200k-row sample, the partial indexes measured ~4 KB each vs ~4.6 MB for a full `(status, created_at)` index, and the gap widens without bound.) `idx_runs_active_status` had no other readers — every other status-filtered query is also scoped by `automation_id` and uses `idx_runs_automation_status` / `idx_runs_concurrency` — so dropping it is safe. The queries are unchanged. A guard comment on the recovery-sweep section documents that `status` must stay a string literal: partial-index matching is syntactic and happens at plan time, so `status = ?` (a bound parameter) would silently revert to a full scan. ## Why not a full `(status, created_at)` index? A full index would also work, but it reintroduces the unbounded-growth coupling that caused the bug — its size grows with the entire append-only table forever. Partial indexes decouple index size from history size, which is the canonical "index the active subset" idiom (cf. Postgres' unbilled-orders example; good_job / graphile-worker / Oban Pro all index the active subset). ## Tests - Existing behavioural recovery-sweep tests still pass. - Added two `EXPLAIN QUERY PLAN` regression guards (integration, against real D1) asserting each sweep is served by its partial index — these catch a migration revert, predicate drift, or the literal→bound-param mistake. - Full suite green: **1365 unit + 370 integration**. ## Follow-up (not in this PR) `automation_runs` still grows without bound. This change makes the *sweeps* immune to table size, but the table itself and per-automation history queries (`listRunsForAutomation`'s `COUNT(*)`) keep growing. A retention/pruning job for old terminal runs is worth a separate change. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Tests** * Added regression tests to verify recovery-sweep queries use correct database indexes and avoid full table scans. * **Chores** * Optimized and restructured database indexes for recovery operations. * Enhanced query performance during recovery-sweep operations through improved indexing strategy. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…art reify (ColeMurray#790) ## Summary A user's **first prompt on a freshly-spawned sandbox can hard-fail with "Execution failed"** when the bridge's `POST /session` to the in-sandbox OpenCode server times out (30 s `httpx.ReadTimeout`). This seeds OpenCode's **global** config dir (`~/.config/opencode`) with the plugin dependency tree we already stage, so OpenCode's cold-start `npm install` is a no-op and the first request no longer blocks on it. ## Root cause (confirmed on a live sandbox) `POST /session` runs OpenCode's per-directory bootstrap. When a plugin is configured (an OpenAI-OAuth session deploys `codex-auth-plugin.js`, and the repo may ship its own `.opencode/plugins/*.ts`), `plugin.init()` calls `config.waitForDependencies()`, which makes the request **block** on a forked `npm install @opencode-ai/plugin` for **every** directory in OpenCode's config search path. That search path includes **`Global.Path.config` = `~/.config/opencode`**, which OpenCode `mkdir`s empty on every startup. We pre-stage `/app/opencode-deps` and copy it into the **repo's** `.opencode/` (`_install_tools`), but we **never** seed the global dir — so it has no `node_modules`, OpenCode's `checkNodeModules` reifies it (a real `arborist.reify()` npm install, no internal timeout), and on a slow/cold install that exceeds the bridge's 30 s budget the prompt fails. Verified on a running sandbox — `~/.config/opencode/` contains: ```json { "dependencies": { "@opencode-ai/plugin": "1.14.41" } } ``` plus `node_modules/` and a 14K `package-lock.json`. That `package.json` has **no `name`/`type`** — it is arborist's synthesized manifest, **not** our staged file (`{"name":"opencode-tools","type":"module",…}`), proving OpenCode reified this directory itself. (Global `npm install -g` lands in `/usr/lib/node_modules`; `_install_tools` targets the repo's `.opencode/` — neither writes here.) This is **not** the previously-suspected `_install_tools` mixed-tree bug: the incident repo committed only `.opencode/plugins/skill-audit.ts` (no `.opencode/package.json`), so its repo `.opencode/` is seeded consistently and does not reify — the unseeded **global** dir is the culprit. ## Fix `entrypoint.py`: - Extract the existing deps-copy into a shared `_stage_opencode_deps(deps_cache, dest_dir)`. - Add `_seed_global_opencode_deps()` — resolves OpenCode's global config dir the way OpenCode does (`OPENCODE_CONFIG_DIR` → `$XDG_CONFIG_HOME/opencode` → `~/.config/opencode`) and copies the staged tree there **only if it has no `node_modules`** (never clobbers a real/existing config). - Call it from `start_opencode` right after `_install_tools`, **best-effort** (a failure only degrades to the slower reify, so it must not crash startup). It's a plain file copy, runs on every serving boot (fresh / repo-image / snapshot, and Daytona since the entrypoint is shared), and is robust to `HOME`/`XDG_CONFIG_HOME` changes. ## Why this approach It removes the **specific directory that actually reified**, for every session, at zero per-session cost. For the reported incident it is complete on its own (the repo's `.opencode/` was already consistent). Considered, deliberately **not** in this PR (tracked as follow-ups): - A bridge-side dedicated `POST /session` timeout + retry (defense in depth) — recommended next. - A general "warm the bootstrap before `ready`" step that pays *any* reify off the prompt path — useful for repos that ship their own `.opencode/` npm deps. ## Testing - New unit tests in `test_tool_installation.py`: - `TestResolveGlobalConfigDir` — `OPENCODE_CONFIG_DIR` override, `XDG_CONFIG_HOME`, and `~/.config` fallback. - `TestSeedGlobalOpencodeDeps` — seeds an empty global dir; no-ops when `node_modules` already present (never clobbers); no-ops when the staging is absent. - `pytest tests/` — 359 passed. - `ruff check` / `ruff format --check` — clean. - `mypy src/` — no new errors (mypy-neutral vs `main`). <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Enhanced OpenCode dependency staging by reusing cached `package*.json` and `node_modules` when absent, and optionally seeding them into the standard global configuration directory when empty. * **Bug Fixes** * Improved startup resilience by running global seeding in best-effort mode and continuing even if seeding fails. * **Tests** * Added coverage for global config directory resolution (environment/XDG/home fallback) and seeding behavior under empty, partially populated, and missing staging-cache scenarios. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary The repo-image build endpoint crashes at runtime with: ``` Modal API error: 'Function' object has no attribute 'with_options' ``` `packages/modal-infra/src/web_api.py:595` (the configurable build-timeout feature) calls: ```python await build_repo_image.with_options(timeout=function_timeout).spawn.aio(...) ``` ## Root cause `Function.with_options()` was added in **Modal 1.4.3**. In 1.3.1–1.4.2 `with_options` existed **only on `Cls`** (`@app.cls()`), never on `Function` (`@app.function()`). `uv.lock` pinned **modal 1.3.1**, and CI deploys with `uv sync --frozen` (`terraform/modules/modal-app/scripts/deploy.sh`), so production ran 1.3.1 and the attribute was missing. It passed locally because dev environments had 1.4.3, and the endpoint unit test *mocks* `with_options`, so neither path exercised the real 1.3.1 object. ## Fix - `pyproject.toml`: floor bumped `modal>=0.73.0` → `modal>=1.4.3` - `uv.lock`: `modal 1.3.1 → 1.4.3`, pinned to exactly **1.4.3** (the minimum that has `Function.with_options`; not the latest 1.5.0, to keep the blast radius minimal) Transitive changes from Modal 1.4.3: `synchronicity` 0.11.1→0.12.5, `starlette` bump; Modal 1.4.3 dropped its `typer`/`shellingham` CLI deps, so uv removed them. The `modal` CLI still works (`modal client version: 1.4.3`), so `deploy.sh`'s `uv run modal deploy` is unaffected. ## Verification - `uv sync --frozen` (mirrors `deploy.sh`) installs Modal 1.4.3 with a working `Function.with_options(timeout=...)`. - **Unmocked** check: the real `build_repo_image.with_options(timeout=4200).spawn.aio` chain now resolves against Modal 1.4.3 — no `AttributeError`. - Full `modal-infra` suite: **152 passed**. At runtime Modal mounts the deploy-client version into the function container, so once CI redeploys with the locked 1.4.3 client, the endpoint gets `with_options`. No `CACHE_BUSTER` bump needed (that only affects sandbox image layers, not client injection). <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Updated modal library dependency to version 1.4.3 or higher for enhanced compatibility. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…leMurray#792) ## Summary The Terraform Modal deploy only redeploys when its `source_hash` changes, but that hash was computed from **only `*.py/*.js/*.ts` under `packages/{modal-infra,sandbox-runtime}/src`** — it excluded `packages/modal-infra/pyproject.toml` and `uv.lock`. The data plane deploys via `uv sync --frozen` (installs exactly the lockfile), so the lockfile *is* a deploy input. Excluding it means a **dependency-only change silently does not redeploy Modal**: `null_resource.modal_deploy` (`terraform/modules/modal-app/main.tf`) sees no trigger change and is a no-op. This is what swallowed ColeMurray#791's `modal>=1.4.3` bump — the lockfile changed, but `modal deploy` never ran, so the running container kept the old client. ## Fix Include `pyproject.toml` + `uv.lock` in `modal_source_hash` (both the Linux `sha256sum` and macOS `shasum` branches), so dependency-only changes trigger a redeploy. ## Verification - `terraform fmt -check` clean; `terraform validate` → "Success! The configuration is valid." - Confirmed the digest changes when only `uv.lock`/`pyproject.toml` change (and stays a valid 64-char hash).
…oleMurray#793) ## Summary Removes the `fileParallelism: false` serialization added in ColeMurray#765. Its stated rationale turned out to be false, so it was slowing the integration job (~2x) for no correctness benefit. ## Why ColeMurray#765's premise was wrong ColeMurray#765 serialized the integration suite on the theory that, since files share one D1 and isolate via `cleanD1Tables()` in `beforeEach`, running them concurrently lets one file's cleanup `DELETE` rows another file just seeded — making the scheduler tick tests flaky. That premise doesn't hold: **`@cloudflare/vitest-pool-workers` 0.16.13 isolates D1 storage per test _file_.** I verified with a two-file probe where each file inserts a marker row and polls 3s for the other's: - Both files ran **concurrently** (identical ~3063ms durations — sequential would be ~6s total). - **Neither saw the other's row.** So within a file tests share storage (hence `cleanD1Tables` is still needed), but **cross-file contamination cannot happen**, and file parallelism is safe. The integration failures on ColeMurray#748 were never this — they were vite 8's luxon CJS interop, fixed in **ColeMurray#784** (that luxon alias is **kept** here). ## What changed - Remove `fileParallelism: false` from `vitest.integration.config.ts`. - Correct the now-inaccurate "integration tests share one D1 instance" comment to describe per-file isolation (so the serialization isn't reintroduced). - **Kept:** ColeMurray#765's `scheduler.test.ts` assertion improvements (scoped to the seeded automation, schedule-advancement checks) — good test design and parallel-safe. ## Verification Real `npm ci` on main's lockfile (**vite 8.0.16**), full integration suite in parallel with the luxon alias: - **6/6 runs green** (372/372 tests each). - `typecheck`, `prettier`, `eslint`: clean. Cross-file isolation was confirmed by the concurrent probe above; the earlier "parallel flakes" I saw locally were pure resource exhaustion on an overloaded laptop (`setup 250s`), not contamination — they don't reproduce on a normal machine, and `main` CI was historically green under parallel. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Optimized test infrastructure configuration to improve integration test execution efficiency. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
ColeMurray#702) (ColeMurray#766) ## Summary Follow-up to ColeMurray#702 (the searchable repo clarification picker). Two things, in one PR: 1. **The discussed system-level change** — surface the classifier's ranked guesses as one-click quick-pick buttons, so the picker stops discarding a signal the system already computes. 2. **The code-quality feedback** left on ColeMurray#702 — extract the shared filter, name the option shape, share the truncation helper, fold the duplicated log path, guard the App Home picker, and dedupe the tests. ## 1. Quick-picks: stop throwing away the classifier's ranking ColeMurray#702 fixed a real bug (repos beyond the first 5 were unreachable) but, in doing so, made every clarification show a flat, unranked list — even when the classifier returned `medium` confidence with a ranked shortlist. That shortlist (`ClassificationResult.alternatives`) was still computed on every call and then **discarded**: nothing read it after ColeMurray#702 removed the `result.alternatives || repos.slice(0,5)` consumer. This PR feeds it back into the UI. The clarification message now renders: - the classifier's reasoning, - its ranked alternatives as **quick-pick buttons** (capped at `MAX_REPO_QUICK_PICKS`, when any), and - the searchable `external_select` over every repo as the fallback ("Or search for another repository:"). Common case → one click. Long tail → search. The buttons route through the **same** selection path as the picker (`handleRepoSelection`), so nothing downstream changes. When the classifier has no basis for a guess (the error/low-confidence path), no buttons render and you get the searchable picker exactly as today. ## 2. Review feedback from ColeMurray#702 - **Extracted `filterReposByQuery`** into `classifier/repos.ts` (canonical repo layer) and reused it in both the clarification picker and the App Home branch picker — the normalize+filter block was duplicated verbatim. - **Named the option shape**: added optional `description` to `SlackSelectOption` and returned `SlackSelectOption[]` explicitly, dropping the `Awaited<ReturnType<typeof …>>` smell. - **Shared the option-text truncation**: new `slack-options.ts` (`truncateSelectOptionText` / `plainTextOption`) replaces the magic `75` + raw `.slice(0,75)`; the button/option `text` is now length-guarded too (it wasn't before). - **Folded the `block_suggestion` handler** to a single central `http.request` log path (removing the inline-duplicated log and closing the gap where the fallback wasn't logged). - **Guarded the App Home picker** against 500s — it had the same unguarded lookup the ColeMurray#702 guard only protected on the clarification side. - **Deduped the suggestion tests**: `buildNumberedRepos()` + `mockReposFetch()` replace the 150-repo scaffolding that was copy-pasted across three tests. ## Module layout New feature/UI logic lives in a focused `repo-clarification.ts` (options endpoint, quick-pick buttons, message blocks) instead of growing the 1.2k-line `index.ts` — which actually **shrank by ~50 lines**. `slack-options.ts` holds the generic select-option text helpers shared by both pickers. ## Testing - `npm run typecheck` ✅ (full workspace) - `npm test -w @open-inspect/slack-bot` ✅ (102 tests, +9: pure-function units for the new builders/filter, plus a quick-pick routing test) - `npm run lint` / prettier ✅ <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added quick-pick buttons to speed up repository selection when multiple matches are found. * Introduced improved repository clarification UI and shared Slack option formatting. * **Bug Fixes** * Improved robustness for repository suggestion generation: failures are handled gracefully and fall back to no options rather than returning an error. * **Tests** * Expanded coverage for repo query filtering, option/blocks rendering, and interaction routing behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…oleMurray#795) ## Summary Follow-up to ColeMurray#790. That PR fixed the user-visible cold-start `ReadTimeout` (ColeMurray#767) by seeding OpenCode's **global** config dir (`~/.config/opencode`) so the first `POST /session` no longer reifies it — but it did the seeding by **copying `node_modules` at boot**, which is a multi-second cost on every sandbox start (observed as ~9–19s in the `opencode.start → opencode.global_deps_seeded` window). This moves that work to **image-build time**: bake the staged plugin tree into the global config dir once, so the runtime seed becomes a no-op. ## Change **`base.py`** — after staging `/app/opencode-deps`, also copy it into the global config dir at build time (the image fixes `HOME=/root`, so the dir is always `/root/.config/opencode`): ```dockerfile mkdir -p /root/.config/opencode cp -a /app/opencode-deps/. /root/.config/opencode/ ``` `CACHE_BUSTER` is bumped `v51 → v52-bake-opencode-global-deps` to rebuild the base image and stamp a distinct `SANDBOX_VERSION` (so we can confirm from telemetry which sandboxes run the baked image; per `manager.py:416`, repo-images/snapshots don't auto-rebuild on a bump and pick it up as they refresh). **`entrypoint.py`** — `_seed_global_opencode_deps()` is now a documented **fallback**: it skips when `node_modules` is already present (which it now is, thanks to the bake). `OpenCode`'s startup `mkdir(recursive)` won't clear a populated dir, so the baked tree survives and still avoids the reify. The seed stays for environments where the baked dir isn't present (e.g. a different `HOME`). Added timing/visibility so the boot cost is measurable (this was the other half of the ask): - `opencode.repo_deps_staged` `duration_ms` — the pre-existing copy of the same tree into the **repo's** `.opencode/` (the remaining boot cost after this change). - `opencode.global_deps_seeded` `duration_ms` — the fallback seed, when it actually runs. - `opencode.global_deps_skip` `reason=already_present|foreign_manifest` — promoted to `info` so the baked steady-state is visible each boot. ## Effect - **Fresh base-image boots:** the global seed is a no-op (`global_deps_skip reason=already_present`) → that ~9s+ copy is gone from boot. - **Repo-images / snapshots:** pick up the baked dir as they rebuild/refresh; until then the runtime fallback keeps them correct. - No runtime `CACHE_BUSTER` dependency — the bake is build-time content; nothing per-session changes. ## Testing - `ruff check` / `ruff format --check` — clean (sandbox-runtime + `base.py`). - `pytest tests/` — 360 passed. - `mypy src/` — no new errors (mypy-neutral vs `main`: 12 ↔ 12). - `python -m py_compile base.py` — valid. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Enhanced logging and performance metrics for dependency operations with duration tracking * Improved observability of dependency staging and global configuration seeding processes <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…ColeMurray#766) (ColeMurray#794) ## Summary Follow-up to the [type-boundary review comment](ColeMurray#766 (comment)) on ColeMurray#766 (now merged). Extracts the generic Slack Block Kit primitives into a neutral module so non-App-Home Slack message builders stop depending on `app-home/*` for types they don't own. ## The problem ColeMurray#766 added `repo-clarification.ts` (a general clarification-message builder) and `slack-options.ts` (generic option-text helpers). Both had to import Slack primitive/block types from `app-home/slack-types.ts` — i.e. non-App-Home code reaching into a feature module for types it doesn't own. As the reviewer put it, this risks `app-home` becoming "the accidental home for all Slack message schemas." ## The change - **New `src/slack-blocks.ts`** — the neutral home for the Block Kit primitives the bot emits: text objects, interactive elements (button / static_select / external_select / plain_text_input), and layout blocks (header / section / actions / context / input / divider). - **`app-home/slack-types.ts`** now holds only App-Home view models (`ModelOption`, `AppHomeBlock`, `AppHomeView`, `AppHomeInteraction*`, …) and imports the primitives it composes from `../slack-blocks`. It shrank from 85 → 19 lines. - **Consumers repointed** — `repo-clarification.ts`, `slack-options.ts`, and the App-Home files (`view.ts`, `modals.ts`, `interactions.ts`) now import the primitives from `slack-blocks`. App-Home files keep importing their own view models from `slack-types`. Pure type relocation — no runtime/behavior change. (The `SlackSectionBlock.accessory` widening to `SlackBlockElement` already landed in ColeMurray#766; it just moved file here.) ## Testing - `npm run typecheck -w @open-inspect/slack-bot` ✅ - `npm test -w @open-inspect/slack-bot` ✅ (103 tests, unchanged) - `npm run lint` / prettier ✅ <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * Internal infrastructure reorganization for improved code organization. No user-facing changes. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (28)
Disabled knowledge base sources:
📝 WalkthroughWalkthroughFour independent changes: (1) a new ChangesControl Plane: SessionEventStream
Control Plane: AutomationStore SQL Constants and D1 Indexes
Slack Bot: Repo Clarification Quick-Pick and Shared Block Types
OpenCode Global Deps Seeding and Modal Image
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
✨ Finishing Touches📝 Generate docstrings
Comment |
Terraform Validation Results
Pushed by: @NicolasWalter, Action: |
Terraform Plan ResultsStatus: ✅ Success Show Planterraform_data.access_control_gate: Refreshing state... [id=841ab6bc-98a7-018c-1031-ebad4f8b62bc]
local_file.web_app_wrangler_production[0]: Refreshing state... [id=9bd3343f867953f1e601d80f48dd1c82430d711a]
module.modal_app[0].null_resource.modal_secrets[0]: Refreshing state... [id=7301815410246887448]
null_resource.slack_bot_build[0]: Refreshing state... [id=6535395330481400982]
data.external.modal_source_hash[0]: Reading...
null_resource.github_bot_build[0]: Refreshing state... [id=3138745404653145176]
null_resource.linear_bot_build[0]: Refreshing state... [id=4070563134202286176]
null_resource.web_app_cloudflare_build[0]: Refreshing state... [id=5668476802919944411]
null_resource.control_plane_build: Refreshing state... [id=2489708833689731854]
module.github_kv[0].cloudflare_workers_kv_namespace.this: Refreshing state... [id=87dbfaa1d9ce4a37a42e04c57c434a72]
cloudflare_r2_bucket.media: Refreshing state... [id=open-inspect-media-primo]
module.session_index_kv.cloudflare_workers_kv_namespace.this: Refreshing state... [id=7f18644fbed34121bbe3a196f373ea93]
module.slack_kv[0].cloudflare_workers_kv_namespace.this: Refreshing state... [id=729b357dbb5e4c9d99ec9212cc45766e]
module.linear_kv[0].cloudflare_workers_kv_namespace.this: Refreshing state... [id=d003f1ad81384910a1f48a0a33f18c09]
cloudflare_d1_database.main: Refreshing state... [id=dba95b03-ace9-47a8-81e9-6e39d8d694c5]
data.external.modal_source_hash[0]: Read complete after 0s [id=-]
module.modal_app[0].null_resource.modal_deploy: Refreshing state... [id=2370527764495417767]
module.slack_bot_worker[0].cloudflare_worker.this: Refreshing state... [id=5200e96d69804ea296e1f3a6b39e4243]
module.linear_bot_worker[0].cloudflare_worker.this: Refreshing state... [id=33782d80e8ff4af9b30b92870084b674]
null_resource.d1_migrations: Refreshing state... [id=3023541701414521585]
module.slack_bot_worker[0].cloudflare_worker_version.this: Refreshing state... [id=1d0da20d-930d-4c88-9ea5-9cc8d107b90c]
module.linear_bot_worker[0].cloudflare_worker_version.this: Refreshing state... [id=5e848275-1e27-4ac1-a626-4eef6fc7d956]
module.linear_bot_worker[0].cloudflare_workers_deployment.this: Refreshing state... [id=91985411-8446-4e31-82df-598cc92fb5ef]
module.slack_bot_worker[0].cloudflare_workers_deployment.this: Refreshing state... [id=25369ad3-bd58-4b50-a3de-bd2925368003]
module.control_plane_worker.cloudflare_worker.this: Refreshing state... [id=c208a60c393e45e38eb502346bb7ce1e]
module.control_plane_worker.cloudflare_worker_version.this: Refreshing state... [id=660ce2fc-88e8-42ba-9a37-984e041bc730]
module.control_plane_worker.cloudflare_workers_deployment.this: Refreshing state... [id=e55c5982-701a-418d-aa74-a3296cc57460]
module.control_plane_worker.cloudflare_workers_cron_trigger.this[0]: Refreshing state... [id=open-inspect-control-plane-primo]
null_resource.web_app_cloudflare_deploy[0]: Refreshing state... [id=6794976783698339297]
module.github_bot_worker[0].cloudflare_worker.this: Refreshing state... [id=4b5e2696491a41eaaa124f4e2a9855f2]
null_resource.web_app_cloudflare_secrets[0]: Refreshing state... [id=8008019977081871834]
module.github_bot_worker[0].cloudflare_worker_version.this: Refreshing state... [id=c488ac9d-bc94-4ba5-a74d-0e24949a69d8]
module.github_bot_worker[0].cloudflare_workers_deployment.this: Refreshing state... [id=d872f1c0-2446-456b-9e9c-1e59609e7aed]
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+ create
~ update in-place
-/+ destroy and then create replacement
Terraform will perform the following actions:
# local_file.web_app_wrangler_production[0] will be created
+ resource "local_file" "web_app_wrangler_production" {
+ content = (sensitive value)
+ content_base64sha256 = (known after apply)
+ content_base64sha512 = (known after apply)
+ content_md5 = (known after apply)
+ content_sha1 = (known after apply)
+ content_sha256 = (known after apply)
+ content_sha512 = (known after apply)
+ directory_permission = "0777"
+ file_permission = "0777"
+ filename = "../../..//packages/web/wrangler.production.toml"
+ id = (known after apply)
}
# null_resource.control_plane_build must be replaced
-/+ resource "null_resource" "control_plane_build" {
~ id = "2489708833689731854" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-06-19T09:55:56Z" -> (known after apply)
}
}
# null_resource.d1_migrations must be replaced
-/+ resource "null_resource" "d1_migrations" {
~ id = "3023541701414521585" -> (known after apply)
~ triggers = { # forces replacement
~ "migrations_sha" = "1610d2f2f098b1fd95961c89018fe5e912a7d8a5449290c672780618b2377c37" -> "0d71f5deb332be8fef764418c4bd37b5a1c9c6fbe3464e697e2e10f436113391"
# (1 unchanged element hidden)
}
}
# null_resource.github_bot_build[0] must be replaced
-/+ resource "null_resource" "github_bot_build" {
~ id = "3138745404653145176" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-06-19T09:55:55Z" -> (known after apply)
}
}
# null_resource.linear_bot_build[0] must be replaced
-/+ resource "null_resource" "linear_bot_build" {
~ id = "4070563134202286176" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-06-19T09:55:56Z" -> (known after apply)
}
}
# null_resource.slack_bot_build[0] must be replaced
-/+ resource "null_resource" "slack_bot_build" {
~ id = "6535395330481400982" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-06-19T09:55:55Z" -> (known after apply)
}
}
# null_resource.web_app_cloudflare_build[0] must be replaced
-/+ resource "null_resource" "web_app_cloudflare_build" {
~ id = "5668476802919944411" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-06-19T09:55:55Z" -> (known after apply)
}
}
# null_resource.web_app_cloudflare_deploy[0] must be replaced
-/+ resource "null_resource" "web_app_cloudflare_deploy" {
~ id = "6794976783698339297" -> (known after apply)
~ triggers = { # forces replacement
~ "always_run" = "2026-06-19T09:56:26Z" -> (known after apply)
}
}
# module.control_plane_worker.cloudflare_worker.this will be updated in-place
~ resource "cloudflare_worker" "this" {
id = "c208a60c393e45e38eb502346bb7ce1e"
name = "open-inspect-control-plane-primo"
~ observability = {
~ logs = {
+ destinations = (known after apply)
# (4 unchanged attributes hidden)
}
~ traces = {
+ destinations = (known after apply)
# (3 unchanged attributes hidden)
}
# (2 unchanged attributes hidden)
}
~ references = {
~ dispatch_namespace_outbounds = [] -> (known after apply)
~ domains = [] -> (known after apply)
~ durable_objects = [
- {
- namespace_id = "4c77239db3614a6aac69a90e1fbd8955" -> null
- namespace_name = "open-inspect-control-plane-primo_SessionDO" -> null
- worker_id = "c208a60c393e45e38eb502346bb7ce1e" -> null
- worker_name = "open-inspect-control-plane-primo" -> null
},
- {
- namespace_id = "bf3328c8ebcb4039855ed7fcca6eb7e9" -> null
- namespace_name = "open-inspect-control-plane-primo_SchedulerDO" -> null
- worker_id = "c208a60c393e45e38eb502346bb7ce1e" -> null
- worker_name = "open-inspect-control-plane-primo" -> null
},
] -> (known after apply)
~ queues = [] -> (known after apply)
~ workers = [
- {
- id = "ac07332f8b0f4cdfa4ca04f966a9fa61" -> null
- name = "open-inspect-web-primo" -> null
},
- {
- id = "4b5e2696491a41eaaa124f4e2a9855f2" -> null
- name = "open-inspect-github-bot-primo" -> null
},
- {
- id = "33782d80e8ff4af9b30b92870084b674" -> null
- name = "open-inspect-linear-bot-primo" -> null
},
- {
- id = "5200e96d69804ea296e1f3a6b39e4243" -> null
- name = "open-inspect-slack-bot-primo" -> null
},
] -> (known after apply)
} -> (known after apply)
tags = []
~ updated_on = "2026-06-19T09:55:58Z" -> (known after apply)
# (6 unchanged attributes hidden)
}
# module.control_plane_worker.cloudflare_worker_version.this must be replaced
-/+ resource "cloudflare_worker_version" "this" {
~ annotations = {
+ workers_message = (known after apply)
+ workers_tag = (known after apply)
~ workers_triggered_by = "create_version_api" -> (known after apply)
} -> (known after apply)
~ bindings = (sensitive value) # forces replacement
~ created_on = "2026-06-19T09:55:59Z" -> (known after apply)
~ id = "660ce2fc-88e8-42ba-9a37-984e041bc730" -> (known after apply)
+ limits = (known after apply)
+ main_script_base64 = (known after apply)
~ migration_tag = "v1" -> (known after apply)
~ modules = [
- { # forces replacement
- content_file = "../../..//packages/control-plane/dist/index.js" -> null
- content_sha256 = "8796a97c6a5c3f96b661e30a4c5f0e68d213c3be14f9f8f04870b8825eb90932" -> null
- content_type = "application/javascript+module" -> null
- name = "index.js" -> null
},
+ { # forces replacement
+ content_file = "../../..//packages/control-plane/dist/index.js"
+ content_sha256 = "077bcc2242183e3bf2b71a57c786bbc66f1b8eeef3956736092e3336bcb72625"
+ content_type = "application/javascript+module"
+ name = "index.js"
},
]
~ number = 33 -> (known after apply)
~ source = "terraform" -> (known after apply)
~ startup_time_ms = 5 -> (known after apply)
~ urls = [] -> (known after apply)
# (6 unchanged attributes hidden)
}
# module.control_plane_worker.cloudflare_workers_deployment.this must be replaced
-/+ resource "cloudflare_workers_deployment" "this" {
~ annotations = {
+ workers_message = (known after apply)
~ workers_triggered_by = "deployment" -> (known after apply)
} -> (known after apply)
~ author_email = "alejo@primo.la" -> (known after apply)
~ created_on = "2026-06-19T09:56:00Z" -> (known after apply)
~ id = "e55c5982-701a-418d-aa74-a3296cc57460" -> (known after apply)
~ source = "terraform" -> (known after apply)
~ versions = [ # forces replacement
~ {
~ version_id = "660ce2fc-88e8-42ba-9a37-984e041bc730" -> (known after apply)
# (1 unchanged attribute hidden)
},
]
# (3 unchanged attributes hidden)
}
# module.github_bot_worker[0].cloudflare_worker.this will be updated in-place
~ resource "cloudflare_worker" "this" {
id = "4b5e2696491a41eaaa124f4e2a9855f2"
name = "open-inspect-github-bot-primo"
~ observability = {
~ logs = {
+ destinations = (known after apply)
# (4 unchanged attributes hidden)
}
~ traces = {
+ destinations = (known after apply)
# (3 unchanged attributes hidden)
}
# (2 unchanged attributes hidden)
}
~ references = {
~ dispatch_namespace_outbounds = [] -> (known after apply)
~ domains = [] -> (known after apply)
~ durable_objects = [] -> (known after apply)
~ queues = [] -> (known after apply)
~ workers = [] -> (known after apply)
} -> (known after apply)
tags = []
~ updated_on = "2026-06-19T09:56:00Z" -> (known after apply)
# (6 unchanged attributes hidden)
}
# module.github_bot_worker[0].cloudflare_worker_version.this must be replaced
-/+ resource "cloudflare_worker_version" "this" {
~ annotations = {
+ workers_message = (known after apply)
+ workers_tag = (known after apply)
~ workers_triggered_by = "create_version_api" -> (known after apply)
} -> (known after apply)
~ bindings = (sensitive value) # forces replacement
~ created_on = "2026-06-19T09:56:01Z" -> (known after apply)
~ id = "c488ac9d-bc94-4ba5-a74d-0e24949a69d8" -> (known after apply)
+ limits = (known after apply)
+ main_script_base64 = (known after apply)
+ migration_tag = (known after apply)
~ number = 17 -> (known after apply)
~ source = "terraform" -> (known after apply)
~ startup_time_ms = 2 -> (known after apply)
~ urls = [
- "https://c488ac9d-open-inspect-github-bot-primo.primo-bf6.workers.dev",
] -> (known after apply)
# (7 unchanged attributes hidden)
}
# module.github_bot_worker[0].cloudflare_workers_deployment.this must be replaced
-/+ resource "cloudflare_workers_deployment" "this" {
~ annotations = {
+ workers_message = (known after apply)
~ workers_triggered_by = "deployment" -> (known after apply)
} -> (known after apply)
~ author_email = "alejo@primo.la" -> (known after apply)
~ created_on = "2026-06-19T09:56:02Z" -> (known after apply)
~ id = "d872f1c0-2446-456b-9e9c-1e59609e7aed" -> (known after apply)
~ source = "terraform" -> (known after apply)
~ versions = [ # forces replacement
~ {
~ version_id = "c488ac9d-bc94-4ba5-a74d-0e24949a69d8" -> (known after apply)
# (1 unchanged attribute hidden)
},
]
# (3 unchanged attributes hidden)
}
# module.linear_bot_worker[0].cloudflare_worker.this will be updated in-place
~ resource "cloudflare_worker" "this" {
id = "33782d80e8ff4af9b30b92870084b674"
name = "open-inspect-linear-bot-primo"
~ observability = {
~ logs = {
+ destinations = (known after apply)
# (4 unchanged attributes hidden)
}
~ traces = {
+ destinations = (known after apply)
# (3 unchanged attributes hidden)
}
# (2 unchanged attributes hidden)
}
~ references = {
~ dispatch_namespace_outbounds = [] -> (known after apply)
~ domains = [] -> (known after apply)
~ durable_objects = [] -> (known after apply)
~ queues = [] -> (known after apply)
~ workers = [
- {
- id = "c208a60c393e45e38eb502346bb7ce1e" -> null
- name = "open-inspect-control-plane-primo" -> null
},
] -> (known after apply)
} -> (known after apply)
tags = []
~ updated_on = "2026-06-19T09:55:56Z" -> (known after apply)
# (6 unchanged attributes hidden)
}
# module.linear_bot_worker[0].cloudflare_worker_version.this must be replaced
-/+ resource "cloudflare_worker_version" "this" {
~ annotations = {
+ workers_message = (known after apply)
+ workers_tag = (known after apply)
~ workers_triggered_by = "create_version_api" -> (known after apply)
} -> (known after apply)
~ bindings = (sensitive value) # forces replacement
~ created_on = "2026-06-19T09:55:57Z" -> (known after apply)
~ id = "5e848275-1e27-4ac1-a626-4eef6fc7d956" -> (known after apply)
+ limits = (known after apply)
+ main_script_base64 = (known after apply)
+ migration_tag = (known after apply)
~ number = 30 -> (known after apply)
~ source = "terraform" -> (known after apply)
~ startup_time_ms = 3 -> (known after apply)
~ urls = [
- "https://5e848275-open-inspect-linear-bot-primo.primo-bf6.workers.dev",
] -> (known after apply)
# (7 unchanged attributes hidden)
}
# module.linear_bot_worker[0].cloudflare_workers_deployment.this must be replaced
-/+ resource "cloudflare_workers_deployment" "this" {
~ annotations = {
+ workers_message = (known after apply)
~ workers_triggered_by = "deployment" -> (known after apply)
} -> (known after apply)
~ author_email = "alejo@primo.la" -> (known after apply)
~ created_on = "2026-06-19T09:55:58Z" -> (known after apply)
~ id = "91985411-8446-4e31-82df-598cc92fb5ef" -> (known after apply)
~ source = "terraform" -> (known after apply)
~ versions = [ # forces replacement
~ {
~ version_id = "5e848275-1e27-4ac1-a626-4eef6fc7d956" -> (known after apply)
# (1 unchanged attribute hidden)
},
]
# (3 unchanged attributes hidden)
}
# module.modal_app[0].null_resource.modal_deploy must be replaced
-/+ resource "null_resource" "modal_deploy" {
~ id = "2370527764495417767" -> (known after apply)
~ triggers = { # forces replacement
~ "source_hash" = "799e872626865b08b5116a9b1611934ab5d6e32067302b0b0d4e0a4249f99b88" -> "e87f39f2f4c09a2cfced5e0996c2059e34e90685d221807b75e94c0fb63b4931"
# (3 unchanged elements hidden)
}
}
# module.slack_bot_worker[0].cloudflare_worker.this will be updated in-place
~ resource "cloudflare_worker" "this" {
id = "5200e96d69804ea296e1f3a6b39e4243"
name = "open-inspect-slack-bot-primo"
~ observability = {
~ logs = {
+ destinations = (known after apply)
# (4 unchanged attributes hidden)
}
~ traces = {
+ destinations = (known after apply)
# (3 unchanged attributes hidden)
}
# (2 unchanged attributes hidden)
}
~ references = {
~ dispatch_namespace_outbounds = [] -> (known after apply)
~ domains = [] -> (known after apply)
~ durable_objects = [] -> (known after apply)
~ queues = [] -> (known after apply)
~ workers = [
- {
- id = "c208a60c393e45e38eb502346bb7ce1e" -> null
- name = "open-inspect-control-plane-primo" -> null
},
] -> (known after apply)
} -> (known after apply)
tags = []
~ updated_on = "2026-06-19T09:55:56Z" -> (known after apply)
# (6 unchanged attributes hidden)
}
# module.slack_bot_worker[0].cloudflare_worker_version.this must be replaced
-/+ resource "cloudflare_worker_version" "this" {
~ annotations = {
+ workers_message = (known after apply)
+ workers_tag = (known after apply)
~ workers_triggered_by = "create_version_api" -> (known after apply)
} -> (known after apply)
~ bindings = (sensitive value) # forces replacement
~ created_on = "2026-06-19T09:55:57Z" -> (known after apply)
~ id = "1d0da20d-930d-4c88-9ea5-9cc8d107b90c" -> (known after apply)
+ limits = (known after apply)
+ main_script_base64 = (known after apply)
+ migration_tag = (known after apply)
~ modules = [
- { # forces replacement
- content_file = "../../..//packages/slack-bot/dist/index.js" -> null
- content_sha256 = "d6498f73b15ad15b850b8ebf820d54fc1b09d3046900f8c57ced89d19fe6215e" -> null
- content_type = "application/javascript+module" -> null
- name = "index.js" -> null
},
+ { # forces replacement
+ content_file = "../../..//packages/slack-bot/dist/index.js"
+ content_sha256 = "c47a636a79e7277319100b5b9ea19d4c73fae47629158f3d85f457f4e60b922a"
+ content_type = "application/javascript+module"
+ name = "index.js"
},
]
~ number = 33 -> (known after apply)
~ source = "terraform" -> (known after apply)
~ startup_time_ms = 6 -> (known after apply)
~ urls = [
- "https://1d0da20d-open-inspect-slack-bot-primo.primo-bf6.workers.dev",
] -> (known after apply)
# (6 unchanged attributes hidden)
}
# module.slack_bot_worker[0].cloudflare_workers_deployment.this must be replaced
-/+ resource "cloudflare_workers_deployment" "this" {
~ annotations = {
+ workers_message = (known after apply)
~ workers_triggered_by = "deployment" -> (known after apply)
} -> (known after apply)
~ author_email = "alejo@primo.la" -> (known after apply)
~ created_on = "2026-06-19T09:55:58Z" -> (known after apply)
~ id = "25369ad3-bd58-4b50-a3de-bd2925368003" -> (known after apply)
~ source = "terraform" -> (known after apply)
~ versions = [ # forces replacement
~ {
~ version_id = "1d0da20d-930d-4c88-9ea5-9cc8d107b90c" -> (known after apply)
# (1 unchanged attribute hidden)
},
]
# (3 unchanged attributes hidden)
}
Plan: 17 to add, 4 to change, 16 to destroy.
─────────────────────────────────────────────────────────────────────────────
Saved the plan to: tfplan
To perform exactly these actions, run the following command to apply:
terraform apply "tfplan"Pushed by: @NicolasWalter |
Automated upstream sync
Clean merge from
ColeMurray/background-agents@main.This PR was opened automatically by
.github/workflows/sync-upstream.yml. Review the commit list and merge when CI is green.Summary by CodeRabbit
Release Notes
New Features
Improvements
Dependencies