#!/usr/bin/env bash # opencode-basalt-5090 entrypoint. Runs inside the container; takes no arguments. # Basalt on 127.0.0.1:9000 (the model), opencode serve on 127.0.0.1:4096, ttyd on 0.0.0.0:7681, # then the control gateway on $M64_PORT. Port 7681 is [runtime.app]: m64 exposes it on a second # front port, where each browser tab gets opencode's terminal interface on the folder home. # 0.0.0.0 is inside the container: only the gate can reach its internal network. set -eu echo '{"phase":"loading","detail":"loading the Basalt pack"}' >> "$M64_STATUS" # HOME is the folder opencode opens on: it holds only the user's files. opencode's DB, config # and caches go to XDG dirs beside it, the gateway's session dirs to WORK_DIR. All are in the # run's /scratch. export HOME=/scratch/home export XDG_CONFIG_HOME=/scratch/state/config XDG_CACHE_HOME=/scratch/state/cache \ XDG_DATA_HOME=/scratch/state/data XDG_STATE_HOME=/scratch/state/state export WORK_DIR=/scratch/work mkdir -p "$HOME" "$XDG_CONFIG_HOME" "$XDG_CACHE_HOME" "$WORK_DIR/sessions" # Engine settings (the pack carries no flags; server settings are the flags below): hal5090's start_qwen38_flash_next_basalt_384k.sh. # Past the trained 262144 tokens the RoPE is stretched with YaRN by 393216/262144 = 1.5. cat > /scratch/basalt_config.json <<'JSON' { "engine": { "expert-cache": "auto", "prefill": "auto", "spec": 7, "spec-min-p": 0.5, "max-context": 393216, "kv": "int8", "resident-experts": true, "vram-reserve-mib": 700, "rope-scaling": "yarn", "rope-scale": 1.5, "yarn-orig-ctx": 262144 } } JSON # Basalt's HOME is the state dir, so CUDA's kernel cache (~/.nv) stays out of the user's folder. HOME=/scratch/state /opt/basalt/bin/basalt serve --config /scratch/basalt_config.json \ --model /weights/basalt/Qwen3.8-FN-GSQ-RCO-IQ3_S.basalt \ --alias qwen3.8-flash-next-iq3_s-basalt-384k \ --vision dynamic --host 127.0.0.1 --port 9000 & BASALT_PID=$! # The gateway reaps neither child, so a dead one lingers as a zombie (state Z): test the # state, not just whether the pid exists. alive() { [ -r "/proc/$1/stat" ] && [ "$(cut -d')' -f2 "/proc/$1/stat" | cut -d' ' -f2)" != Z ]; } until curl -sf --max-time 2 http://127.0.0.1:9000/health >/dev/null; do if ! alive "$BASALT_PID"; then echo '{"phase":"error","detail":"Basalt exited while loading"}' >> "$M64_STATUS" exit 1 fi sleep 2 done echo '{"phase":"loading","detail":"starting opencode"}' >> "$M64_STATUS" # Packages opencode installs on first start, warmed at build time (no network at run time). cp -rn /opt/opencode-capsule/.config/. "$XDG_CONFIG_HOME/" cp -rn /opt/opencode-capsule/.cache/. "$XDG_CACHE_HOME/" export OPENCODE_CONFIG=/capsule/opencode.json # Run from $HOME: it is the one project. The gateway still gives each of its sessions a # directory under $WORK_DIR/sessions. cd "$HOME" # opencode names a project by its git root commit; without one the folder is just "global". if [ ! -d .git ]; then git init -q -b main git -c user.name=capsule -c user.email=capsule@localhost commit -q --allow-empty -m home fi opencode serve --hostname 127.0.0.1 --port 4096 --pure & OPENCODE_PID=$! until curl -sf --max-time 2 http://127.0.0.1:4096/global/health >/dev/null; do alive "$OPENCODE_PID" || { echo '{"phase":"error","detail":"opencode exited while starting"}' >> "$M64_STATUS"; exit 1; } sleep 1 done # Each browser tab runs one TUI on the shared server: sessions show up in the API too. ttyd --writable --interface 0.0.0.0 --port 7681 -t titleFixed=opencode -t disableLeaveAlert=true \ opencode attach http://127.0.0.1:4096 --dir "$HOME" & TTYD_PID=$! # If Basalt, opencode or ttyd dies, take the container down so m64 reports the error. $$ is the # gateway after the exec below. ( while alive "$BASALT_PID" && alive "$OPENCODE_PID" && alive "$TTYD_PID"; do sleep 5; done echo '{"phase":"error","detail":"Basalt, opencode or ttyd exited"}' >> "$M64_STATUS" kill -TERM $$ ) & export AGENT_PROVIDER=basalt AGENT_MODEL=qwen3.8-flash-next-iq3_s-basalt-384k exec /opt/gateway/bin/python /capsule/agent_gateway.py