Skip to content

Latest commit

 

History

History
197 lines (141 loc) · 30.2 KB

File metadata and controls

197 lines (141 loc) · 30.2 KB

AGENTS.md

Canonical guide for every AI tool working in this repo (Claude Code, OpenCode, Omoi, …). CLAUDE.md is a symlink to this file — edit AGENTS.md only.

FIX PLAN CLOSED (2026-06-13): docs/MASTER-FIX-PLAN.md — all 8 workstreams resolved (kill-D1, sandbox tool plumbing, repo-image kill, CORS/deploy, CI conformance harness, Void decision, doc/ADR cleanup, Modal hygiene). The doc is now a shipped log with per-WS evidence + commits; each landed fix is guarded by a tripwire test. Re-ground with npm run verify:claims + scripts/verify-claims/deep-verify.md before assuming anything reopened. Backing audits: docs/gaps-and-issues/2026-06-12-{doc-rot,infra-reality}-audit.md.

How this is loaded. This file is the always-on entry. For navigation, start with CODEBASE_MAP.md — the layout map for the entire codebase. The small always-on safety rules live as separate blocks in .opencode/rules/*.md deep "why" is not inlined — it lives in docs/references/01-…05.md and the design pack; load those on demand. Keep this file dense and lean.

What this is

agent-coordinator is the control plane for a hosted coding-agent SDK — a Cloudflare Worker that:

  • Authenticates users (Better Auth: cookies, Bearer, x-api-key) and scopes everything to a (userId, organizationId) tuple.
  • Persists session metadata in D1 (Drizzle-managed).
  • Routes each session to a per-session SessionDO Durable Object that owns its own SQLite, the WebSocket hub (clients + one sandbox), the FIFO message queue, and an alarm-driven watchdog.
  • Asks Modal (the data plane, packages/modal-infra/) to spawn a sandbox running OpenCode + an agent bridge that dials back to the DO over WSS.

Consumer pattern: docs/consumer-saas-example.md. Subsystem deep-dives (the "why"): docs/references/01-…05.md.

Common commands

npm run dev                  # wrangler dev — control plane only, http://localhost:8789 (matches BETTER_AUTH_URL in .dev.vars; OAuth callback origin)
npm run dev:local            # full local stack: control plane (:8789) + chat (:5174) together, with a wedged-port guard. Flags: -- --real | --cp-only | --migrate. Prereq: ./dev.sh up (pg0) + .dev.vars
npm run setup                # one-time bootstrap: install + verify .dev.vars + ./dev.sh up (pg0) + local migrations → ready for dev:local
npm run doctor               # read-only health check: Node, .dev.vars, pg0, ports, D1/pg migration drift, stale dev procs, non-local DATABASE_URL warning
npm run deploy               # wrangler deploy to Cloudflare
npm run deploy:control-plane # prod deploy via acdeploy CLI (also deploy:chat, deploy:all; or `npm run acdeploy -- <args>`)
npm run env:up               # local deps pg0 + LiteLLM via acdev CLI (also env:down, env:verify; or `npm run acdev -- <args>`)
npm test                     # vitest run (Workers pool, miniflare, real DO + Postgres test DB)
npm run test:watch           # interactive
npx vitest run test/route.test.ts -t "creates a session"   # single test by name

npm run pg:generate          # drizzle-kit generate from src/db/pg/* → drizzle-pg/*.sql
npm run pg:migrate:local     # apply Postgres migrations to local pg0
npm run pg:migrate:remote    # apply to remote Neon (needs DATABASE_URL)
npm run cf-typegen           # regenerate worker-configuration.d.ts from wrangler.jsonc
npm run auth:generate        # regenerate src/db/auth-schema.ts from the auth.ts plugin set

test/e2e/*.sh are manual smoke scripts against a deployed env — not part of npm test. Modal-side Python: see packages/modal-infra/README.md (deploy with modal deploy deploy.py).

Always-on rules — read the block before editing the matching area

These small blocks in .opencode/rules/ are imperatives (OpenCode loads them automatically; everyone else: read on the trigger). Violating them can break tenant isolation or desync the system.

Rule block Covers Read when touching
.opencode/rules/multi-tenant-invariants.md DO-ID format, resolveContext as sole org resolver, RESERVED_SYSTEM_KEYS merge order, per-tenant AES-GCM, GitHub-App tokens, log-no-secrets auth, secrets, spawn/env-var build, DO routing, crypto, logging
.opencode/rules/provider-neutrality.md no default provider/model anywhere; provider = model.split("/")[0]; bare model → "unknown" never "anthropic" model/provider routing, sandbox config, bridge.py/entrypoint.py/types.py, Modal API
.opencode/rules/architectural-rules.md skeleton-vs-motion separation, port/adapter invariants, event-sourced state, idempotent effects, registry bijection, deterministic domain port interfaces, adapters, controller, event sink, state machines, DI wiring, cutover
.opencode/rules/cross-file-sync.md bridge protocol (both sides), duplicated Modal HMAC generator, generated files, modal deploy deploy.py only bridge events, auth/env/DB schema, Modal auth/deploy

Architecture (the parts that span multiple files)

Request → DO routing (src/index.ts, the only Hono entry). DO ID is resolved per session:

  • Real session: D1 session_index row → DO ID ${organizationId}:${sessionId}. Load-bearing — gives tenant isolation by construction (one org can't name another's DO).
  • Sandbox-only / no D1 row: DO ID sandbox:${sessionId} (smoke tests + sandbox-bridge handshake; auth happens in the DO via token-hash).
  • resolveSessionAndDo() + requireAuth() enforce caller organizationId == session's. The WS upgrade GET /api/sessions/:id/ws runs auth inline (the standard middleware path conflicts with the upgrade).

Auth precedence (requireAuth): (1) x-api-key → Better Auth apiKey plugin (referenceId=userId, optional metadata.organizationId; membership re-checked every call) → (2) Authorization: Bearer → direct session.token lookup (fast path) → (3) cookie via auth.api.getSession(). resolveContext() (src/lib/resolve-context.ts) is the single place that decides the organizationId — valid activeOrganizationId or fail-closed; else earliest membership; no memberships → personal workspace auto-created.

Generated auth schema. src/db/pg/auth-schema.ts is generated by Better Auth from the plugin set in the root auth.ts stub (CLI-only). Change plugins in src/lib/auth.tsmirror in auth.tsnpm run auth:generate. src/db/pg/app-schema.ts is yours (session_index, audit_log, tenant_secret, env profiles, …). The drizzle-pg config reads src/db/pg/index.ts. Never hand-edit auth-schema.ts or worker-configuration.d.ts.

SessionDO (src/do/session-do.ts) — one DO/session, hibernates when idle. Survives hibernation: a private SQLite (session, participants, messages, events, artifacts, sandbox_state, ws_client_mapping; initSchema() recreates idempotently), hibernation-safe WS (ctx.acceptWebSocket() + auto ping/pong), and alarms (exec timeout 5 min, heartbeat stale 1 min, connecting timeout 30 s, inactivity 30 min → snapshot/cleanup). The FIFO queue = messages rows (pending|processing|completed|failed); processMessageQueue() is the only dispatcher (short-circuits if any processing or no sandbox), called on message create, execution_complete, and sandbox connect. Bridge handshake: auth_token_hash (SHA-256) stored in sandbox_state before spawn; bridge presents bare token once, DO never re-sees it; sandbox WS carries X-Sandbox-ID. Critical events (execution_complete, error, snapshot_ready, push_complete, push_error) get an ackId, re-sent until ACKed. POST /init = idempotent prod session-init (INSERT OR REPLACE); POST /start = HTTP spawn trigger (fire-and-forget, 200 spawning); ?action=__init|__trigger_alarm|__set_processing_time are test-only (__ prefix).

Modal data plane glue (src/lib/): github-token-resolver.ts (GitHub-App installation tokens, TTL-cached, 5-min margin — always for clone/push, never user OAuth); secret-repo.ts+crypto.ts (tenant_secret, per-tenant AES-GCM via HKDF of ENCRYPTION_KEY+organization_id — cross-tenant decrypt impossible); mcp-config.ts (validates user MCP configs — rejects shell metachars, path traversal, absolute paths); Modal HMAC = Bearer <ts>.<HMAC-SHA256(MODAL_API_SECRET, ts)> (session-do.ts:generateModalToken). In spawnSandbox()/buildSandboxEnvVars(), system vars override user secrets for RESERVED_SYSTEM_KEYSdo not reorder this merge (re-opens a privilege-escalation path).

Repo-image stack removed (2026-06-12). The dead repo-image feature — the gated CP cron (src/cron/repo-image-build.ts), the Modal */30 scheduler (image_builder.py), and the api_build_repo_image/api_delete_provider_image Modal endpoints — was deleted in the merged WS3+WS1 cut. There is no scheduled worker handler and no crons trigger. Pre-built images are delivered solely by the env-profile composite path (api_validate_environmentagent_runtime_layerprovider_image_id → spawn from_id). See docs/MASTER-FIX-PLAN.md WS3.

