Skip to content

chore: sync with upstream 2026-06-20 - #38

Merged
NicolasWalter merged 10 commits into
mainfrom
sync/upstream-2026-06-20
Jun 20, 2026
Merged

chore: sync with upstream 2026-06-20#38
NicolasWalter merged 10 commits into
mainfrom
sync/upstream-2026-06-20

Conversation

@NicolasWalter

@NicolasWalter NicolasWalter commented Jun 20, 2026

Copy link
Copy Markdown

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

    • Added quick-pick buttons to repository selection in Slack for faster repo clarification.
    • Improved error handling for repository suggestion options in Slack App Home.
  • Improvements

    • Optimized automation run recovery sweeps with targeted database indexes.
    • Enhanced sandbox startup performance by pre-caching plugin dependencies.
  • Dependencies

    • Updated Modal runtime dependency to version 1.4.3.

ColeMurray and others added 10 commits June 19, 2026 10:38
## 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 -->
@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e8f43354-b546-4c59-b672-c2fb6ac6dc75

📥 Commits

Reviewing files that changed from the base of the PR and between ebdcae5 and 79b2a5a.

⛔ Files ignored due to path filters (1)
  • packages/modal-infra/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (28)
  • packages/control-plane/src/db/automation-store.ts
  • packages/control-plane/src/session/durable-object.ts
  • packages/control-plane/src/session/event-stream.test.ts
  • packages/control-plane/src/session/event-stream.ts
  • packages/control-plane/src/session/http/handlers/messages.handler.test.ts
  • packages/control-plane/src/session/http/handlers/messages.handler.ts
  • packages/control-plane/src/session/services/message.service.test.ts
  • packages/control-plane/src/session/services/message.service.ts
  • packages/control-plane/src/types.ts
  • packages/control-plane/test/integration/automation-store.test.ts
  • packages/control-plane/vitest.integration.config.ts
  • packages/modal-infra/pyproject.toml
  • packages/modal-infra/src/images/base.py
  • packages/sandbox-runtime/src/sandbox_runtime/entrypoint.py
  • packages/sandbox-runtime/tests/test_tool_installation.py
  • packages/slack-bot/src/app-home/interactions.ts
  • packages/slack-bot/src/app-home/modals.ts
  • packages/slack-bot/src/app-home/slack-types.ts
  • packages/slack-bot/src/app-home/view.ts
  • packages/slack-bot/src/classifier/repos.ts
  • packages/slack-bot/src/index.test.ts
  • packages/slack-bot/src/index.ts
  • packages/slack-bot/src/repo-clarification.test.ts
  • packages/slack-bot/src/repo-clarification.ts
  • packages/slack-bot/src/slack-blocks.ts
  • packages/slack-bot/src/slack-options.ts
  • terraform/d1/migrations/0024_fix_recovery_sweep_indexes.sql
  • terraform/environments/production/modal.tf

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.


📝 Walkthrough

Walkthrough

Four independent changes: (1) a new SessionEventStream class centralizes event replay, history pagination, and listing in the control plane, replacing inline logic in SessionDO and MessageService, with EventResponse/ListEventsResponse moved to @open-inspect/shared; (2) two D1 partial indexes replace a single combined index for recovery sweep queries, exposed as static SQL constants on AutomationStore; (3) a new repo-clarification.ts module adds quick-pick buttons and a shared slack-blocks.ts type foundation to the Slack bot's repo-picker flow; (4) OpenCode global dependency seeding is restructured into staged helpers in entrypoint.py, baked into the Modal base image at build time.

Changes

Control Plane: SessionEventStream

Layer / File(s) Summary
EventResponse/ListEventsResponse moved to @open-inspect/shared
packages/control-plane/src/types.ts
Removes locally declared EventResponse and ListEventsResponse interfaces and re-exports them from @open-inspect/shared.
SessionEventStream class: replay, history, list
packages/control-plane/src/session/event-stream.ts
Defines exported cursor/history types, SessionEventStreamRepository pick type, and SessionEventStream with getReplay, getHistoryPage, listEvents, plus private helpers parseSandboxEvents, cursorFromRow, toEventResponse, and clampHistoryLimit.
SessionEventStream unit tests
packages/control-plane/src/session/event-stream.test.ts
Adds Vitest tests covering getReplay (limits, cursor, hasMore, malformed JSON), getHistoryPage (param mapping, clamping), and listEvents (row projection, cursor encoding).
MessageService delegates listEvents to SessionEventStream
packages/control-plane/src/session/services/message.service.ts, packages/control-plane/src/session/services/message.service.test.ts
MessageService holds a SessionEventStream field and delegates listEvents to it; ListEventsRequest becomes a type alias of SessionEventListRequest. Test adds field-level assertions on mapped event shape.
messages.handler passes listEvents result directly
packages/control-plane/src/session/http/handlers/messages.handler.ts, ...messages.handler.test.ts
Removes manual field mapping and JSON.parse from the listEvents handler, returning Response.json(result) directly. Test mock updated to camelCase fields and object data.
SessionDO wired to SessionEventStream
packages/control-plane/src/session/durable-object.ts
Adds a lazy eventStream getter, replaces removed getReplayData() with eventStream.getReplay() in subscribe, and replaces inline timeline paging with eventStream.getHistoryPage() in fetch_history.

