Skip to content

fix(agent-sync): surface workspace resolution failures, stop tmp path poisoning - #1056

Merged
namastex888 merged 1 commit into
devfrom
fix/agent-sync-silent-workspace-failure
Apr 5, 2026
Merged

namastex888 merged 1 commit into
devfrom
fix/agent-sync-silent-workspace-failure

Conversation

@namastex888

Copy link
Copy Markdown
Contributor

The bug I hit

I have a real agent on disk at workspace/agents/omni/AGENTS.md. Every other agent in that directory shows up in genie ls. Omni doesn't. genie spawn omni says:

Error: Agent "omni" not found in directory or built-ins.
  Register with: genie dir add omni --dir <path>

Running genie dir sync manually instantly registered 15 agents that had been invisible — omni plus totvs, helena, researchers, 5 sub-agents, etc. So the sync code works. Something was preventing it from running on serve startup.

Root cause

startAgentSync() in serve.ts silently returned null when findWorkspace() returned null. That disabled the entire auto-discovery subsystem — both the startup sync and the file watcher — for the whole serve lifetime. No warning, no log, no entry in genie serve status.

Why was findWorkspace() returning null? Because ~/.genie/config.json had this persisted from an old test run:

"workspaceRoot": "/tmp/genie-workspace-test-1775349146465-185rx3i66o8"

saveWorkspaceRoot() had written it, /tmp got cleaned, and the fallback path in findWorkspace() has been returning a dead path ever since. Any genie serve booted from outside the real workspace walked up, failed, fell back to the stale path, existsSync returned false, returned null, startAgentSync silently exited. For however long that serve instance ran.

The fix

Three small, contained changes — all behind existing function boundaries:

src/lib/workspace.ts

  • saveWorkspaceRoot() refuses to persist paths under os.tmpdir(). Test workspaces can no longer poison the global fallback. Future test runs write their tmp path and the guard drops it on the floor.
  • loadWorkspaceRoot() self-heals — if the saved path no longer has a .genie/workspace.json marker, scrub it from config instead of returning a dead fallback forever.
  • GENIE_HOME is now resolved lazily via genieHome() instead of a module-level const. Tests can override it per-test via process.env.GENIE_HOME. Without this change, the previous module-level capture meant test env overrides were silently ignored — I caught it because my first test run ended up scrubbing my real ~/.genie/config.json (which, happy accident, was exactly the stale tmp path that originally caused this bug).

src/term-commands/serve.ts

  • startAgentSync() logs loudly when workspace is unresolved, with actionable fix guidance, instead of silently returning null:
    Agent sync: DISABLED — no workspace found from cwd or ~/.genie/config.json
      Fix: `cd <workspace> && genie serve restart`, or run `genie init` to bootstrap one
    
  • Surfaces per-agent errors from syncResult.errors[] so a single agent with bad frontmatter doesn't vanish silently. Today those errors are collected by syncAgentDirectory() and never printed on the serve-startup path.
  • Includes the resolved workspace root in the success log so "which workspace did serve attach to" is visible at a glance, and flags watcher install failures with a warning instead of swallowing them.

src/lib/workspace.test.ts

  • Two new tests covering the tmp-path guard and stale-path self-heal.
  • All tests now isolate GENIE_HOME per-test so they don't touch (or get contaminated by) the developer's real ~/.genie/config.json.

Scope I deliberately did NOT include

While diagnosing this with the user, we identified 6 DX improvements. This PR implements only the 3 that directly address the silent-failure root cause. The other 3 — which are good ideas but larger surface-area changes — are left for follow-ups:

  1. genie spawn <name> should fall back to scanning disk when PG returns null (friendlier error with "found but not registered" message).
  2. genie serve status should expose agent-sync/watcher health as a first-class line.
  3. genie ls should have an "UNREGISTERED (on disk, not in directory)" section for agents that exist but failed validation.

These are each worth a separate focused PR.

Verification

  • bun test src/lib/workspace.test.ts → 12 pass, 0 fail (10 existing + 2 new)
  • bun test src/lib/ → 1571 pass, 0 fail
  • bunx tsc --noEmit → clean
  • bunx biome check src/lib/workspace.ts src/lib/workspace.test.ts src/term-commands/serve.ts → 0 errors, 1 pre-existing warning on startForeground complexity (unrelated, confirmed present on base dev before my changes via git stash round-trip)
  • Pre-push hook ran the full 2073-test suite: 0 fail
  • Manually verified the omni case end-to-end: genie dir sync from the real workspace root registered 15 previously-invisible agents including omni; genie spawn omni now resolves

Test plan

  • bun test src/lib/workspace.test.ts passes
  • Full bun test passes (2073/2073)
  • bunx tsc --noEmit clean
  • bunx biome check 0 errors on changed files
  • Manual: start genie serve from outside a workspace → see Agent sync: DISABLED warning with fix guidance
  • Manual: start genie serve from inside a workspace → see Agent sync: up to date (workspace: …) line
  • Manual: add an invalid frontmatter AGENTS.md, run genie dir sync or restart serve → see the per-agent error line instead of the file silently going missing

… poisoning

Root cause of "my agent exists on disk but genie spawn can't find it":

startAgentSync() in serve.ts silently returned null when findWorkspace()
returned null, disabling the entire auto-discovery subsystem (initial sync +
file watcher) for the whole serve lifetime. Users saw new agents in
workspace/agents/ never register and received no diagnostic anywhere.

The reason findWorkspace() returned null in practice: test runs persist their
tmpdir workspace paths to ~/.genie/config.json via saveWorkspaceRoot(). Once
a serve boots from outside the real workspace, walk-up fails, the fallback
hits the stale tmp path (now removed), existsSync returns false, returns null.

Fixes:

- workspace.ts: saveWorkspaceRoot() refuses to persist paths under os.tmpdir(),
  so test runs can't poison the global config.
- workspace.ts: loadWorkspaceRoot() self-heals — if the saved path no longer
  has a .genie/workspace.json marker, scrub it from config instead of
  returning a stale fallback forever.
- workspace.ts: GENIE_HOME is now resolved lazily via genieHome() so tests
  can override it via process.env.GENIE_HOME per-test.
- serve.ts: startAgentSync() logs loudly when workspace is unresolved, when
  sync has per-agent errors, and when the watcher fails to start. Also
  includes the resolved workspace in the success log so "which workspace
  did serve attach to" is visible at a glance.
- workspace.test.ts: two new tests covering tmp-path guard and stale-path
  self-heal; tests now isolate GENIE_HOME so they don't touch the developer's
  real ~/.genie/config.json.
@coderabbitai

coderabbitai Bot commented Apr 5, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 88645d71-e2f8-409d-af57-d934896e05ca

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/agent-sync-silent-workspace-failure

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request enhances workspace configuration handling by introducing a self-healing mechanism for stale workspace roots and preventing the persistence of temporary paths to the global configuration. Additionally, it improves the observability of the genie serve command through more detailed logging and warnings during agent synchronization. Review feedback recommends using realpathSync to correctly identify temporary directories on systems with symlinked temp paths and suggests dynamically resolving the configuration file path in user-facing warnings for better accuracy when GENIE_HOME is customized.

Comment thread src/lib/workspace.ts
Comment on lines +71 to +75
function isTempPath(root: string): boolean {
const normalizedTmp = resolve(tmpdir());
const normalizedRoot = resolve(root);
return normalizedRoot === normalizedTmp || normalizedRoot.startsWith(normalizedTmp + sep);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

On macOS and some other Unix systems, the system temporary directory (e.g., /tmp or /var/folders/...) is often a symbolic link. Since resolve() does not resolve symlinks, isTempPath might return false even for paths within the temporary directory if they are accessed via their real (canonical) path. This would bypass the protection against 'poisoning' the global config. Using realpathSync ensures a robust comparison. Note: Since the top-level imports cannot be modified in this diff, require is used here for a self-contained suggestion.

function isTempPath(root: string): boolean {
  try {
    const { realpathSync } = require('node:fs');
    const normalizedTmp = realpathSync(tmpdir());
    const normalizedRoot = realpathSync(root);
    return normalizedRoot === normalizedTmp || normalizedRoot.startsWith(normalizedTmp + sep);
  } catch {
    return false;
  }
}
References
  1. Use dynamic imports (require() or import()) to break circular dependency cycles that would otherwise occur at module load time.

if (!ws) {
// Loud failure — silent return used to hide the whole discovery subsystem
// when serve booted from outside a workspace (or with a stale saved root).
console.warn(' Agent sync: DISABLED — no workspace found from cwd or ~/.genie/config.json');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The warning message hardcodes ~/.genie/config.json, which may be inaccurate if the GENIE_HOME environment variable is set. It's better to dynamically resolve the config path to provide accurate guidance to the user.

Suggested change
console.warn(' Agent sync: DISABLED — no workspace found from cwd or ~/.genie/config.json');
console.warn(" Agent sync: DISABLED — no workspace found from cwd or " + join(genieHome(), 'config.json'));

@namastex888
namastex888 merged commit 79b1420 into dev Apr 5, 2026
6 checks passed
namastex888 pushed a commit that referenced this pull request Apr 5, 2026
…path in serve warning

Follow-up to #1056 addressing two review comments from gemini-code-assist.

1. isTempPath() now uses realpathSync to canonicalize both tmpdir() and the
   candidate root before prefix-comparing. On macOS /tmp is a symlink to
   /private/tmp and tmpdir() returns /var/folders/..., so the previous
   path.resolve()-only check would miss any literal /tmp/... path and bypass
   the guard against poisoning the global workspaceRoot config. Fails CLOSED
   on canonicalization errors (treats as temp → refuses to persist) since the
   whole purpose of this guard is to be conservative.

2. startAgentSync() now builds its "no workspace found" warning using
   genieHome() (newly exported) instead of hardcoding ~/.genie/config.json,
   so users who set GENIE_HOME see the actual path in the fix guidance.

Also adds:
- A regression test using a real symlink within the test's tmpdir to verify
  canonicalization works end-to-end (skips gracefully on CI environments
  that disallow symlinks).
- A sanity test that genieHome() honors the GENIE_HOME env override.
@namastex888
namastex888 deleted the fix/agent-sync-silent-workspace-failure branch April 10, 2026 16:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant