osborn 0.9.46 → 0.9.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,10 +2,42 @@
2
2
  # Installs osborn as npm package (not from source) for lightweight per-user machines.
3
3
  # Build: docker build -f Dockerfile.sandbox -t registry.fly.io/osborn-sandbox/agent:latest .
4
4
  # Push: fly auth docker && docker push registry.fly.io/osborn-sandbox/agent:latest
5
+ #
6
+ # ARCHITECTURE — HOME-on-volume (no chroot)
7
+ # ==========================================
8
+ # The persistent Fly volume holds HOME and all user-mutable state, but osborn
9
+ # itself lives in the IMAGE. Image-swap brings new osborn versions; the volume
10
+ # preserves user OAuth, sessions, skills, gh tokens, ssh keys, git config, etc.
11
+ #
12
+ # Key env:
13
+ # - HOME=/workspace/home → all HOME-derived paths persist on volume
14
+ # - OSBORN_CWD=/workspace → osborn's project dir, also on volume
15
+ # - osborn comes from /usr/local/bin/osborn (image's native location)
16
+ #
17
+ # Updates:
18
+ # - Image-swap delivers new osborn binary + new system deps
19
+ # - No bind-mounts, no chroot, no runtime npm install
20
+ # - User state on volume survives image rebuilds
21
+ #
22
+ # WHY NOT CHROOT (history)
23
+ # ------------------------
24
+ # We built and verified a chroot architecture on 2026-05-28 that bind-mounted
25
+ # /usr /lib /lib64 /bin /sbin /opt from image into a chroot at /workspace/root-chroot,
26
+ # with HOME=/root pointing inside the chroot. Real Fly A/B test 2026-05-28 showed:
27
+ # - First-boot time: chroot 102s vs no-chroot 111s (within polling noise)
28
+ # - EBUSY on shutdown: chroot 8 vs no-chroot 8 (identical — not a chroot issue)
29
+ # - Persistence: both pass
30
+ # - LOC in entrypoint: chroot ~270 vs no-chroot ~165 (no-chroot saves ~100 LOC)
31
+ # - Path layout: chroot /workspace/root-chroot/root/... vs /workspace/home/...
32
+ # Subagent research confirmed chroot in our codebase solves only HOME-and-persistence
33
+ # layout, NOT library access (Linux dynamic linker is mount-agnostic per ld.so(8)).
34
+ # Since HOME=/workspace/home achieves the same persistence with ~100 fewer LOC and
35
+ # no bind-mount complexity, the chroot version was retired.
36
+ # Archive: docs/archive/Dockerfile.sandbox.chroot-2026-05-28.md
5
37
 
6
38
  FROM node:22-slim
7
39
 
8
- # Runtime deps for osborn + claude-code
40
+ # Runtime deps (same as chroot variant — comes from image, image-swap upgrades)
9
41
  RUN apt-get update -qq && \
10
42
  apt-get install --no-install-recommends -y \
11
43
  ca-certificates \
@@ -16,131 +48,137 @@ RUN apt-get update -qq && \
16
48
  python-is-python3 && \