Local development & database

Front door = npm scripts (see Common commands). One-time bootstrap: npm run setup. Daily driver: npm run dev:local (control plane :8789 + chat :5174 together, with a wedged-port guard + clean teardown). Health/triage: npm run doctor (read-only). Deps only: npm run env:up (pg0 + LiteLLM). Reset a LOCAL db: npm run db:reset:local (-- --test targets the test db). All are zero-dep scripts/*.ts run via Node native TS stripping.

Data stores. Each Durable Object (SessionDO, OrgEventsDO) owns a private internal SQLite (DO-only; not a shared DB). Postgres via the HYPERDRIVE binding (pg0 on 127.0.0.1:5440 locally, Neon in prod) backs everything external: Better Auth (users/orgs/sessions/api keys), session_index (DO routing, src/db/pg/app-schema.ts), and app data (agent_runs, tenant_secret, env profiles, audit_log). src/lib/auth.ts = drizzleAdapter(getPgDb(env.HYPERDRIVE.connectionString)). D1 was removed 2026-06-12 (only the dead cron used it). Local dev db = agent_coordinator; the test suite is isolated in agent_coordinator_test.

Three load-bearing local-dev gotchas:

  1. The pg pool is per-request EVERYWHERE — never cached at module scope (prod included). getPgDb (src/db/pg/pool.ts) builds a fresh pg.Pool per call and never caches across requests. In workerd a pg socket opened in one request's IO context can't be reused from a later request's context — pool.query() checks out that dead socket and the request hangs ("…code had hung…"). This was originally thought to be a local-only issue (no Hyperdrive proxy), gated behind globalThis.__PG_NO_POOL_CACHE__ for dev/test only — but production hung the same way (a cached connection dies on Neon idle / scale-to-zero, biting auth sign-up + provider-key writes + every PG endpoint), so the cache, the flag, the pg-no-pool-cache test setupFile, and closePgPool are all removed: per-request pools unconditionally. Hyperdrive does the real server-side pooling; withPgDb (src/db/pg/client.ts) is the explicit open/run/close variant.
  2. DATABASE_URL is a prod footgun for :remote / raw drizzle-kit. npm run pg:migrate:local is now loopback-hardcoded — it pins the local pg0 URL and ignores any ambient DATABASE_URL (matching db:reset:local, and protecting the pg:migrate:local call inside dev:local), so a stray prod Neon URL can never be migrated by a :local command. The ambient value still wins for pg:migrate:remote and a raw drizzle-kit migrate; setup still pins the local URL explicitly; doctor warns when DATABASE_URL is non-local. Keep it unset for local work.
  3. pg:migrate:local (drizzle-kit) still can't replay 0006 from scratch — but the test migrator now can. drizzle-pg/0006 adds the installation_repository → github_installation(installation_id) FK before the unique key it needs, so a raw drizzle-kit replay of 0006 onto an empty db errors (SQLSTATE 42830), and editing 0006 would change its hash and break __drizzle_migrations idempotency on live dev/CI dbs — never edit it. The custom migrator in test/helpers/pg-global-setup.ts now tolerates this (defers any FK that fails 42830, retries after the whole chain applies — by which point 0007's named UNIQUE constraint exists; eb4af34), so the test/CI TEST_DATABASE_URL from-scratch path replays cleanly and CREATE DATABASE … TEMPLATE cloning is now a speed optimization, not a workaround. But npm run setup's pg:migrate:local uses drizzle-kit (no deferral), so fresh-machine setup still expects a pg0 that already carries the schema (clone/branch to provision). drizzle-pg/0009_fix_fk_ordering idempotently guarantees the unique constraint (a DO $$ guard, no-op on migrated dbs) but is journaled after 0006, so it can't rescue a raw 0006 replay.

Testing

@cloudflare/vitest-pool-workers runs every test inside a real Workers isolate + miniflare. Postgres-backed tests run against an isolated agent_coordinator_test DB via TEST_DATABASE_URL (test/helpers/pg-global-setup.ts replays the drizzle-pg chain from scratch). Import from cloudflare:test: SELF (the worker — SELF.fetch(...)) and env (bindings — env.SESSION_DO.idFromName(...) for direct DO tests; env.HYPERDRIVE for Postgres). Auth tests use the HTTP signup→cookie pattern (shared factories in test/helpers/auth.ts; whole-project nets in test/integration/). There is no D1 — see test/node/no-d1.test.ts.

Postgres-backed tests run against an isolated agent_coordinator_test db. Locally, test/helpers/pg-global-setup.ts creates it by CREATE DATABASE … TEMPLATE from the dev schema (see gotcha 3) and TRUNCATEs it per run, so the suite never pollutes the dev agent_coordinator db. In CI (.github/workflows/ci.yml) the typescript-tests job runs a postgres:18 service (matching pg0) exposing an empty agent_coordinator_test on 127.0.0.1:5440, and TEST_DATABASE_URL points the suite at it; pg-global-setup then replays the drizzle-pg chain from scratch (its deferred-FK loop tolerates 0006's ordering), so CI is self-contained and needs no external Neon branch/secret. The worker isolate disables the pg pool cache via test/helpers/pg-no-pool-cache.ts.

Packages monorepo (packages/)

Two vendored Python packages (own uv locks; each has a DEVIATIONS.md for the namespace rename from upstream background-agents-reference):

  • modal-infra/ — Modal app: 6 web endpoints api_create_sandbox/api_sandbox_lifecycle/api_snapshot_sandbox/api_restore_sandbox/api_validate_environment/api_health, HMAC auth, GitHub-App token issuance, image defs. Deploy with modal deploy deploy.py (never modal deploy src/app.py — only deploy.py/-m src registers all modules). (api_warm_sandbox/api_build_repo_image/api_delete_provider_image were removed — consolidated into api_sandbox_lifecycle / repo-image kill.)
  • sandbox-runtime/ — runs inside the sandbox: supervisor entrypoint, bridge, OpenCode integration. Module agent_coordinator_sandbox_runtime (renamed from upstream sandbox_runtime).

Bridge-protocol changes must land in both packages/sandbox-runtime/ and src/do/session-do.ts:processSandboxEvent (see cross-file-sync.md).

Configuration

  • wrangler.jsonc vars — non-secret config (BETTER_AUTH_URL, GITHUB_CLIENT_ID, GITHUB_APP_ID, ENCRYPTION_KEY, CONTROL_PLANE_URL, MODAL_API_URL). The ENCRYPTION_KEY here is a dev placeholder; prod uses wrangler secret put ENCRYPTION_KEY.
  • Secrets — BETTER_AUTH_SECRET, RESEND_API_KEY, GITHUB_CLIENT_SECRET, GITHUB_APP_PRIVATE_KEY, MODAL_API_SECRET. Dev: .dev.vars. Prod: wrangler secret put ….
  • drizzle.config.ts — Drizzle d1-http driver; CLI needs CLOUDFLARE_API_TOKEN.
  • tsconfig.json@/*./src/* (rare; most code uses relative imports).

Repo conventions (beyond the always-on rule blocks)

  • One auth resolver: resolveContext. Never read member directly to infer the org.
  • Audit writes are non-blocking: audit.log() failures are caught and swallowed — never make the primary operation depend on audit success.
  • session-do.ts is large (~1500+ lines) on purpose — single actor; schema/queue/spawn/event-processing share private state. Add helpers inside the class; don't extract modules without a stable boundary.

Project skills — design-pack routing + workflow

Seven project-scoped skills in .claude/skills/. Five encode the design-pack contracts (slices 1–7, §06); two are workflow skills (phase-review gate and session-state orientation). Claude Code auto-loads by keyword; other harnesses use this table — read the skill's SKILL.md first when the work matches. They're thin pointers (checklist + one copy-into-src/ starter artifact, not wired into the build); the design pack is the source of truth and .opencode/rules/*.md still owns the always-on invariants.

Working on… Skill Slice
spawn path, run creation/cancel, API-key minting, credential chain (session-do.ts:doSpawn, src/index.ts) governing-agent-runs 1–3
sandbox config, bridge protocol, LiteLLM gateway / virtual keys agent-run-contract (+ agent-run-contract.schema.ts) 6
adding/swapping a sandbox provider, lifecycle/heartbeat/cleanup provider-adapter-contract (+ sandbox-provider.ts) 7
src/db/pg/*, migrations, run/event data placement (Postgres) tenant-data-boundary (+ agent-runs.schema.ts, adr-0009-*.md) 4
making a repo agent-runnable: env profiles, setup autodiscovery environment-profiles §06
Workflow: iterative review gate after each phase (ask→approve→commit vs iterate) phase-review all
Workflow: session bootstrap — read state file, re-orient, track steps, verify project-orientation (+ session-state.json) all
Workflow: end-of-session wrap-up — verify, update state, log actions, plan next task-wrap-up all
Skill management: use the skill-freezer skill to move skills in/out of cold storage (e.g. "pull in wiki skills", "freeze the ads stuff").

packages/chat — design source of truth

Canonical target look = .sisyphus/design-reference/ (port verbatim): Thread.tsx (assistant-ui shadcn shell — warm layout, turn dividers / no assistant bubble, .surface user bubble, action bars, branch picker, .surface-elevated composer), theme.css (brown-red "Vertical sheen": shadcn light+.dark tokens, @theme inline, body glow/grain, .surface/.surface-elevated/.recessed-well/.code-pill), README.md (adoption notes; target shots in .sisyphus/evidence/reference/). The package keeps its custom MessageRenderer (streamdown + @pierre/diffs + 16 cards) wired into the shell; the 16 cards are rethemed to the warm tokens.

Void framework

Void is used by packages/chat only (SSR SPA, no custom Durable Objects — deploys via npx void deploy). For the control plane the Void entry-switch was ABANDONED 2026-06-12 (WS6): Void's generated entry can't re-export the custom DO classes (SessionDO, OrgEventsDO) that wrangler main requires, so the entry stays src/index.ts. Instead routes/* use the additive-mount patterndefineHandler files from the lightweight void/handler subpath, imported into the Hono app and mounted via app.get(...) (src/index.ts:29-39 mounts routes/api/runs/*, routes/{index,health,docs,openapi.json}, routes/api/config). void/handler bundles cleanly in Workers; the full void entry (pulls vite/node) does NOT.

Key fact: defineHandler(c) receives a standard Hono Contexthono/*/@hono/* packages, auth resolvers, streaming, and response helpers transfer unchanged; only the export differs (export const GET = defineHandler(...) vs app.get(...)).

import { defineHandler } from "void/handler";
export const GET  = defineHandler(async (c) => ({ data: "automatic JSON" }));
export const POST = defineHandler.withValidator({ body: schema })(async (c, { body }) => ({ ok: true }));

npm run void:prepare (void prepare) regenerates .void/*.d.ts and is required before tsc (the typecheck script runs it first); keep the generated .void/ dir. There is no void:build for the CP (removed — the entry is src/index.ts, deployed via wrangler). The guard test/node/void-cp-abandoned.test.ts keeps the dead routes/middleware/ scaffold from returning and locks the DO re-export. Rationale: docs/MASTER-FIX-PLAN.md WS6 + the archived .sisyphus/plans/void-migration-plan.md.

Known gaps / remaining work

Grounded in the code, prioritized. (See .sisyphus/plans/server-modernization-plan.md + docs/design/agent-architecture-review/.)

  • fresh-machine pg:migrate:local (drizzle-kit) still can't replay from scratch — the test/CI TEST_DATABASE_URL from-scratch path is now FIXED (pg-global-setup.ts defers the 0006 FK and retries; eb4af34), but drizzle-kit has no deferral, so a fresh dev machine still needs a cloned/branched pg0. A NEW forward migration is journaled after 0006 so can't help; durable fix = teach npm run setup to clone, or route bootstrap through the tolerant migrator instead of drizzle-kit.
  • Platform events backend EXISTS (previously listed missing) — OrgEventsDO (src/do/org-events-do.ts) is implemented, exported (src/index.ts), and bound as ORG_EVENTS (wrangler.jsonc v2 migration); GET /api/events/ws is live. Real-time / cross-tab updates work with an API key.
  • Repo-image cron + D1 removed (2026-06-12) — the dead repo-image stack and the vestigial D1 binding/migrations were deleted (merged WS3+WS1 cut); a test/node/no-d1.test.ts tripwire fails the build on any D1 reintroduction.
  • /api/usage EXISTS (previously listed missing) — agent_runs roll-up served from src/index.ts; settings/usage + settings/models show real data with an API key.
  • OpenAPI/SDK not schema-first — routes are hand-written Hono; src/openapi.ts (699 lines) is dead/unserved; no Zod→OpenAPI generation (Modernization Phases 1–2).
  • Void abandoned for the control plane (2026-06-12) — entry stays src/index.ts (~720 lines); routes/* use additive-mount via void/handler. Void's inability to re-export custom DOs blocks the entry-switch (WS6); a test/node/void-cp-abandoned.test.ts guard prevents regression. Chat stays on Void.
  • GitHub OAuth real-data flow — unblocked by the pg pool fix but unverified; needs a real sign-in at http://localhost:5174. Until then /account + /repos have no live data locally.
  • Design-pack slices (.claude/skills/: governing-agent-runs, agent-run-contract, provider-adapter-contract, tenant-data-boundary, environment-profiles) are checklists + starter artifacts, not wired into the build.
  • E2E / browser-test harness is in place. packages/chat has Playwright E2E (packages/chat/e2e/, mock mode, npm run test:e2e), Vitest-4 browser mode + visual regression (npm run test:browser), and a generated-spec tool at tools/test-gen/ (npm run test-gen -- smoke|capture|gen ...; npm run test-gen:setup to install). GOTCHA: the app is Void-SSR'd, so a Playwright test must wait for hydration before interacting (packages/chat/e2e/hydration.ts) — a click/type before hydration is a silent no-op (NOT a trusted-event incompatibility).
  • dev:real prod-leak footguns fixed — the SDK requires an explicit url (no hardcoded prod default), chat stores resolve via controlPlaneUrl(), and dev:local --real pins/​warns on VITE_BASE_URL/VITE_API_KEY (see .opencode/rules/browser-testing.md).
  • Hexagonal port/adapter layer built; S1–S5 landed, S6 ~80% (W1b cutover) — 15 ports across 5 planes under src/lib/{sandbox,agent,controller,transport,session,record,scm,secrets,gateway,identity,ingress,blob,observability,metering}/*.ts, each with an in-memory Fake + a fault catalog. Proven (315 tests / 33 files green, 2026-06-07): test/integration/walking-skeleton.test.ts (full run lifecycle in-memory, zero infra), test/integration/disruption.test.ts (break each seam → assert fail-closed), test/ports/conformance-registry.test.ts (rename/drop a fake or add a port without fake+faults = RED BUILD). S1–S5 landed 2026-06-05/07: DoAgentController in the DO with fakes (S1) → SqlEventSink durable (S2, e6dccd8) → DoSqliteSessionState (S3, d2d6877) → DoWebSocketChannel (S4, 746176c) → LocalProvider 13-state FSM (S5, 3823880). S6 (Real CodingAgent) — ~80% done. Three real adapters behind the CodingAgent interface: OpenCodeAgent (880-line HTTP/SSE, src/lib/agent/opencode-agent.ts), AcpAgent (generic ACP to any agent binary, src/lib/agent/acp-agent.ts), and OpenCodeAcpAgent (stdio opencode acp preset, src/lib/agent/opencode-acp-agent.ts). Proven end-to-end: scripts/opencode-acp-loop.ts (real opencode acpLocalProviderDoAgentController → real model token → execution_complete); scripts/opencode-acp-conformance.ts (contract test, opt-in Node — child processes can't run in the Workers vitest pool). LocalProcessProvider + scripts/local-sandbox-runtime.ts now spawn real opencode in isolated sandboxes, dial back over a real WebSocket, authenticate, and reach ready (three spawn/handshake bugs fixed e0e6a7a). Remaining S6: (1) FakeControlPlane.prompt/promptThenDropMidStream test-harness methods for the reconnect-resume e2e in scripts/local-process-sandbox-e2e.ts, (2) LocalProcessProvider conformance runner with real agent. Then: S7 (LiteLLM Gateway), S5-cloud (Modal swap), S8 (the flip — delete inline Modal path). The SANDBOX_RUNTIME=controller|acp flags gate the swap; absent in prod.

Agent memory & continuity

Durable project state lives in THIS file (auto-loaded every session, in both OMP and Claude Code) — treat it as the source of truth, not the memory backend. The OMP memory backend (Hindsight, remote https://hindsight.omoios.dev, per-project-tagged) only auto-injects a small generic slice at session start and does not re-query mid-session, so proactively recall/reflect when starting a work segment or shifting topic — don't trust the auto-injected <mental_models>/<memories> block to hold recent specifics. Memory is split-brain: OMP→Hindsight (sessions in ~/.omp/agent/sessions/), Claude Code→claude-mem (raw transcripts in ~/.claude/projects/-Users-kevinhill-Coding-Projects-agent-coordinator/*.jsonl); the two stores don't see each other, so "what did we do recently" means checking both (recall/reflect and mining the JSONL).

Decisions packet — read FIRST when working on the architecture rewrite

docs/decisions/ is the living source of truth for the hexagonal rewrite (the move off super-coupled/Modal-hardwired toward ports-&-adapters). Read order: docs/decisions/why-cutover.md (why we're doing this + what NOT to do) → docs/decisions/README.mdcurrent-state.md → the relevant ADR — before touching code. The non-negotiable rules (staged in docs/decisions/rules-for-agents.md, graduating here as they harden):

  • A port is a bundle, not an interface: (interface, ≥1 real adapter, 1 fake adapter, 1 contract test, 1 fault catalog). DoD = passing contract + fault tests, never prose.
  • Never weaken a fail-closed/negative test to go green — esp. cross-tenant reads. After a hard pass, re-introduce a known fault and confirm a test goes red.
  • Interface stable, adapter swappable — swap Modal/LiteLLM by writing a new adapter, never by changing the core's shape. Modal is being demoted to one adapter; local-disk adapter first (ADR-0004).
  • Repo docs (L1) are authoritative over any memory store; cloud memory is a mirror, never the only copy. Write durable facts the session they're decided (ADR → decision-log.md, action → action-log.md).
  • Verify platform facts against live docs, not memory (CF DO observability export, Modal lifecycle, gateway routing).

Source decisions are recovered in docs/design/architecture-retrospective/ (the 7-file pack: system frame, M1–M7 mistakes, principles, 14-port registry, conformance model, action sequence). The load-bearing faults are in docs/decisions/fault-catalog.md.

Deeper references (load on demand)

  • Scanning errors / debugging: docs/runbooks/scanning-errors.md (how to find & review errors) + docs/debugging.md (trace_id correlation diagram + event catalog). Custom trace_id correlation, NOT Sentry/OTel, across two log planes joined by trace_id: control-plane Worker+DO via npm run logs (scripts/cf-logs.ts, needs CLOUDFLARE_API_TOKEN+CLOUDFLARE_ACCOUNT_ID) or npx wrangler tail; Modal sandbox/bridge via modal app logs open-inspect. Durable post-mortem: audit_log, agent_runs/agent_run_events (PG). Gotcha: a failed agent run logs at info/warn (execution.complete success:false + prompt.provider_error), NOT error--level error alone misses it; chat-SPA errors hit no server log (browser DevTools only).
  • docs/references/01-…05.md — the "why" behind the DO, spawn flow, bridge protocol, event flow.
  • docs/consumer-saas-example.md — downstream consumer pattern.
  • docs/design/background-agent-platform/ — target design pack; docs/design/agent-architecture-review/ — as-built gap analysis.
  • docs/design/sandboxes-and-snapshots/ — the design behind the hexagonal cutover (decoupled-architecture.md = keystone; testability.md = contract test blueprint; port-catalog.md = 15-port inventory with conformance registry).
  • docs/design/coding-agent-seam/current-reality-and-flake-catalog.mdload-bearing for S6: 10 silent-degradation flakes in the OpenCode SSE bridge, each with validated fix direction.
  • docs/cutover-guide/ — interactive web dashboard showing sprint status, architecture map, data flow, code map, and failure modes. cd docs/cutover-guide && npm run dev.
  • .sisyphus/plans/void-migration-plan.md — ARCHIVED Void migration plan (control plane abandoned; chat-only).
  • packages/*/README.md + packages/*/DEVIATIONS.md — Modal/sandbox specifics + the upstream rename.
  • node_modules/void/docs/ — full Void API.