Spaces:
Running
Running
OpenClaw gets its own HOME on local disk (no symlinks — boundary check rejects them; no FUSE — fence rejects that): boot restore + 60s backup
Browse files- entrypoint.sh +22 -17
- server/src/index.js +2 -2
- server/src/runner.js +6 -2
- web/src/components/Sidebar.tsx +7 -3
- web/src/styles.css +3 -3
entrypoint.sh
CHANGED
|
@@ -40,25 +40,30 @@ export PATH="$AM_LOCAL/py/bin:$AM_LOCAL/npm/bin:$AM_LOCAL/bin:$HOME/.local/bin:$
|
|
| 40 |
# embedded prompt lock was released"). Its state therefore lives on LOCAL disk,
|
| 41 |
# with a durable copy on the bucket: restored on boot, synced back every 60s.
|
| 42 |
# Worst case on an unclean stop: the last minute of chat history.
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
#
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
#
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
#
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
[ -
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
( while :; do
|
| 60 |
sleep 60
|
| 61 |
-
rsync -a --delete "$OPENCLAW_STATE_DIR/" "$
|
| 62 |
done ) &
|
| 63 |
|
| 64 |
# Durable, user-editable setup script. Runs on EVERY start (keep it idempotent);
|
|
|
|
| 40 |
# embedded prompt lock was released"). Its state therefore lives on LOCAL disk,
|
| 41 |
# with a durable copy on the bucket: restored on boot, synced back every 60s.
|
| 42 |
# Worst case on an unclean stop: the last minute of chat history.
|
| 43 |
+
# OpenClaw can't run its state on the FUSE bucket (its session fence
|
| 44 |
+
# false-positives on unstable metadata) and it REJECTS symlinked paths (the
|
| 45 |
+
# workspace boundary check). No symlinks, no env overrides — OpenClaw simply
|
| 46 |
+
# gets its OWN HOME on local disk: a real, ordinary install from its point of
|
| 47 |
+
# view. Durable copy on the bucket: restored on boot, synced back every 60s.
|
| 48 |
+
# Worst case on an unclean stop: the last minute of claw state.
|
| 49 |
+
export OPENCLAW_HOME="$AM_LOCAL/oc-home" # runner launches openclaw with HOME=$OPENCLAW_HOME
|
| 50 |
+
export OPENCLAW_STATE_DIR="$OPENCLAW_HOME/.openclaw" # where the server finds its config/traces
|
| 51 |
+
OC_BACKUP="$DATA_DIR/state/openclaw-backup"
|
| 52 |
+
mkdir -p "$OPENCLAW_STATE_DIR" "$OC_BACKUP"
|
| 53 |
+
# heal from the earlier symlink experiment
|
| 54 |
+
[ -L "$HOME/.openclaw" ] && rm "$HOME/.openclaw"
|
| 55 |
+
# seed local state: backup (freshest) first, then legacy dirs fill gaps (--update: never clobber newer)
|
| 56 |
+
[ -n "$(ls -A "$OC_BACKUP" 2>/dev/null)" ] && rsync -a "$OC_BACKUP/" "$OPENCLAW_STATE_DIR/" 2>/dev/null
|
| 57 |
+
for legacy in "$HOME/.openclaw.pre-symlink" "$HOME/.openclaw"; do
|
| 58 |
+
if [ -d "$legacy" ] && [ ! -L "$legacy" ]; then
|
| 59 |
+
rsync -a --update "$legacy/" "$OPENCLAW_STATE_DIR/" 2>/dev/null || true
|
| 60 |
+
fi
|
| 61 |
+
done
|
| 62 |
+
# small comforts in the private HOME (harmless if missing)
|
| 63 |
+
cp "$HOME/.gitconfig" "$OPENCLAW_HOME/.gitconfig" 2>/dev/null || true
|
| 64 |
( while :; do
|
| 65 |
sleep 60
|
| 66 |
+
rsync -a --delete "$OPENCLAW_STATE_DIR/" "$OC_BACKUP/" 2>/dev/null || true
|
| 67 |
done ) &
|
| 68 |
|
| 69 |
# Durable, user-editable setup script. Runs on EVERY start (keep it idempotent);
|
server/src/index.js
CHANGED
|
@@ -180,7 +180,7 @@ const BUILD_ENV_KEYS = (() => {
|
|
| 180 |
const NON_SECRET = new Set([
|
| 181 |
'HOME', 'CLAUDE_CONFIG_DIR', 'CODEX_HOME', 'NPM_CONFIG_PREFIX', 'PWD', 'OLDPWD', 'SHLVL', '_', 'HOSTNAME',
|
| 182 |
'ACCELERATOR', 'COMMIT_SHA', 'CPU_CORES', 'HF_DATASETS_TRUST_REMOTE_CODE', 'IMAGE_SHA', 'MEMORY', 'OMP_NUM_THREADS',
|
| 183 |
-
'UV_CACHE_DIR', 'PIP_CACHE_DIR', 'PYTHONPYCACHEPREFIX', 'PYTHONUSERBASE', 'OPENCLAW_STATE_DIR',
|
| 184 |
]);
|
| 185 |
const NON_SECRET_PREFIX = ['SPACE_', 'KUBERNETES_', 'NVIDIA_', 'CUDA_', 'NV_', 'AM_'];
|
| 186 |
function injectedEnvKeys() {
|
|
@@ -255,7 +255,7 @@ Hermes — alongside plain shells and a file browser.
|
|
| 255 |
- \`/data\` is **durable storage** (a mounted bucket). Files under \`/data/workspaces/…\` and \`/data/home/…\` survive restarts and sleep.
|
| 256 |
- **Empty directories are not persisted** — only files. If a folder must exist, keep a file in it.
|
| 257 |
- Sessions are **tmux-backed**: they keep running when the browser disconnects and can be resumed after the Space sleeps.
|
| 258 |
-
- Exception: OpenClaw
|
| 259 |
|
| 260 |
## You may not be alone
|
| 261 |
- Other agents run in **sibling folders** under \`/data/workspaces/\`, and you may be **grouped** to share a single folder with other agents.
|
|
|
|
| 180 |
const NON_SECRET = new Set([
|
| 181 |
'HOME', 'CLAUDE_CONFIG_DIR', 'CODEX_HOME', 'NPM_CONFIG_PREFIX', 'PWD', 'OLDPWD', 'SHLVL', '_', 'HOSTNAME',
|
| 182 |
'ACCELERATOR', 'COMMIT_SHA', 'CPU_CORES', 'HF_DATASETS_TRUST_REMOTE_CODE', 'IMAGE_SHA', 'MEMORY', 'OMP_NUM_THREADS',
|
| 183 |
+
'UV_CACHE_DIR', 'PIP_CACHE_DIR', 'PYTHONPYCACHEPREFIX', 'PYTHONUSERBASE', 'OPENCLAW_STATE_DIR', 'OPENCLAW_HOME',
|
| 184 |
]);
|
| 185 |
const NON_SECRET_PREFIX = ['SPACE_', 'KUBERNETES_', 'NVIDIA_', 'CUDA_', 'NV_', 'AM_'];
|
| 186 |
function injectedEnvKeys() {
|
|
|
|
| 255 |
- \`/data\` is **durable storage** (a mounted bucket). Files under \`/data/workspaces/…\` and \`/data/home/…\` survive restarts and sleep.
|
| 256 |
- **Empty directories are not persisted** — only files. If a folder must exist, keep a file in it.
|
| 257 |
- Sessions are **tmux-backed**: they keep running when the browser disconnects and can be resumed after the Space sleeps.
|
| 258 |
+
- Exception: OpenClaw runs with its own \`$HOME\` on local disk for filesystem compatibility; that state is backed up to the bucket every minute.
|
| 259 |
|
| 260 |
## You may not be alone
|
| 261 |
- Other agents run in **sibling folders** under \`/data/workspaces/\`, and you may be **grouped** to share a single folder with other agents.
|
server/src/runner.js
CHANGED
|
@@ -212,8 +212,12 @@ function commandFor(session) {
|
|
| 212 |
// config exists, go straight to the TUI. Decided by config-file existence —
|
| 213 |
// same honest pattern as the Claude transcript check.
|
| 214 |
if (cli.id === 'openclaw') {
|
| 215 |
-
|
| 216 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
}
|
| 218 |
|
| 219 |
// Codex: resume this agent's pinned conversation (captured from its rollout
|
|
|
|
| 212 |
// config exists, go straight to the TUI. Decided by config-file existence —
|
| 213 |
// same honest pattern as the Claude transcript check.
|
| 214 |
if (cli.id === 'openclaw') {
|
| 215 |
+
// OpenClaw runs with its own HOME on local disk (see entrypoint.sh): its
|
| 216 |
+
// state can't live on the FUSE bucket and it rejects symlinked paths.
|
| 217 |
+
// Locally (no OPENCLAW_HOME) the real HOME is used unchanged.
|
| 218 |
+
return 'HOME="${OPENCLAW_HOME:-$HOME}"; export HOME; '
|
| 219 |
+
+ 'if [ -s "$HOME/.openclaw/openclaw.json" ]; then exec openclaw chat; '
|
| 220 |
+
+ 'else openclaw onboard && exec openclaw chat; fi';
|
| 221 |
}
|
| 222 |
|
| 223 |
// Codex: resume this agent's pinned conversation (captured from its rollout
|
web/src/components/Sidebar.tsx
CHANGED
|
@@ -229,14 +229,18 @@ export default function Sidebar({
|
|
| 229 |
)}
|
| 230 |
</div>
|
| 231 |
|
| 232 |
-
{/* Overview: pinned above the
|
|
|
|
|
|
|
| 233 |
<div className="ov-fixed">
|
| 234 |
<div
|
| 235 |
-
className={`row ov-row${activeRef === 'overview' ? ' active' : ''}`}
|
| 236 |
onClick={() => onActivate('overview')}
|
|
|
|
| 237 |
>
|
|
|
|
|
|
|
| 238 |
<PulseGlyph className="ov-row-ico" />
|
| 239 |
-
<span className="name">Overview</span>
|
| 240 |
</div>
|
| 241 |
</div>
|
| 242 |
|
|
|
|
| 229 |
)}
|
| 230 |
</div>
|
| 231 |
|
| 232 |
+
{/* Overview: pinned above the tree, styled EXACTLY like a session row
|
| 233 |
+
(dot · mono name · glyph) so it reads as clickable — its dot is the
|
| 234 |
+
aggregate of every agent's state. */}
|
| 235 |
<div className="ov-fixed">
|
| 236 |
<div
|
| 237 |
+
className={`row session ov-row${activeRef === 'overview' ? ' active' : ''}`}
|
| 238 |
onClick={() => onActivate('overview')}
|
| 239 |
+
title="All agents: digests, states, replies"
|
| 240 |
>
|
| 241 |
+
<span className={`status ${agg}`} />
|
| 242 |
+
<span className="name">overview</span>
|
| 243 |
<PulseGlyph className="ov-row-ico" />
|
|
|
|
| 244 |
</div>
|
| 245 |
</div>
|
| 246 |
|
web/src/styles.css
CHANGED
|
@@ -181,10 +181,10 @@ body {
|
|
| 181 |
.stepper button:disabled { opacity: 0.35; cursor: not-allowed; }
|
| 182 |
.stepper-n { min-width: 20px; text-align: center; font-size: 12px; font-variant-numeric: tabular-nums; }
|
| 183 |
|
| 184 |
-
/* overview: pinned sidebar row + cards view */
|
| 185 |
.ov-fixed { padding: 6px 8px 0; }
|
| 186 |
-
.ov-row
|
| 187 |
-
.ov-row-ico { width:
|
| 188 |
/* ---- Overview: one reading column of capsules (design mock v4.12) ---- */
|
| 189 |
.ov-wrap { height: 100%; overflow-y: auto; }
|
| 190 |
.ov-feed { max-width: 680px; margin: 0 auto; display: flex; flex-direction: column; gap: 18px; padding: 2px 2px 40px; }
|
|
|
|
| 181 |
.stepper button:disabled { opacity: 0.35; cursor: not-allowed; }
|
| 182 |
.stepper-n { min-width: 20px; text-align: center; font-size: 12px; font-variant-numeric: tabular-nums; }
|
| 183 |
|
| 184 |
+
/* overview: pinned sidebar row (session-row anatomy) + cards view */
|
| 185 |
.ov-fixed { padding: 6px 8px 0; }
|
| 186 |
+
.ov-row { cursor: pointer; }
|
| 187 |
+
.ov-row-ico { width: 13px; height: 13px; color: var(--accent); flex: none; }
|
| 188 |
/* ---- Overview: one reading column of capsules (design mock v4.12) ---- */
|
| 189 |
.ov-wrap { height: 100%; overflow-y: auto; }
|
| 190 |
.ov-feed { max-width: 680px; margin: 0 auto; display: flex; flex-direction: column; gap: 18px; padding: 2px 2px 40px; }
|