Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,7 @@ Every community PR that lands in main is credited here — that's a project rule
- [@davekopecek](https://github.com/davekopecek) (Dave Kopecek) — committed the design-reference fixture so the design-token guard runs on every machine (#30)
- [@snapsynapse](https://github.com/snapsynapse) (Sam Rogers) — graceful shutdown on SIGINT/SIGTERM with worker-tree cleanup and finished state, plus the 14-test end-to-end CLI regression suite (#4)
- [@mlava](https://github.com/mlava) (Mark Lavercombe) — named setup failures across every diagnostic surface (#37) and `run --baseline`, the no-workers check preflight (#38)
- [@jeffhamons](https://github.com/jeffhamons) (Jeff Hamons) — a private OpenCode session store per worker, ending the `database is locked` deaths that made concurrent cheap-tier workers look like model failures (#79), and fix-swarm staging scoped to owned files instead of sweeping worktree debris (#80)

Contributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md) for the philosophy and what gets a PR merged fast. The short version: small and scoped, rebased on current main, every claim backed by an executed test. Authorship is always preserved — where a maintainer pushes a mechanical fix to your branch, you remain the commit author.

Expand Down
30 changes: 28 additions & 2 deletions engines/opencode-sandboxed.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
# flag (required for headless runs) disables ALL of its interactive approval
# prompts. This wrapper supplies the real containment: full network and reads,
# writes confined to the task dir, a per-run scratch/cache dir, and OpenCode's
# own state dirs.
# own state dirs. The scratch root also carries a private per-run XDG_DATA_HOME,
# which is what keeps concurrent workers off one shared session database (see the
# comment on that export below — it is a correctness fix for parallel runs).
#
# Usage (as a ringer engine bin):
# opencode-sandboxed.sh <taskdir> [--no-sandbox] <opencode args...>
Expand All @@ -28,6 +30,9 @@ if ! OPENCODE_BIN="$(command -v opencode)" || [ -z "$OPENCODE_BIN" ]; then
fi

if [ "$SANDBOX" = "0" ]; then
# Full-access mode keeps OpenCode's real ~/.local/share/opencode, so it also
# keeps the shared-session-DB contention documented below. Fine for a lone
# full-access task; do not fan out several at once.
exec "$OPENCODE_BIN" "$@" < /dev/null
fi

Expand Down Expand Up @@ -59,7 +64,8 @@ cat > "$PROFILE" <<'SBEOF'
(subpath (param "SCRATCH"))
(subpath (param "OC_SHARE"))
(subpath (param "OC_STATE"))
(subpath (param "OC_CONFIG")))
(subpath (param "OC_CONFIG"))
(subpath (param "OC_BASE")))
; /dev is needed for /dev/null, /dev/urandom, etc.; writes there can't create
; persistent files without root, so a few literals are allowed rather than via param.
(allow file-write-data
Expand All @@ -72,6 +78,25 @@ export TMPDIR="$SCRATCH"
export XDG_CACHE_HOME="$SCRATCH/cache"
mkdir -p "$XDG_CACHE_HOME"

# Per-run DATA root — this is a concurrency fix, not just containment. OpenCode
# keeps its session store at $XDG_DATA_HOME/opencode/opencode.db, defaulting to
# ~/.local/share/opencode: one file, shared by every worker, opened for write,
# growing without bound. On 2026-07-27 it stood at 1.78 GB and two of three
# workers launched simultaneously died before their first tool call with
# "Error: Unexpected error / database is locked" — recorded as model failures
# though no model had run. Since every non-Codex model routes through this
# engine, shared-DB contention penalises exactly the cheap tier, and worsens as
# parallelism rises. A private store per worker removes the contention entirely.
# Credentials live beside the DB, so seed the fresh root with auth.json — copy,
# never symlink: the profile denies writes outside SCRATCH, and a symlink would
# resolve straight back to the shared file this exists to avoid.
export XDG_DATA_HOME="$SCRATCH/share"
mkdir -p "$XDG_DATA_HOME/opencode"
if [ -f "$HOME/.local/share/opencode/auth.json" ]; then
cp "$HOME/.local/share/opencode/auth.json" "$XDG_DATA_HOME/opencode/auth.json"
chmod 600 "$XDG_DATA_HOME/opencode/auth.json"
fi

# Run as a child (not exec) so the EXIT trap fires and cleans up the profile +
# scratch dir even on the success path; propagate the child's exit status.
set +e
Expand All @@ -81,6 +106,7 @@ set +e
-D "OC_SHARE=$HOME/.local/share/opencode" \
-D "OC_STATE=$HOME/.local/state/opencode" \
-D "OC_CONFIG=$HOME/.config/opencode" \
-D "OC_BASE=$HOME/.opencode" \
-f "$PROFILE" "$OPENCODE_BIN" "$@" < /dev/null
status=$?
set -e
Expand Down