Skip to content
Merged

Dev #16

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
f166ded
Implement post-1.1 memory integration
May 1, 2026
0bdce79
Fix CI path assumptions for post11 tests
May 1, 2026
cfbd0f6
Fix release build shared module boundaries
May 1, 2026
0752be0
fix codex context accounting and memory UI filters
May 2, 2026
63a663b
ci: isolate windows process cleanup tests
May 2, 2026
41d8038
fix web ctx footer visibility
May 2, 2026
b0bac56
fix memory management filters
May 2, 2026
2b2c15a
fix memory context injection and management filters
May 2, 2026
2979dae
fix memory management and transport compact handling
May 2, 2026
9af76aa
test: expect shared memory project summaries
May 2, 2026
1e028ca
feat(memory): add feature flag management toggles
May 2, 2026
ba3b4e0
fix(memory): align feature toggles and context model resolution
May 2, 2026
0f19976
test(web): stabilize memory feature blocked state
May 2, 2026
b41035a
test(daemon): stabilize p2p barrier ordering
May 2, 2026
9c80328
Fix Codex SDK context model propagation
May 3, 2026
d14be41
Support legacy Codex SDK model fallback
May 3, 2026
4e875a7
Fix P2P discussion content wrapping
May 3, 2026
22a84df
Fix P2P participant cap with stale config
May 3, 2026
2443480
Fix pinned file preview switching
May 3, 2026
cc7a0df
Keep chat action menu visible
May 3, 2026
4f21933
fix codex sdk context window reporting
May 3, 2026
67ac048
test update subsession bar context expectations
May 3, 2026
15c60f0
harden post-1.1 memory management
May 3, 2026
c4876a4
fix memory metadata semantic hashing
May 3, 2026
dfeae59
fix global memory feature flags
May 4, 2026
d475a01
fix mobile websocket reconnect state
May 4, 2026
f78ad2e
fix web terminal and editor paste handling
May 4, 2026
f6d8de4
fix daemon upgrade coordination
May 4, 2026
2ebe1c1
feat status daemon uptime
May 5, 2026
1b4589e
fix status uptime on all platforms
May 5, 2026
180a326
fix ime composing keyboard shortcuts
May 5, 2026
ea2fca0
fix terminal application cursor keys
May 5, 2026
81b3c85
improve code block copy button placement
May 5, 2026
7e49b3b
fix compression hang when agent CLI is missing
May 5, 2026
4bd65ac
record memory compression + per-turn SDK token usage
May 5, 2026
6f42d8a
fix telemetry data corruption + outcome semantics + retry off-by-one
May 5, 2026
7cc3522
fix mobile chat layout left-shift from tab scrollIntoView
May 5, 2026
ab5fd78
fix gemini-sdk per-turn token telemetry via ACP usage_update
May 5, 2026
38c94e4
fix cursor-agent token usage stuck at 0 — camelCase to snake_case
May 5, 2026
4571bd2
fix copilot-sdk per-turn input/cache token + cost capture
May 5, 2026
29fb8ed
fix card chat infinite jitter near bottom — single scroll container
May 5, 2026
bf86ce6
fix card chat infinite jitter — disable pinned banner in preview mode
May 5, 2026
b22d614
fix memory project picker selects rendering super tall
May 5, 2026
a1a0f79
fix file browser changes list silently clipping past ~10 items
May 5, 2026
ea14591
fix: handle compact control commands across SDKs
May 5, 2026
2d29c55
test: update openclaw compact capability expectation
May 5, 2026
75a950a
test: stabilize timeline projection under CI load
May 5, 2026
e2deec8
fix: avoid lingering upgrade cleanup sleepers
May 5, 2026
1d87f6c
fix: prioritize SDK cancel and faster daemon upgrades
May 6, 2026
132d776
fix: show persisted daemon version immediately
May 6, 2026
db7aa79
fix: show stop request feedback in timeline
May 6, 2026
04eebe9
fix: render changes previews reliably
May 6, 2026
771e3c8
style: refine external link dialog
May 6, 2026
2a4515f
fix: preserve file preview scroll on refresh
May 6, 2026
8abb5b8
fix: scope p2p config by server
May 6, 2026
5842cc1
fix: resolve canonical ids for self-hosted gitlab ssh remotes
May 6, 2026
ad5a98e
fix: gate daemon upgrades on npm publication
May 6, 2026
275c12f
feat: add file creation to file browser
May 6, 2026
9f45959
Implement daemon file preview worker
May 7, 2026
2025c35
fix: make preview worker tests macos canonical-safe
May 7, 2026
68947cc
fix(windows-upgrade): escape parens in echo lines inside if-blocks
May 7, 2026
f6e227b
chore(scripts): add Windows restart-daemon.cmd
May 7, 2026
ed7eb23
fix(windows-upgrade): use [...] not ^(...^) in if-block echoes + trac…
May 7, 2026
eff1d0c
fix: increase quick input tab height
May 7, 2026
e7e8c68
fix(scripts): convert restart-daemon.cmd to CRLF + add invariant tests
May 7, 2026
9749357
fix(windows-daemon): self-heal stuck upgrade.lock + CRLF for .cmd in CI
May 7, 2026
15595d6
test(windows-watchdog): real cmd.exe + PowerShell self-heal e2e
May 7, 2026
f06c9f9
fix(windows-daemon): replace cmd.exe upgrade batch with a Node.js runner
May 7, 2026
5603dff
fix(daemon): unexpected web commands no longer crash the daemon
May 7, 2026
b98b0ef
fix: stabilize daemon ci unit tests
May 7, 2026
e0a463f
fix: make quick input list height adaptive
May 7, 2026
eb718a2
feat: self-healing daemon launcher for half-finished upgrades
May 7, 2026
2e1bd32
fix(windows-upgrade): npm spawn fails when npm path contains spaces
May 7, 2026
1dc5c8a
Fix external URL detection priority
May 7, 2026
54856f3
fix(.gitattributes): force LF on .sh / .mjs / bin/* + symlink → re-ex…
May 7, 2026
7d15d2e
fix(repo-types): change file mode to 100644 (was stuck at 120000 syml…
May 7, 2026
380a6da
feat: self-healing preinstall script for `npm install -g imcodes@…`
May 8, 2026
4456d1e
Keep daemon status live on stats updates
May 8, 2026
df70b60
fix web session start timeout race
May 8, 2026
33ecce2
feat(web): chat font picker with bundled JetBrains Mono default
May 8, 2026
4e4de15
fix(web): preserve user-added phrases across refresh and tab close
May 8, 2026
a67eadb
feat(web): font picker on mobile + explicit CJK fallback in preset st…
May 8, 2026
6240e69
fix(web): move chat font dropdown to the left edge of the title bar
May 8, 2026
4783fe0
refactor(web): restructure font dropdown — size row + named select
May 8, 2026
47a0b13
fix(web): add explicit ▾ chevron to font dropdown so it reads as a dr…
May 8, 2026
4386f2b
Add desktop window maximize controls
May 8, 2026
cea8af0
ci: cut coverage job from 10+ min to ~2 min
May 8, 2026
9158848
Fix session fullscreen controls
May 8, 2026
4e49cad
ci(coverage): restore npm run build — packaging + postinstall tests n…
May 8, 2026
1c601ff
Fix sub-session window clamp test
May 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
41 changes: 41 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# ── Force LF for Unix-format files regardless of host's core.autocrlf ──
#
# Why: a developer with `git config core.autocrlf=true` on Windows will
# silently get CRLF on every text file at checkout, including shell
# scripts. Real-world hit on 2026-05-08: running scripts/restart-daemon.cmd
# (which calls `npm install` / `npm run build` / `npm link --force`)
# triggered git to refresh `bin/imcodes-launch.sh`, autocrlf converted it
# to CRLF, `git status` showed it modified, and a careless `git add .`
# would have committed CRLF into a #!/usr/bin/env bash script — bash on
# Linux/macOS treats the trailing `\r` as part of the next token and
# fails on every line ("$'\r': command not found", broken `if` tests,
# malformed variable assignments, etc.).
#
# `text eol=lf` means: treat as text in the index, force LF on checkout
# everywhere — overriding any local core.autocrlf=true setting.
*.sh text eol=lf
*.bash text eol=lf
*.mjs text eol=lf
*.cjs text eol=lf
*.mts text eol=lf
*.cts text eol=lf
# Files that have a Unix shebang interpreter — bin entries in npm
# packages and the husky hook scripts. Their extensions vary or are
# missing, so list them explicitly.
bin/* text eol=lf
.husky/_/husky.sh text eol=lf

# ── Force CRLF on Windows-only batch / VBS files regardless of host ──
#
# Why: cmd.exe parses LF inconsistently (especially inside `if (...)` blocks)
# and a `\r`-less line tail can swallow the next line as part of the previous
# command. Without this rule, git on macOS/Linux checks the file out with LF
# (matching the blob), and any test that reads the bytes directly trips a
# "bare LF — needs CRLF" assertion (see test/util/restart-daemon-cmd.test.ts).
#
# `text eol=crlf` means: treat as text in the index, force CRLF on checkout
# everywhere. The blob can still be LF — git converts on the way out.
*.cmd text eol=crlf
*.bat text eol=crlf
*.vbs text eol=crlf
*.ps1 text eol=crlf
37 changes: 31 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,18 @@ jobs:
- run: npm run build
- run: npm run test:unit

preview-dist-smoke:
name: Preview Dist Smoke
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION_PRIMARY }}
cache: 'npm'
- run: ./scripts/ci-npm-ci.sh .
- run: npm run test:preview-dist

embedding-real-tests:
name: Embedding Integration Tests
runs-on: ubuntu-latest
Expand Down Expand Up @@ -132,7 +144,11 @@ jobs:
- run: bash ./scripts/ci-npm-ci.sh .
- run: npm run build
- name: Run Windows-specific unit tests
run: npx vitest run test/agent/wezterm.test.ts test/daemon/hook-send.test.ts test/daemon/env-injection.test.ts test/cli/send.test.ts test/util/windows-daemon.test.ts test/util/windows-upgrade-script.test.ts test/util/windows-launch-artifacts.test.ts test/util/windows-launch-artifacts.cmd-parse.test.ts test/util/windows-stale-watchdog-cleanup.test.ts test/util/postinstall-sharp-repair.test.ts
run: npx vitest run test/agent/wezterm.test.ts test/daemon/hook-send.test.ts test/daemon/env-injection.test.ts test/cli/send.test.ts test/util/windows-daemon.test.ts test/util/windows-upgrade-script.test.ts test/util/windows-upgrade-runner.test.ts test/util/windows-launch-artifacts.test.ts test/util/windows-launch-artifacts.cmd-parse.test.ts test/util/postinstall-sharp-repair.test.ts test/util/sharp-repair-script.test.ts test/util/restart-daemon-cmd.test.ts
env:
IMCODES_MUX: wezterm
- name: Run Windows process-cleanup regression tests
run: npx vitest run test/util/windows-stale-watchdog-cleanup.test.ts
env:
IMCODES_MUX: wezterm

Expand All @@ -148,7 +164,9 @@ jobs:
- run: bash ./scripts/ci-npm-ci.sh .
- run: npm run build
- name: Run Windows ConPTY / startup regression tests
run: npx vitest run test/agent/conpty.test.ts test/agent/drivers/drivers.test.ts test/util/windows-daemon.test.ts test/util/windows-upgrade-script.test.ts test/util/windows-launch-artifacts.test.ts test/util/windows-launch-artifacts.cmd-parse.test.ts test/util/windows-stale-watchdog-cleanup.test.ts
run: npx vitest run test/agent/conpty.test.ts test/agent/drivers/drivers.test.ts test/util/windows-daemon.test.ts test/util/windows-upgrade-script.test.ts test/util/windows-upgrade-runner.test.ts test/util/windows-launch-artifacts.test.ts test/util/windows-launch-artifacts.cmd-parse.test.ts test/util/sharp-repair-script.test.ts test/util/restart-daemon-cmd.test.ts
- name: Run Windows ConPTY process-cleanup regression tests
run: npx vitest run test/util/windows-stale-watchdog-cleanup.test.ts
# ── Web frontend tests ────────────────────────────────────────────────────

web-tests-unit:
Expand Down Expand Up @@ -291,10 +309,17 @@ jobs:
with:
node-version: ${{ env.NODE_VERSION_PRIMARY }}
cache: 'npm'
- name: Install tmux
run: sudo apt-get install -y tmux
- name: Prime tmux server
run: tmux new-session -d -s init && tmux kill-session -t init
# tmux is no longer needed here: `test:coverage` skips the e2e project
# (which spawns real tmux + agent processes), saving ~1 min per run.
#
# `npm run build` IS still needed: although most tests resolve from
# `src/` via vitest's tsx transform, two suites assert against the
# built output and FAIL without `dist/`:
# - test/packaging.test.ts (verifies bin/main/files paths)
# - test/util/postinstall-sharp-repair.test.ts (executes dist/.../postinstall-sharp-repair.js)
# Both run in the `daemon` project, which is included in coverage. They
# pass in the regular Unit Tests jobs because those run `npm run build`
# first; coverage must do the same.
- run: ./scripts/ci-npm-ci.sh .
- name: Install web deps (needed for tsx component tests)
run: ./scripts/ci-npm-ci.sh web
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ The web project uses `i18next` with `react-i18next` for internationalization.
- Main sessions and sub-sessions are the same session model. Treat them as equally important in behavior, queueing, timeline semantics, edit/undo, and lifecycle handling. Differences should come only from parent/attachment relationship and presentation constraints, not from weaker semantics for sub-sessions.
- Agent types: Process = `'claude-code' | 'codex' | 'gemini' | 'opencode' | 'shell' | 'script'`, Transport = `'openclaw' | 'qwen'` — the `AgentType` union in `src/agent/detect.ts`.
- **Pod-sticky routing (MANDATORY for daemon-dependent requests)**: The server runs multiple replicas. Each daemon connects to ONE pod via WebSocket. The ingress uses `:serverId` in the URL path to route requests to the pod holding that daemon's WS. Any endpoint that depends on the daemon (file transfer, session commands, Watch API) **MUST** include `:serverId` in the URL path (e.g., `/api/server/:serverId/...`). In-memory state (download tokens, WsBridge instances, terminal streams) is per-pod — requests without serverId routing will hit a random pod and fail.
- **MANDATORY — Transport command liveness contract:** Daemon command receipt and urgent-control delivery MUST preserve current dev behavior. The daemon MUST NOT intercept `/compact`; `/compact` is an ordinary SDK-native message and is forwarded unchanged. Ordinary `session.send` ack is a daemon-receipt ack and MUST NOT wait for recall, live context bootstrap, memory lookup/enrichment, embedding, transport lock, pending relaunch, provider send-start, provider settlement, telemetry, or any background memory work. `/stop` and approval/feedback/control responses MUST use the priority path and MUST NOT be routed through or blocked by the ordinary send queue/locks.
- **MANDATORY — Transport command liveness contract:** Daemon command receipt and urgent-control delivery MUST preserve current dev behavior. The daemon MUST NOT intercept `/compact`; `/compact` is an ordinary SDK-native message and is forwarded unchanged to the transport provider. Provider adapters that expose a native compact RPC (for example Codex app-server `thread/compact/start`) MUST translate the raw `/compact` command at the SDK boundary instead of sending it as model text. Ordinary `session.send` ack is a daemon-receipt ack and MUST NOT wait for recall, live context bootstrap, memory lookup/enrichment, embedding, transport lock, pending relaunch, provider send-start, provider settlement, telemetry, or any background memory work. `/stop` and approval/feedback/control responses MUST use the priority path and MUST NOT be routed through or blocked by the ordinary send queue/locks.
- Server secrets (`JWT_SIGNING_KEY`) are set via environment variables, never committed.
- E2E tests require tmux. They are auto-skipped when `SKIP_TMUX_TESTS=1` or inside a Claude Code session (`CLAUDECODE` env var set).
- **MANDATORY — Test session hygiene:** Any e2e/integration test that creates tmux sessions, main sessions, sub-sessions, or temporary projects/cwds **MUST** use naming/path patterns covered by `shared/test-session-guard.ts`. If a new test introduces a new naming family, you **MUST** update `shared/test-session-guard.ts` and its tests in the same change. Leaked test sessions must never persist to `~/.imcodes/sessions.json`, must never be written to the server DB, and must be cleaned from live terminal backends on daemon startup.
Expand Down
181 changes: 181 additions & 0 deletions bin/imcodes-launch.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
#!/usr/bin/env bash
# imcodes-launch — self-healing daemon supervisor.
#
# Entry point for systemd ExecStart / launchctl ProgramArguments. Sits
# in front of the real Node entry and pre-flight-checks the global
# install for half-finished upgrades:
#
# - if `imcodes upgrade` (or any `npm install -g imcodes@…`) gets
# killed mid-write — power loss, OOM-kill, ssh-disconnect, etc —
# npm leaves CRITICAL_DEPS as empty placeholder directories
# (e.g. `node_modules/commander/` exists but has no package.json).
# The next daemon start hits ERR_MODULE_NOT_FOUND on the FIRST
# `import 'commander'`, exits 1, systemd Restart=always thrashes
# forever.
#
# - this launcher detects that signature, re-installs the SAME
# pinned version (read from the surviving package.json — never
# rolls forward without explicit user intent), then execs the
# real daemon. systemd / launchctl never has to know.
#
# Pure bash by design — node_modules being broken is exactly when
# Node-side guard rails go missing too. No tool we use here lives
# under node_modules.
#
# Idempotent: if node_modules is healthy, this is just a thin wrapper
# that exec's the real entry with no overhead beyond a directory stat.
set -u

# Resolve our own real path so we can find the package root regardless
# of how npm symlinked the bin (`$PREFIX/bin/imcodes-launch ->
# ../lib/node_modules/imcodes/bin/imcodes-launch.sh`). `readlink -f`
# is GNU on Linux; on macOS we fall back to `python3 os.path.realpath`,
# and finally `$0` raw if neither is around (works when invoked via
# absolute path, which systemd always does).
resolve_self() {
if command -v readlink >/dev/null 2>&1 && readlink -f "$0" >/dev/null 2>&1; then
readlink -f "$0"
elif command -v python3 >/dev/null 2>&1; then
python3 -c 'import os,sys; print(os.path.realpath(sys.argv[1]))' "$0"
else
echo "$0"
fi
}

SELF_REAL="$(resolve_self)"
PKG_ROOT="$(cd "$(dirname "$SELF_REAL")/.." 2>/dev/null && pwd)"
ENTRY="$PKG_ROOT/dist/src/index.js"
NODE="${IMCODES_NODE_BIN:-$(command -v node 2>/dev/null || echo /usr/bin/node)}"
NPM="${IMCODES_NPM_BIN:-$(command -v npm 2>/dev/null || true)}"
HOME_DIR="${IMCODES_HOME:-$HOME}"
REPAIR_LOG="${IMCODES_LAUNCH_REPAIR_LOG:-$HOME_DIR/.imcodes/launch-repair.log}"

log() {
echo "[imcodes-launch $(date '+%Y-%m-%d %H:%M:%S')] $*" >&2
}

# Critical deps — daemon CANNOT start without these. Each is a top-level
# dependency directly required by `dist/src/index.js` or its synchronous
# transitive imports. If npm install was killed mid-tarball-extract,
# these dirs exist (npm pre-creates them before fetching) but have no
# `package.json` inside.
#
# Keep this list short and stable — over-eager checks cost startup
# latency on every healthy boot.
CRITICAL_DEPS=(commander ws cors body-parser hono "@huggingface/transformers")

# Returns 0 if a dep dir exists but lacks package.json (the half-install
# signature); 1 otherwise. A missing dep dir entirely is also fine —
# npm dedupes some packages to higher levels — only the EMPTY-DIR case
# is the smoking gun.
is_half_installed() {
local dep_dir="$1"
[ -d "$dep_dir" ] && [ ! -f "$dep_dir/package.json" ]
}

# Clear stale `upgrade.lock.d/` left behind by killed `imcodes upgrade`
# runs. The lock isn't a daemon-blocker on Linux/macOS (it gates only
# the next upgrade attempt — see step 0.5 of the bash upgrade script)
# but a stuck lock surprises operators ("why does my upgrade say
# 'another upgrade is in progress'?"). The bash upgrade script has a
# 1800 s stale watchdog that fires when a NEW upgrade is initiated;
# this is the same logic, but at daemon-start time, so the lock gets
# cleared even if no human-or-cron-triggered upgrade ever arrives.
#
# Cheap: a stat per startup. Idempotent: never touches a lock that's
# fresh (a real upgrade really does block daemon restarts during its
# 5–60 s install window).
LOCK_DIR="$HOME_DIR/.imcodes/upgrade.lock.d"
LOCK_STALE_AFTER_SEC="${IMCODES_LAUNCH_LOCK_STALE_AFTER_SEC:-1800}"
if [ -d "$LOCK_DIR" ]; then
started=""
source="none"
if [ -f "$LOCK_DIR/started" ]; then
started="$(cat "$LOCK_DIR/started" 2>/dev/null || true)"
source="started-file"
fi
# Fall back to dir mtime if `started` was never written. `stat -c %Y`
# is GNU; `stat -f %m` is BSD. Try both — empty (= "stat failed") is
# treated as "unknown age", NOT zero, so we don't accidentally
# classify a fresh lock as stale because of a probe error.
if [ -z "$started" ]; then
started="$(stat -c %Y "$LOCK_DIR" 2>/dev/null || stat -f %m "$LOCK_DIR" 2>/dev/null || true)"
source="dir-mtime"
fi
# Reject blanks / non-numerics — anything else means we couldn't
# determine the age and SHOULD NOT decide to delete.
case "$started" in
''|*[!0-9]*) started="" ;;
esac
if [ -n "$started" ]; then
now="$(date +%s)"
age=$(( now - started ))
if [ "$age" -gt "$LOCK_STALE_AFTER_SEC" ]; then
log "clearing stale upgrade.lock.d (age ${age}s, source=${source}, threshold ${LOCK_STALE_AFTER_SEC}s)"
rm -rf "$LOCK_DIR" 2>/dev/null || true
fi
fi
fi

needs_repair=0
missing_summary=""
if [ -d "$PKG_ROOT/node_modules" ]; then
for dep in "${CRITICAL_DEPS[@]}"; do
d="$PKG_ROOT/node_modules/$dep"
if is_half_installed "$d"; then
needs_repair=1
missing_summary="$missing_summary $dep"
fi
done
fi

# `dist/src/index.js` itself missing → the package files were also
# wiped (rare; tarball extraction is per-package atomic in npm v9+,
# but a corrupted disk or an accidentally-rm'd dist/ can produce
# this). Treat as needs_repair so the launcher doesn't just exec a
# missing file and lose the diagnostic.
if [ ! -f "$ENTRY" ]; then
needs_repair=1
missing_summary="$missing_summary dist/src/index.js"
fi

if [ "$needs_repair" = "1" ]; then
if [ -z "$NPM" ]; then
log "node_modules half-installed (missing:$missing_summary) but npm not on PATH — cannot self-repair"
elif [ ! -f "$PKG_ROOT/package.json" ]; then
log "node_modules half-installed but $PKG_ROOT/package.json missing — cannot read pinned version"
else
pinned="$("$NODE" -e "console.log(require('$PKG_ROOT/package.json').version)" 2>/dev/null || true)"
if [ -z "$pinned" ]; then
log "node_modules half-installed but pinned version unreadable — proceeding without repair"
else
log "node_modules half-installed (missing:$missing_summary) — reinstalling imcodes@$pinned"
mkdir -p "$(dirname "$REPAIR_LOG")"
# Clear the leftovers npm leaves when its rename step fails:
# 1. `.imcodes-XXXXX` — npm's atomic-rename tempdir from the
# interrupted install.
# 2. `~/.imcodes/upgrade.lock.d/` — daemon's own coordination
# lock from the killed `imcodes upgrade` flow.
# Both make the next `npm install` fail with ENOTEMPTY/EBUSY.
GLOBAL_LIB="$(dirname "$PKG_ROOT")"
{
echo "==== $(date '+%Y-%m-%d %H:%M:%S') self-repair start (target imcodes@$pinned) ===="
echo "GLOBAL_LIB=$GLOBAL_LIB"
echo "PKG_ROOT=$PKG_ROOT"
echo "missing:$missing_summary"
} >>"$REPAIR_LOG" 2>&1 || true
rm -rf "$GLOBAL_LIB"/.imcodes-* "$HOME_DIR/.imcodes/upgrade.lock.d" >>"$REPAIR_LOG" 2>&1 || true
if "$NPM" install -g --ignore-scripts --prefer-online "imcodes@$pinned" >>"$REPAIR_LOG" 2>&1; then
log "self-repair OK"
else
log "self-repair FAILED — see $REPAIR_LOG"
# Fall through and let exec fail; systemd will keep retrying
# and the next attempt may succeed (e.g. transient network).
fi
fi
fi
fi

# Hand off to the real daemon. `exec` replaces this shell so
# systemd/launchctl tracks the node PID directly — no extra hop.
exec "$NODE" "$ENTRY" "$@"
2 changes: 2 additions & 0 deletions openspec/changes/daemon-file-preview-worker/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-05-07
Loading
Loading