Fix nectar dormancy: live-reload projects/credentials so post-boot bindings activate without restart#26
Conversation
…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>
📝 WalkthroughWalkthroughReplaces boot-time-only active-project resolution in the CLI daemon with a ChangesLive Active Projects Hot-Attach
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
test/live-active-projects.test.ts (1)
89-125: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a fail-soft test for transient/malformed
projects.jsonreads, 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 callsonErrorwhenreadActiveProjectsthrows (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 toprojects.json(simulating a torn write) and assertsresolve()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
📒 Files selected for processing (3)
src/cli.tssrc/hive-graph/live-active-projects.tstest/live-active-projects.test.ts
| 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), | ||
| }; |
There was a problem hiding this comment.
🗄️ 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.tsRepository: 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.tsRepository: 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' srcRepository: 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 400Repository: legioncodeinc/nectar
Length of output: 30919
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C3 'export function createProjectContext|function createProjectContext|createProjectContext\(' srcRepository: 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.
| { | ||
| 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); |
There was a problem hiding this comment.
🔒 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.tsRepository: 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.tsRepository: 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.tsRepository: 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.
| 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); | ||
| }); | ||
| } |
There was a problem hiding this comment.
🩺 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.
| 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.
Why
nectar's
hiveantennaedaemon 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
~/.deeplake/credentials.json+projects.jsonon 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-graphAPI still read the boot credentials, so a late login leaves those two subsystems dormant until restart (single-project follow-up).Tests
tsc --noEmitclean; suite 844 pass / 5 pre-existing skips. Newtest/live-active-projects.test.ts(incl. anassembleDaemonintegration test).🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes