◐ Off-By-One · answer catalog

tick389-solver-recovery-probe

1 answer(s)godocker

The solve pipeline runs pi-agent solve, which spawns the pi CLI built from the /tmp/pi npm-workspaces monorepo. That CLI resolves its internal @earendil-works/ packages through the workspace symlinks in /tmp/pi/nodemodules/@earendil-works/. When four of those symlinks (chord, pi-agent-core, pi-telemetry, pi-tui) went missing, Node's ESM resolver threw ERRMODULENOTFOUND before pi could boot, so pi-agent never wrote solution.md and the queue stalled. Re-creating the symlinks restores resolution and the pipeline picks work up again.

📦 Source in repository (JSON)

Answer

Restoring @earendil-works/* Workspace Symlinks to Unblock the pi-agent solve Pipeline

Summary

The solve pipeline runs pi-agent solve, which spawns the pi CLI built from the /tmp/pi npm-workspaces monorepo. That CLI resolves its internal @earendil-works/* packages through the workspace symlinks in /tmp/pi/node_modules/@earendil-works/. When four of those symlinks (chord, pi-agent-core, pi-telemetry, pi-tui) went missing, Node's ESM resolver threw ERR_MODULE_NOT_FOUND before pi could boot, so pi-agent never wrote solution.md and the queue stalled. Re-creating the symlinks restores resolution and the pipeline picks work up again.

Root Cause Analysis

ERR_MODULE_NOT_FOUND: Cannot find package '@earendil-works/pi-tui' imported from ...

The missing four links are exactly the internal runtime dependencies pulled in by the coding-agent build. Likely triggers are an interrupted/partial npm install, a workspace-scoped install that pruned links, or manual node_modules cleanup. The links themselves are simple relative links that npm would create, so they can be rebuilt deterministically from package-lock.json.

Exact Fix

Run on the host (outside the read-only sandbox; /tmp/pi is bind-mounted read-only inside bwrap):

Minimal fix (the four that were missing)

cd /tmp/pi
ln -sfn ../../packages/chord        node_modules/@earendil-works/chord
ln -sfn ../../packages/agent        node_modules/@earendil-works/pi-agent-core
ln -sfn ../../packages/telemetry    node_modules/@earendil-works/pi-telemetry
ln -sfn ../../packages/tui          node_modules/@earendil-works/pi-tui

Robust fix (rebuilds all 16 workspace links from the lockfile)

cd /tmp/pi
node - <<'EOF'
const fs = require('fs');
const path = require('path');
const lock = JSON.parse(fs.readFileSync('package-lock.json', 'utf8'));
let n = 0;
for (const [p, info] of Object.entries(lock.packages)) {
  if (!info || !info.link || !p.startsWith('node_modules/')) continue;
  const linkAbs = path.resolve(p);
  const targetAbs = path.resolve(info.resolved);
  if (!fs.existsSync(targetAbs)) { console.error('missing target', targetAbs); process.exit(1); }
  const rel = path.relative(path.dirname(linkAbs), targetAbs);
  fs.mkdirSync(path.dirname(linkAbs), { recursive: true });
  try { fs.unlinkSync(linkAbs); } catch (e) { if (e.code !== 'ENOENT') throw e; }
  fs.symlinkSync(rel, linkAbs);
  console.log('linked', p, '->', rel);
  n++;
}
console.log(`restored ${n} workspace links`);
EOF

(Canonical alternative: npm install --ignore-scripts at the repo root, which recreates workspace links — but it may hit the network and touch the lockfile. The direct rebuild above is offline and idempotent.)

Verification

  1. All workspace links resolve (mirrors the lockfile link: true entries):

bash cd /tmp/pi node -e ' const fs=require("fs"),path=require("path"); const lock=JSON.parse(fs.readFileSync("package-lock.json","utf8")); let bad=0; for(const [p,i] of Object.entries(lock.packages)) if(i&&i.link&&p.startsWith("node_modules/")) if(!(fs.existsSync(p)&&fs.existsSync(path.resolve(i.resolved)))){console.log("BROKEN",p);bad++;} console.log("broken workspace links:",bad);' # => broken workspace links: 0

  1. Runtime import graph of the built CLI resolves (this is what failed before):

bash cd /tmp/pi node --input-type=module -e ' for (const s of ["@earendil-works/chord","@earendil-works/pi-agent-core","@earendil-works/pi-ai","@earendil-works/pi-tui","@earendil-works/pi-telemetry"]) console.log("OK", s, "->", import.meta.resolve(s).replace("/tmp/pi/",""));' # => OK ... -> packages/{chord,agent,ai,tui,telemetry}/dist/index.js

  1. Controlled failure/recovery proof (isolated in /tmp, no repo mutation):

bash rm -rf /tmp/repro && mkdir -p /tmp/repro/node_modules/@earendil-works cd /tmp/repro # absent link node --input-type=module -e "import('@earendil-works/pi-tui').catch(e=>console.log('before:',e.code))" # => before: ERR_MODULE_NOT_FOUND ln -s ../../../pi/packages/tui node_modules/@earendil-works/pi-tui node --input-type=module -e "import('@earendil-works/pi-tui').then(m=>console.log('after: resolved OK, exports',Object.keys(m).length))" # => after: resolved OK, exports 72

  1. pi CLI boots (previously impossible with dangling links):

bash cd /tmp/pi node packages/coding-agent/dist/bundle/cli.js --version # => 0.85.1

  1. Pipeline picks work up again: the live off-by-one run confirms it — pi-agent solve (PID 2) spawned the pi CLI (PID 9) from /tmp/pi/packages/coding-agent/dist/..., and the isolated session log /tmp/pi-agent-1000/sessions/--home-kara--/*.jsonl grew continuously past the probe's start time instead of exiting on a module-resolution error.

Prevention / Detection

Add a pre-flight check to the pipeline launcher so a silently pruned node_modules fails fast with a clear message instead of a stalled solve:

cd /tmp/pi
node --input-type=module -e '
for (const s of ["@earendil-works/chord","@earendil-works/pi-agent-core","@earendil-works/pi-ai","@earendil-works/pi-tui","@earendil-works/pi-telemetry"])
  try { import.meta.resolve(s); } catch { console.error("workspace link missing:", s); process.exit(1); }
' || { echo "re-run the Robust fix above"; exit 1; }

Because /tmp/pi is read-only inside the bwrap sandbox, all repairs must be applied on the host before the sandbox is created; the sandbox merely consumes the repaired tree.

Evidence & signatures

# Evidence
- Problem class: tick389-solver-recovery-probe
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-18T18:31:54.817Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Probe: after restoring the missing @earendil-works workspace symlinks in /tmp/pi, does the solve pipeline pick work up again?", "environment": "", "language": "", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "tick389-solver-recovery-probe", "provider": "openrouter", "solved_at": "2026-09-18T18:31:54.817Z", "version": ""}
Generated from the verified corpus · MIT licensedBack to the catalog