17
49
  rm -rf /var/lib/apt/lists/*
18
50
 
19
- # Pin osborn to an explicit version so Docker layer cache invalidates whenever
20
- # the version changes. Earlier `RUN npm install -g osborn@latest` looked correct
21
- # but it never invalidated: `latest` is a tag resolved by npm at install time,
22
- # but Docker's layer cache keys on the literal RUN command string. So every
23
- # rebuild reused the cached layer from when "latest" was a previous version,
24
- # producing images labeled :0.9.21 that actually contained osborn 0.9.19 inside.
25
- #
26
- # Pass --build-arg OSBORN_VERSION=X.Y.Z when building. image-build-check.ts
27
- # does this automatically using the npm-registry-resolved latest version.
28
51
  ARG OSBORN_VERSION=latest
29
52
  RUN npm install -g "osborn@${OSBORN_VERSION}" @anthropic-ai/claude-code
30
53
 
31
- # Persistent workspace + claude config dirs
32
- RUN mkdir -p /workspace /root/.claude
54
+ # Persistent volume mount point
55
+ RUN mkdir -p /workspace
33
56
 
34
- # Marker so orchestration (machines.ts isManifestAware) can detect this image
35
- # supports the manifest-driven update flow. Pre-marker machines fall back to
36
- # the image-swap update path, which brings them onto a marker-aware image;
37
- # from then on, all updates use the manifest flow defined in the entrypoint.
38
- RUN touch /etc/osborn-manifest-aware
57
+ # Markers (preserved for back-compat probes from machines.ts):
58
+ # /etc/osborn-manifest-aware — older marker (still here)
59
+ # /etc/osborn-option-d-aware — new marker; machines.ts can detect this variant
60
+ RUN touch /etc/osborn-manifest-aware /etc/osborn-option-d-aware
39
61
 
40
- ENV OSBORN_CWD=/workspace
41
62
  ENV OSBORN_API_PORT=8741
42
63
  ENV NODE_ENV=production
43
-
44
- # HOME points at the volume so user-space config from any tool that respects
45
- # HOME (gh, git, ssh, aws, etc.) automatically writes to the persistent
46
- # volume instead of the ephemeral container overlay. The existing /root/.claude
47
- # symlink machinery below stays in place it's redundant with HOME=/workspace
48
- # but harmless.
49
- ENV HOME=/workspace
64
+ ENV OSBORN_IMAGE_VERSION=${OSBORN_VERSION}
65
+
66
+ # THE KEY DIFFERENCE FROM CHROOT VARIANT:
67
+ # HOME and OSBORN_CWD are set as IMAGE-LEVEL ENV here. The entrypoint reads
68
+ # them, ensures the dirs exist on the volume, and execs osborn. No bind-mount
69
+ # trickery — HOME just IS on the volume because the path resolves to a volume
70
+ # subdir.
71
+ ENV HOME=/workspace/home
72
+ ENV OSBORN_CWD=/workspace
50
73
 
51
74
  WORKDIR /workspace
52
-
53
75
  EXPOSE 8741
54
76
 
55
- # Entrypoint: credential persistence + onboarding suppression + start
77
+ # Entrypoint: HOME-on-volume seed + onboarding + start
56
78
  COPY <<'ENTRYPOINT' /entrypoint.sh
57
79
  #!/bin/bash
58
80
  set -e
59
81
 
60
- # Persistent log capture for post-disconnect upload to Supabase Storage.
61
- # Fly Machines has NO REST endpoint for fetching machine logs (the previous
62
- # implementation hit /v1/apps/{app}/machines/{id}/logs which returns 404 → that
63
- # 404 error string was getting uploaded as "the log" for every session).
64
- # Volume-backed /workspace/osborn.log survives reboots and is readable via
65
- # the documented /exec endpoint (`tail -n 500 /workspace/osborn.log`).
66
- #
67
- # We use process substitution to tee output to BOTH the log file AND the
68
- # original stdout (so `flyctl logs` keeps working for ad-hoc debugging).
69
- # Requires bash, hence the #!/bin/bash shebang.
70
- #
71
- # Size cap: if log grows past 100 MB, keep only the last 50 MB. Prevents
72
- # disk-fill from long-running retry loops (we saw 17h × ~1 line/min = ~1000
73
- # lines today, but anything connecting to LiveKit produces orders of
74
- # magnitude more output).
82
+ # === Persistent log capture ===
83
+ # Same as chroot variant: tee stdout/stderr to /workspace/osborn.log (volume).
84
+ # Rotates at 100MB keeps last 50MB.
75
85
  LOGFILE=/workspace/osborn.log
76
86
  mkdir -p /workspace
77
87
  if [ -f "$LOGFILE" ] && [ "$(stat -c%s "$LOGFILE" 2>/dev/null || echo 0)" -gt 104857600 ]; then
78
- echo "[sandbox] Rotating /workspace/osborn.log (>100MB, keeping last 50MB)"
88
+ echo "[sandbox-d] Rotating /workspace/osborn.log (>100MB, keeping last 50MB)"
79
89
  tail -c 52428800 "$LOGFILE" > "$LOGFILE.tmp" && mv "$LOGFILE.tmp" "$LOGFILE"
80
90
  fi
81
- echo "[sandbox] === boot at $(date -Iseconds) ===" >> "$LOGFILE"
82
- # Redirect all subsequent stdout+stderr from this script (and the eventual
83
- # `exec osborn`) to both the original fd (Fly stdout collector) AND the
84
- # append-only log file. tee runs as a backgrounded subshell that survives
85
- # the final exec replacement.
91
+ echo "[sandbox-d] === boot at $(date -Iseconds) ===" >> "$LOGFILE"
86
92
  exec > >(tee -a "$LOGFILE") 2>&1
87
93
 
88
- # Claude credential persistence (volume at /workspace)
89
- mkdir -p /workspace/.claude
90
- rm -rf /root/.claude
91
- ln -sf /workspace/.claude /root/.claude
92
-
93
- # Suppress Claude Code interactive onboarding prompts
94
+ # Onboarding-suppression JSON
94
95
  ONBOARDING_JSON='{"numStartups":10,"installMethod":"npm","autoUpdates":false,"hasCompletedOnboarding":true,"hasTrustDialogAccepted":true,"hasTrustDialogHooksAccepted":true,"hasCompletedProjectOnboarding":true,"hasAcknowledgedCostThreshold":true,"effortCalloutV2Dismissed":true,"theme":"dark","projects":{"/workspace":{"hasTrustDialogAccepted":true,"hasTrustDialogHooksAccepted":true,"hasCompletedProjectOnboarding":true}}}'
95
- echo "$ONBOARDING_JSON" > /root/.claude.json
96
- # Additional write at $HOME/.claude.json. With HOME=/workspace this is where
97
- # Claude Code actually reads its top-level config from; the /root/.claude.json
98
- # write above becomes dead but is left in place (harmless).
99
- echo "$ONBOARDING_JSON" > /workspace/.claude.json
100
- mkdir -p /workspace/.claude
101
- echo "$ONBOARDING_JSON" > /workspace/.claude/.config.json
102
- echo "$ONBOARDING_JSON" > /workspace/.claude/claude.json
103
-
104
- # Restore OAuth token if persisted on volume
105
- if [ -f /workspace/.claude/.oauth-token ]; then
106
- export CLAUDE_CODE_OAUTH_TOKEN="$(cat /workspace/.claude/.oauth-token)"
107
- echo "[sandbox] Restored CLAUDE_CODE_OAUTH_TOKEN from volume"
96
+
97
+ # ============================================================
98
+ # === HOME-on-volume seed ===
99
+ # ============================================================
100
+ # HOME=/workspace/home, set via ENV in Dockerfile. Ensure the dir exists on
101
+ # the volume and seed Claude/onboarding/skills on first boot. Idempotent.
102
+ echo "[sandbox-d] HOME=$HOME OSBORN_CWD=$OSBORN_CWD"
103
+ mkdir -p "$HOME" "$HOME/.claude" "$HOME/.osborn"
104
+
105
+ # === Legacy migration ===
106
+ # Existing production sandboxes have credentials at /workspace/.claude/
107
+ # (the pre-D legacy symlink architecture). Migrate to HOME=/workspace/home/.claude/
108
+ # on first boot of the D image. Atomic mv — safe.
109
+ if [ -d /workspace/.claude ] && [ ! -d "$HOME/.claude/projects" ] && [ ! -f "$HOME/.claude/.credentials.json" ]; then
110
+ echo "[sandbox-d] migrating legacy /workspace/.claude → \$HOME/.claude"
111
+ # Move CONTENTS, not the dir itself (target may already exist with seeded skills)
112
+ for item in /workspace/.claude/.* /workspace/.claude/*; do
113
+ [ -e "$item" ] || continue
114
+ BASENAME=$(basename "$item")
115
+ [ "$BASENAME" = "." ] && continue
116
+ [ "$BASENAME" = ".." ] && continue
117
+ [ -e "$HOME/.claude/$BASENAME" ] && continue
118
+ mv "$item" "$HOME/.claude/$BASENAME" 2>/dev/null || true
119
+ done
120
+ rmdir /workspace/.claude 2>/dev/null || true
121
+ fi
122
+ if [ -f /workspace/.claude.json ] && [ ! -f "$HOME/.claude.json" ]; then
123
+ echo "[sandbox-d] migrating legacy /workspace/.claude.json → \$HOME/.claude.json"
124
+ mv /workspace/.claude.json "$HOME/.claude.json"
125
+ fi
126
+
127
+ # Onboarding config (overwrites every boot — intentional, deterministic state)
128
+ echo "$ONBOARDING_JSON" > "$HOME/.claude.json"
129
+ echo "$ONBOARDING_JSON" > "$HOME/.claude/.config.json"
130
+ echo "$ONBOARDING_JSON" > "$HOME/.claude/claude.json"
131
+
132
+ # Restore OAuth token if persisted
133
+ if [ -f "$HOME/.claude/.oauth-token" ]; then
134
+ export CLAUDE_CODE_OAUTH_TOKEN="$(cat "$HOME/.claude/.oauth-token")"
135
+ echo "[sandbox-d] restored CLAUDE_CODE_OAUTH_TOKEN from volume"
108
136
  fi
109
137
 
110
- # Seed default skills into ~/.claude/skills/ single source of truth.
111
- # The agent only loads skills from this path; defaults shipped in the npm
112
- # package get copied here on first boot. Idempotent: existing skills
113
- # (learned via PostCompact or manually edited) are preserved across restarts.
114
- HOME_SKILLS_DIR=/root/.claude/skills
115
- PKG_SKILLS_DIR=/usr/local/lib/node_modules/osborn/.claude/skills
138
+ # === Seed default skills with version-aware refresh (ported from chroot B6 fix) ===
139
+ # Same invariants as chroot variant:
140
+ # (1) USER-ADDED skills preserved across image upgrades
141
+ # (2) IMAGE-DEFAULT skills refreshed when image version changes
142
+ HOME_SKILLS_DIR="$HOME/.claude/skills"
143
+ PKG_SKILLS_DIR="/usr/local/lib/node_modules/osborn/.claude/skills"
144
+ SEED_VERSION_FILE="${HOME_SKILLS_DIR}/.seed-version"
116
145
  mkdir -p "$HOME_SKILLS_DIR"
146
+ CURRENT_SEED_VERSION=$(cat "$SEED_VERSION_FILE" 2>/dev/null | tr -d '[:space:]' || echo "")
147
+ IMAGE_SEED_VERSION="${OSBORN_IMAGE_VERSION:-latest}"
117
148
  if [ -d "$PKG_SKILLS_DIR" ]; then
149
+ REFRESHED=0
150
+ SEEDED=0
118
151
  for d in "$PKG_SKILLS_DIR"/*/; do
119
152
  [ -d "$d" ] || continue
120
153
  NAME=$(basename "$d")
121
- [ -d "$HOME_SKILLS_DIR/$NAME" ] && continue
122
- cp -r "$d" "$HOME_SKILLS_DIR/$NAME"
123
- echo "[sandbox] seeded default skill: $NAME"
154
+ if [ ! -d "$HOME_SKILLS_DIR/$NAME" ]; then
155
+ cp -r "$d" "$HOME_SKILLS_DIR/$NAME"
156
+ echo "[sandbox-d] seeded default skill: $NAME"
157
+ SEEDED=$((SEEDED+1))
158
+ elif [ "$CURRENT_SEED_VERSION" != "$IMAGE_SEED_VERSION" ]; then
159
+ rm -rf "$HOME_SKILLS_DIR/$NAME"
160
+ cp -r "$d" "$HOME_SKILLS_DIR/$NAME"
161
+ echo "[sandbox-d] refreshed default skill: $NAME (image $CURRENT_SEED_VERSION → $IMAGE_SEED_VERSION)"
162
+ REFRESHED=$((REFRESHED+1))
163
+ fi
124
164
  done
165
+ if [ "$CURRENT_SEED_VERSION" != "$IMAGE_SEED_VERSION" ]; then
166
+ echo "$IMAGE_SEED_VERSION" > "$SEED_VERSION_FILE"
167
+ [ "$REFRESHED" -gt 0 ] && echo "[sandbox-d] skills marker: $CURRENT_SEED_VERSION → $IMAGE_SEED_VERSION (refreshed $REFRESHED, seeded $SEEDED)"
168
+ fi
125
169
  fi
126
170
 
127
- # Manifest-driven version check.
128
- # Orchestration writes /workspace/.osborn-want-version on update (machines.ts
129
- # updateViaManifest). On every boot we compare to the currently-installed
130
- # osborn and run `npm install -g osborn@<want>` if they differ. The install
131
- # lands in the container overlay (default npm prefix) — Fly wipes overlay on
132
- # stop/start so the install re-runs on every boot until the base image is
133
- # rebuilt with that version baked in. Update is fast between Fly restarts;
134
- # only the first boot after a restart pays the npm install cost.
135
- WANT=$(cat /workspace/.osborn-want-version 2>/dev/null | tr -d '[:space:]')
136
- if [ -n "$WANT" ]; then
137
- CURRENT=$(osborn --version 2>/dev/null | head -1 | tr -d '[:space:]')
138
- if [ "$WANT" != "$CURRENT" ]; then
139
- echo "[sandbox] osborn ${CURRENT:-none} → ${WANT} (manifest install)"
140
- npm install -g "osborn@${WANT}" || echo "[sandbox] install failed — running ${CURRENT:-image-baked} version"
141
- fi
171
+ # Container-view session inventory (debugging)
172
+ if [ -d "$HOME/.claude/projects" ]; then
173
+ echo "[sandbox-d] Session inventory:"
174
+ for slug_dir in "$HOME/.claude/projects"/*/; do
175
+ [ -d "$slug_dir" ] || continue
176
+ count=$(find "$slug_dir" -maxdepth 1 -name '*.jsonl' 2>/dev/null | wc -l | tr -d ' ')
177
+ echo "[sandbox-d] $(basename "$slug_dir"): ${count} jsonl files"
178
+ done
142
179
  fi
143
180
 
181
+ echo "[sandbox-d] exec'ing osborn (no chroot, HOME=$HOME)"
144
182
  exec osborn
145
183
  ENTRYPOINT
146
184
 
@@ -211,6 +211,19 @@ export class ClaudeLLM extends llm.LLM {
211
211
  mcpServers: this.#mcpServers,
212
212
  voiceMode: opts.voiceMode || 'realtime',
213
213
  skipTTSQueue: opts.skipTTSQueue || false,
214
+ // CRITICAL: the PreCompact / PostCompact hooks call
215
+ // `this.#opts.onCompactionEvent?.(...)` to invoke the bridge to the
216
+ // frontend (chat-bubble + banner). Without including the callback in
217
+ // this whitelisted literal, callers can pass it correctly via opts but
218
+ // it's silently dropped during construction → hooks invoke undefined →
219
+ // no chat bubble appears. This was the real reason the compaction UI
220
+ // never showed up in 0.9.44–0.9.46 despite the wiring at every caller
221
+ // looking right. Confirmed 2026-05-28 by reading the live dist on Fly
222
+ // and seeing PreCompact/PostCompact emoji logs + the SDK iterator
223
+ // marker [COMPACT-SDK-ITER] firing while [COMPACT-AGENT-RX] never did.
224
+ // `onPermissionRequest` is handled separately via its own private field
225
+ // and does NOT need to be in this literal.
226
+ onCompactionEvent: opts.onCompactionEvent,
214
227
  };
215
228
  this.#eventEmitter = opts.eventEmitter || new EventEmitter();
216
229
  console.log('🟠 ClaudeLLM initialized (Research Mode)');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osborn",
3
- "version": "0.9.46",
3
+ "version": "0.9.48",
4
4
  "description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
5
5
  "type": "module",
6
6
  "bin": {