Control Plane: AutomationStore SQL Constants and D1 Indexes

Layer / File(s) Summary
D1 migration: partial indexes for recovery sweep
terraform/d1/migrations/0024_fix_recovery_sweep_indexes.sql
Drops idx_runs_active_status and creates idx_runs_orphan_sweep (status='starting', created_at) and idx_runs_timeout_sweep (status='running', started_at).
AutomationStore static SQL constants and integration tests
packages/control-plane/src/db/automation-store.ts, packages/control-plane/test/integration/automation-store.test.ts, packages/control-plane/vitest.integration.config.ts
Extracts recovery-sweep SQL into ORPHANED_STARTING_RUNS_SQL and TIMED_OUT_RUNNING_RUNS_SQL static constants; integration tests run EXPLAIN QUERY PLAN and assert each index name; integration config drops fileParallelism: false.

Slack Bot: Repo Clarification Quick-Pick and Shared Block Types

Layer / File(s) Summary
Shared Slack Block Kit types and option helpers
packages/slack-bot/src/slack-blocks.ts, packages/slack-bot/src/slack-options.ts, packages/slack-bot/src/app-home/slack-types.ts, packages/slack-bot/src/app-home/modals.ts
Adds slack-blocks.ts with all Block Kit primitive types and slack-options.ts with truncateSelectOptionText/plainTextOption; slack-types.ts re-imports from slack-blocks instead of defining them locally.
repo-clarification.ts: quick-pick buttons and clarification blocks
packages/slack-bot/src/repo-clarification.ts, packages/slack-bot/src/classifier/repos.ts
Introduces SELECT_REPO_ACTION_ID, SELECT_REPO_QUICK_PICK_ACTION_ID, MAX_REPO_QUICK_PICKS, getRepoClarificationOptions, buildRepoQuickPickButtons, and buildRepoClarificationBlocks; adds filterReposByQuery to the classifier.
repo-clarification tests
packages/slack-bot/src/repo-clarification.test.ts
Tests filterReposByQuery, buildRepoQuickPickButtons (capping, truncation, fallback), and buildRepoClarificationBlocks block composition.
app-home view.ts: use plainTextOption, remove truncation
packages/slack-bot/src/app-home/view.ts
Removes local truncateSelectOptionText and replaces option text construction with plainTextOption() from slack-options.
interactions.ts and index.ts: wire quick-pick and error handling
packages/slack-bot/src/app-home/interactions.ts, packages/slack-bot/src/index.ts
interactions.ts uses filterReposByQuery and wraps block_suggestion in try/catch; index.ts uses buildRepoClarificationBlocks and handles SELECT_REPO_QUICK_PICK_ACTION_ID in the interaction switch.
index.test.ts: quick-pick test and shared helpers
packages/slack-bot/src/index.test.ts
Adds buildNumberedRepos/mockReposFetch test helpers, a new test for SELECT_REPO_QUICK_PICK_ACTION_ID, and refactors existing tests to use the shared helpers.

OpenCode Global Deps Seeding and Modal Image

Layer / File(s) Summary
Modal dependency bump and Terraform hash update
packages/modal-infra/pyproject.toml, terraform/environments/production/modal.tf
Raises modal to >=1.4.3; extends modal_source_hash to include pyproject.toml and uv.lock.
base.py: bake OpenCode deps into global config dir
packages/modal-infra/src/images/base.py
Bumps CACHE_BUSTER to v52-bake-opencode-global-deps; adds build-time shell commands to copy /app/opencode-deps into /root/.config/opencode.
entrypoint.py: structured global-config seeding helpers
packages/sandbox-runtime/src/sandbox_runtime/entrypoint.py
Introduces _stage_opencode_deps, _resolve_opencode_global_config_dir, _seed_global_opencode_deps, and _prepare_opencode_filesystem; replaces the explicit install sequence in start_opencode with a single call.
Seeding tests
packages/sandbox-runtime/tests/test_tool_installation.py
Adds TestResolveGlobalConfigDir, TestSeedGlobalOpencodeDeps, and test_noop_when_staging_absent with a shared staging-tree fixture helper.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • primo-devs/primo-bg-coding-agent#31: Overlaps on the Slack repo clarification flow (shared action IDs, external_select options handling) and the vitest.integration.config.ts fileParallelism setting.
  • primo-devs/primo-bg-coding-agent#22: Overlaps on packages/sandbox-runtime/src/sandbox_runtime/entrypoint.py startup wiring and image-build callback flow.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown

Terraform Validation Results

Step Status
Format
Init
Validate

Pushed by: @NicolasWalter, Action: pull_request

@github-actions

Copy link
Copy Markdown

Terraform Plan Results

Status: ✅ Success

Show Plan
terraform_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

@NicolasWalter
NicolasWalter merged commit ea94237 into main Jun 20, 2026
17 of 18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants