Skip to content

Fix nectar dormancy: live-reload projects/credentials so post-boot bindings activate without restart#26

Merged
thenotoriousllama merged 1 commit into
mainfrom
fix/fleet-connectivity-live-reload
Jul 5, 2026
Merged

Fix nectar dormancy: live-reload projects/credentials so post-boot bindings activate without restart#26
thenotoriousllama merged 1 commit into
mainfrom
fix/fleet-connectivity-live-reload

Conversation

@thenotoriousllama

@thenotoriousllama thenotoriousllama commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Why

nectar's hiveantennae daemon snapshotted tenancy at boot; when credentials were absent at boot (login lands later) it never armed the active-projects supervisor, so a project bound afterward stayed dormant (all metrics 0, "no active projects") until a manual restart.

What

  • Wire the active-projects supervisor unconditionally and re-resolve ~/.deeplake/credentials.json + projects.json on the reconcile tick (LiveActiveProjects), so a post-boot bind / late login activates brooding with no restart. Reads stay fail-soft (transient rewrite retries next tick).

Deferred (flagged)

The primary-scoped enricher loop and /api/hive-graph API still read the boot credentials, so a late login leaves those two subsystems dormant until restart (single-project follow-up).

Tests

tsc --noEmit clean; suite 844 pass / 5 pre-existing skips. New test/live-active-projects.test.ts (incl. an assembleDaemon integration test).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Projects can now become active after credentials or project bindings are added, without restarting the daemon.
    • The projects dashboard updates to reflect newly activated projects during runtime.
  • Bug Fixes

    • Improved handling for projects that were previously dormant at startup, allowing them to activate once the required files appear.
    • Better consistency when project bindings do not match the current account context.

…ndings activate without restart

- Wire the active-projects supervisor unconditionally and re-resolve
  ~/.deeplake/credentials.json + projects.json on the reconcile tick (LiveActiveProjects),
  so a project bound (or a login that lands) after the daemon booted activates brooding
  without a manual restart. Previously the daemon snapshotted tenancy at boot and, when
  credentials were absent at boot, never armed the supervisor at all (the "no active
  projects" dormancy). Reads stay fail-soft (a transient rewrite retries next tick).
- Deferred (flagged): the primary-scoped enricher loop and /api/hive-graph API still read
  the boot credentials, so a late login leaves those two subsystems dormant until restart.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Replaces boot-time-only active-project resolution in the CLI daemon with a LiveActiveProjects supervisor that re-resolves credentials and project bindings on reconcile ticks, enabling projects to activate without a restart. Updates daemon assembly and dashboard API wiring accordingly, and adds unit/integration tests validating hot activation, fail-soft, and tenancy-scoping behavior.

Changes

Live Active Projects Hot-Attach

Layer / File(s) Summary
LiveActiveProjects wiring in daemon assembly
src/cli.ts
Replaces boot-only activeProjects construction with a LiveActiveProjects-backed resolve/factory pair that reloads credentials and projects.json on reconcile, updates remediation comments, and passes activeProjects unconditionally into AssembleOptions.
Projects dashboard API using boot control options
src/cli.ts
Removes conditional mounting gate on mountProjectsApi and switches view/setProject/setGlobal handlers to use bootControlOptions instead of controlOptions.
Unit tests for resolve/factory hot activation
test/live-active-projects.test.ts
Adds fixtures and unit tests validating resolver activation after a new binding, fail-soft behavior with missing credentials, and tenancy-mismatch scoping.
Integration test for daemon hot-attach
test/live-active-projects.test.ts
Adds fetchHealth/inertTimer helpers and an end-to-end test triggering reconcileActiveProjects() and verifying the health endpoint reflects activation.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • legioncodeinc/nectar#21: Builds on the same multi-root active-project resolution/reconcile and /health flow that LiveActiveProjects extends.

Poem

A rabbit checks the burrow twice a day,
No restart needed—credentials find their way!
Bindings appear, the watcher springs to life,
Health checks hop along without a hitch or strife.
🐰 Hot-attach, hot-attach, reconcile and cheer! 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: live-reloading projects and credentials so bindings can activate after boot without restarting.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/fleet-connectivity-live-reload

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

@thenotoriousllama thenotoriousllama merged commit 58e808e into main Jul 5, 2026
5 of 6 checks passed
@thenotoriousllama thenotoriousllama deleted the fix/fleet-connectivity-live-reload branch July 5, 2026 13:19

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
test/live-active-projects.test.ts (1)

89-125: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a fail-soft test for transient/malformed projects.json reads, not just missing credentials.

This test thoroughly covers the "credentials absent" dormant/fail-soft path, but the PR description's core resiliency claim is broader: "Reads remain fail-soft, so transient rewrite issues are retried on the next tick." Per the upstream resolve() contract, there's a separate catch path that returns an empty resolution and calls onError when readActiveProjects throws (e.g., on a partial/malformed write mid-rewrite) — that branch isn't exercised anywhere in this file. Consider adding a test that writes invalid JSON to projects.json (simulating a torn write) and asserts resolve() still returns an empty resolution rather than throwing.

✅ Suggested additional test
test("W1-N: a transient malformed projects.json read fails soft (empty resolution, no throw)", () => {
  const cacheDir = tempDir("nectar-live-cache-");
  const stateDir = tempDir("nectar-live-state-");
  const errors: unknown[] = [];
  const live = new LiveActiveProjects({
    loadCredentials: () => CREDS,
    createStore: () => fakeStore,
    buildContext: () => { throw new Error("must not be reached"); },
    controlOptions: { cacheDir, broodingState: { dir: stateDir } },
    onError: (err) => errors.push(err),
  });
  try {
    writeFileSync(join(cacheDir, "projects.json"), "{not valid json", "utf8");
    const result = live.resolve();
    assert.equal(result.active.length, 0, "malformed cache reads empty rather than throwing");
    assert.equal(errors.length, 1, "onError is invoked for the transient parse failure");
  } finally {
    rmSync(cacheDir, { recursive: true, force: true });
    rmSync(stateDir, { recursive: true, force: true });
  }
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/live-active-projects.test.ts` around lines 89 - 125, Add a fail-soft
test for malformed projects.json reads in LiveActiveProjects, not just the
credentials-absent path. Extend the test coverage around resolve() to simulate a
torn or invalid cache write by creating malformed JSON in projects.json, then
assert resolve() returns an empty resolution instead of throwing and that
onError is invoked; use the existing LiveActiveProjects, resolve,
readActiveProjects, and onError hooks to locate the behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/cli.ts`:
- Around line 1318-1346: The active-project path is creating a separate
DeepLakeHiveGraphStore instead of reusing the boot-time durableStore, which can
split seqHighWater tracking for the same tenancy. Update
LiveActiveProjects.createStore and the primary active-project wiring in
src/cli.ts so both the registration bridge and enricher go through one shared
store instance when the credentials match. Keep the store reuse logic keyed on
the current tenancy/credentials and preserve the existing buildContext and
activeProjects flow.
- Around line 1431-1441: The projects dashboard view is still using the static
boot-time tenancy state instead of the live credentials path. Update the
`view()` implementation passed to `mountProjectsApi` in `src/cli.ts` to use
`LiveActiveProjects` (or the same live tenancy resolution path it uses) instead
of `readActiveProjects(bootControlOptions)`, so `buildProjectsView` is rendered
from the current supervised projects set.

In `@test/live-active-projects.test.ts`:
- Around line 154-162: `fetchHealth` needs to fail fast and reject cleanly on
bad responses. Add a request timeout to the `get(...)` call so a hung `/health`
endpoint does not leave the test pending, and handle timeout by rejecting the
Promise and aborting the request. Also guard the `JSON.parse` in the
`res.on("end")` path so malformed or non-JSON bodies from `fetchHealth` reject
the Promise with a clear error instead of throwing uncaught.

---

Nitpick comments:
In `@test/live-active-projects.test.ts`:
- Around line 89-125: Add a fail-soft test for malformed projects.json reads in
LiveActiveProjects, not just the credentials-absent path. Extend the test
coverage around resolve() to simulate a torn or invalid cache write by creating
malformed JSON in projects.json, then assert resolve() returns an empty
resolution instead of throwing and that onError is invoked; use the existing
LiveActiveProjects, resolve, readActiveProjects, and onError hooks to locate the
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cab515f4-4d68-438a-8723-aa1d0d10aa55

📥 Commits

Reviewing files that changed from the base of the PR and between 9fec6d3 and 2f18798.

📒 Files selected for processing (3)
  • src/cli.ts
  • src/hive-graph/live-active-projects.ts
  • test/live-active-projects.test.ts

Comment thread src/cli.ts
Comment on lines +1318 to +1346
const liveActive = new LiveActiveProjects({
loadCredentials: () => {
try {
return loadDeepLakeCredentials();
} catch {
return undefined;
}
},
createStore: (credentials) => new DeepLakeHiveGraphStore({ credentials }),
buildContext: ({ project, credentials, store }): RunningContext =>
createProjectContext({
project,
tenancy: { orgId: credentials.orgId, workspaceId: credentials.workspaceId, projectId: project.projectId },
store,
...(portkey.enabled
? { broodDeps: { portkey: portkey as PortkeyEnabled, embedProvider, embedModelId: embedModel } }
: {}),
log: (line) => process.stderr.write(`${JSON.stringify({ ts: new Date().toISOString(), ...line })}\n`),
}),
controlOptions: liveControlOptions,
onError: (err) =>
process.stderr.write(
`nectar daemon: active-project resolve failed (non-fatal): ${err instanceof Error ? err.message : String(err)}\n`,
),
});
const activeProjects = {
resolve: () => liveActive.resolve(),
factory: (project: ResolvedProject): RunningContext => liveActive.factory(project),
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm seqHighWater/seqChains are per-instance and that the enricher + brood/watch
# contexts for the primary project use different DeepLakeHiveGraphStore instances.
fd -t f 'deeplake-store.ts' src -x sed -n '1,60p;260,340p' {}
echo '--- who constructs DeepLakeHiveGraphStore in cli.ts ---'
rg -nP '\bnew DeepLakeHiveGraphStore\b' src/cli.ts
echo '--- enricher store source (buildLiveDurableWiring) ---'
fd -t f -x rg -nP -C3 'enricher|buildLiveDurableWiring' {} src
echo '--- LiveActiveProjects store caching ---'
rg -nP -C3 'currentStore|createStore|cached' src/hive-graph/live-active-projects.ts

Repository: legioncodeinc/nectar

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- src/hive-graph/deeplake-store.ts: high-water logic ---'
sed -n '1,120p' src/hive-graph/deeplake-store.ts
echo '--- relevant mid section ---'
sed -n '240,340p' src/hive-graph/deeplake-store.ts

echo '--- src/hive-graph/live-active-projects.ts ---'
wc -l src/hive-graph/live-active-projects.ts
sed -n '1,220p' src/hive-graph/live-active-projects.ts

echo '--- src/cli.ts around durableStore/liveActive wiring ---'
sed -n '1280,1425p' src/cli.ts

Repository: legioncodeinc/nectar

Length of output: 26253


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- createProjectContext signature / store usage ---'
ast-grep outline src/project-supervisor.ts --view expanded || true
rg -n -C3 'store:' src/project-supervisor.ts src/registration/store-bridge.ts src/enricher/store-adapter.ts src/hive-graph/store.ts

echo '--- live active project wiring and enricher wiring in cli.ts ---'
rg -n -C4 'createStore: \(credentials\)|enricherStore: live\.enricher|buildLiveDurableWiring|factory: \(project' src/cli.ts

echo '--- search for seqHighWater docs across repo ---'
rg -n -C2 'seqHighWater|seqChains|appendVersionAtNextSeq|same store instance|one store instance|ctx\.store into both' src

Repository: legioncodeinc/nectar

Length of output: 1647


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- locate createProjectContext ---'
rg -n -C4 'createProjectContext' src

echo '--- locate any store usage in project context / supervisor path ---'
rg -n -C4 '\bstore\b' src/project-supervisor.ts src/*project* src/registration src/brooding src/enricher | head -n 400

Repository: legioncodeinc/nectar

Length of output: 30919


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C3 'export function createProjectContext|function createProjectContext|createProjectContext\(' src

Repository: legioncodeinc/nectar

Length of output: 1183


Reuse the boot-time Deep Lake store for the primary active project LiveActiveProjects.createStore always builds a new DeepLakeHiveGraphStore, while the primary enricher still uses the boot-time durableStore (src/cli.ts:1326-1331, src/cli.ts:1361-1414). That splits seqHighWater across two async store instances for the same tenancy, so the registration bridge and enricher can still mint the same (nectar, seq) under Deep Lake read-after-write lag. Reuse the boot store when creds match, or route both paths through one shared store instance.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli.ts` around lines 1318 - 1346, The active-project path is creating a
separate DeepLakeHiveGraphStore instead of reusing the boot-time durableStore,
which can split seqHighWater tracking for the same tenancy. Update
LiveActiveProjects.createStore and the primary active-project wiring in
src/cli.ts so both the registration bridge and enricher go through one shared
store instance when the credentials match. Keep the store reuse logic keyed on
the current tenancy/credentials and preserve the existing buildContext and
activeProjects flow.

Comment thread src/cli.ts
Comment on lines +1431 to +1441
{
mountProjectsApi(daemon, {
view: () => {
const { resolution, cache } = readActiveProjects(controlOptions);
const { resolution, cache } = readActiveProjects(bootControlOptions);
return buildProjectsView(resolution, cache, (id) => daemonWatcherState(daemon, id));
},
setProject: (projectId, brooding) => {
persistProjectBrooding(projectId, brooding, controlOptions);
persistProjectBrooding(projectId, brooding, bootControlOptions);
},
setGlobal: (global) => {
persistGlobalBrooding(global, controlOptions);
persistGlobalBrooding(global, bootControlOptions);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm expect drives tenancy scoping in projects.json cache resolution,
# and that an empty options ({}) skips the guard.
rg -nP -C4 '\bexpect\b' src/projects-control.ts
fd -t f 'projects-cache*' src -x rg -nP -C4 'expect|tenancy|mismatch' {}

Repository: legioncodeinc/nectar

Length of output: 1060


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant areas in src/cli.ts and the projects-control implementation.
sed -n '1330,1460p' src/cli.ts
printf '\n--- projects-control ---\n'
sed -n '1,220p' src/projects-control.ts

Repository: legioncodeinc/nectar

Length of output: 11643


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the cache loading and active-project resolution semantics.
ast-grep outline src/hive-graph/project-scope.ts --view expanded
printf '\n---\n'
ast-grep outline src/hive-graph/active-projects.ts --view expanded
printf '\n--- loadProjectsCache / resolveActiveProjects ---\n'
rg -n -C3 'function loadProjectsCache|function resolveActiveProjects|expect|tenancy|binding|workspace|org' src/hive-graph/project-scope.ts src/hive-graph/active-projects.ts

Repository: legioncodeinc/nectar

Length of output: 19889


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the live tenancy source and whether the projects API can reuse it.
rg -n -C3 'liveActive|liveControlOptions|readActiveProjects\(|reconcileActiveProjects|credentialsWatch' src/cli.ts

Repository: legioncodeinc/nectar

Length of output: 3476


Dashboard /projects should read with the live tenancy guard
view() still calls readActiveProjects(bootControlOptions), so a daemon that started before login can render an unscoped or stale projects.json even though LiveActiveProjects re-resolves tenancy from current credentials. Reuse the live credentials path here so the dashboard matches the supervised set.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli.ts` around lines 1431 - 1441, The projects dashboard view is still
using the static boot-time tenancy state instead of the live credentials path.
Update the `view()` implementation passed to `mountProjectsApi` in `src/cli.ts`
to use `LiveActiveProjects` (or the same live tenancy resolution path it uses)
instead of `readActiveProjects(bootControlOptions)`, so `buildProjectsView` is
rendered from the current supervised projects set.

Comment on lines +154 to +162
function fetchHealth(port: number): Promise<{ status: number; body: any }> {
return new Promise((resolve, reject) => {
get({ host: "127.0.0.1", port, path: "/health" }, (res) => {
const chunks: Buffer[] = [];
res.on("data", (c: Buffer) => chunks.push(c));
res.on("end", () => resolve({ status: res.statusCode ?? 0, body: JSON.parse(Buffer.concat(chunks).toString("utf8")) }));
}).on("error", reject);
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Harden fetchHealth against hangs and malformed responses.

No request timeout is set, so a hung/unresponsive server would leave the test pending indefinitely rather than failing fast. Also, JSON.parse on the response body isn't guarded — a non-JSON error body would throw outside the Promise executor's synchronous scope, producing a confusing uncaught exception instead of a clean rejection.

🛡️ Proposed fix
 function fetchHealth(port: number): Promise<{ status: number; body: any }> {
   return new Promise((resolve, reject) => {
-    get({ host: "127.0.0.1", port, path: "/health" }, (res) => {
+    const req = get({ host: "127.0.0.1", port, path: "/health", timeout: 2000 }, (res) => {
       const chunks: Buffer[] = [];
       res.on("data", (c: Buffer) => chunks.push(c));
-      res.on("end", () => resolve({ status: res.statusCode ?? 0, body: JSON.parse(Buffer.concat(chunks).toString("utf8")) }));
+      res.on("end", () => {
+        try {
+          resolve({ status: res.statusCode ?? 0, body: JSON.parse(Buffer.concat(chunks).toString("utf8")) });
+        } catch (err) {
+          reject(err);
+        }
+      });
     }).on("error", reject);
+    req.on("timeout", () => req.destroy(new Error("fetchHealth timed out")));
   });
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function fetchHealth(port: number): Promise<{ status: number; body: any }> {
return new Promise((resolve, reject) => {
get({ host: "127.0.0.1", port, path: "/health" }, (res) => {
const chunks: Buffer[] = [];
res.on("data", (c: Buffer) => chunks.push(c));
res.on("end", () => resolve({ status: res.statusCode ?? 0, body: JSON.parse(Buffer.concat(chunks).toString("utf8")) }));
}).on("error", reject);
});
}
function fetchHealth(port: number): Promise<{ status: number; body: any }> {
return new Promise((resolve, reject) => {
const req = get({ host: "127.0.0.1", port, path: "/health", timeout: 2000 }, (res) => {
const chunks: Buffer[] = [];
res.on("data", (c: Buffer) => chunks.push(c));
res.on("end", () => {
try {
resolve({ status: res.statusCode ?? 0, body: JSON.parse(Buffer.concat(chunks).toString("utf8")) });
} catch (err) {
reject(err);
}
});
}).on("error", reject);
req.on("timeout", () => req.destroy(new Error("fetchHealth timed out")));
});
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/live-active-projects.test.ts` around lines 154 - 162, `fetchHealth`
needs to fail fast and reject cleanly on bad responses. Add a request timeout to
the `get(...)` call so a hung `/health` endpoint does not leave the test
pending, and handle timeout by rejecting the Promise and aborting the request.
Also guard the `JSON.parse` in the `res.on("end")` path so malformed or non-JSON
bodies from `fetchHealth` reject the Promise with a clear error instead of
throwing uncaught.

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