Skip to content

fix(pi): resolve native embedding routing before registration - #462

Merged
ualtinok merged 2 commits into
cortexkit:masterfrom
Qiiks:fix/pi-synapse-routing
Sep 17, 2026
Merged

ualtinok merged 2 commits into
cortexkit:masterfrom
Qiiks:fix/pi-synapse-routing

Conversation

@Qiiks

@Qiiks Qiiks commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Pi's ensureProjectRegisteredFromPiDirectory passed the raw config.embedding block straight to registerProjectEmbedding. The OpenCode plugin resolves the same block through resolveEmbeddingRouting at the identical boundary (packages/plugin/src/plugin/embedding-bootstrap.ts:30-46); Pi did not, so:

  • provider:"synapse" with an unreachable daemon registered a Synapse identity backed by an empty connection file — a fallback vector labelled Synapse.
  • shadow_embedding was ignored entirely.
  • A fallback resolution was memoized for the process lifetime, so a recovered daemon was never picked up.

Change

  • Call the existing shared resolveEmbeddingRouting at the pre-registration boundary and register routing.primary / arm routing.shadow.
  • Log routing warnings (same text as the OpenCode path).
  • Only set the config memoization fingerprint when the resolved lane matches the configured provider, so fallback lanes re-resolve on the next call.
  • Untrusted-config path unchanged (returns before the resolver, parity with OpenCode).
  • Regression: registers the fallback identity when native discovery is unavailable fails pre-fix (registry.provider === "synapse" with an unreachable daemon) and passes post-fix. Note embedding.provider/subc/shadow_embedding are user-tier only (stripUnsafeProjectConfigFields), so the test arms the lane from the user config and pins XDG_CONFIG_HOME, not just HOME.

Verification

cd packages/pi-plugin && bun test src/embedding-bootstrap.test.ts2 pass / 0 fail post-fix; pre-fix the new test fails with Expected: not "synapse".


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Fixes Pi's embedding registration so it resolves the embedding config through resolveEmbeddingRouting before registering, matching how the OpenCode plugin handles the same boundary.

Bug Fixes

  • An unreachable Synapse daemon no longer registers a Synapse identity backed by an empty connection file; the resolved fallback lane is registered instead.
  • shadow_embedding is armed when present and retired (and disposed) when routing no longer resolves a shadow.
  • The config fingerprint is only memoized when the resolved lane matches the configured provider, so fallback lanes re-resolve on the next registration call.
  • Adds regression tests covering fallback identity registration, shadow retirement, and warning deduplication.

Written for commit c972f50. Summary will update on new commits.

Review in cubic

RetriggerConfidence Score: 5/5

The PR appears safe to merge, with no outstanding finding that meets the threshold for a new review comment.

Summary

Fixes Pi embedding bootstrap behavior by resolving configured embedding routes before registration.

  • Registers the resolved primary and optional shadow lanes.
  • Removes a shadow registration when routing no longer selects one.
  • Retries daemon discovery failures while memoizing configuration-only failures.
  • Adds regression coverage for fallback routing, shadow retirement, and warning behavior.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Load trusted Pi configuration] --> B[Resolve embedding routing]
  B --> C[Register resolved primary lane]
  C --> D{Shadow lane resolved?}
  D -->|Yes| E[Register shadow lane]
  D -->|No| F[Unregister prior shadow lane]
  E --> G{Recoverable discovery failure?}
  F --> G
  G -->|Yes| H[Clear fingerprint so next call retries]
  G -->|No| I[Memoize configuration fingerprint]
Loading

Reviews (2) · Last reviewed commit: "fix(pi): retire stale shadow routing and..."

Copilot AI lite review requested due to automatic review settings September 17, 2026 14:38

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread packages/pi-plugin/src/embedding-bootstrap.ts

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 2 files

You’re at about 94% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/pi-plugin/src/embedding-bootstrap.ts
Comment thread packages/pi-plugin/src/embedding-bootstrap.ts Outdated
@magic-alfonso

magic-alfonso Bot commented Sep 17, 2026

Copy link
Copy Markdown

Thanks @Qiiks — the routing-boundary analysis is correct. I reproduced master registering synapse:v1:pending over an empty connection file for an unreachable daemon, and your regression test is non-vacuous (reverting the routing call turns it red). Parity with the OpenCode boundary holds across off, local, openai-compatible, reachable Synapse, unreachable Synapse with local/off fallback, and shadow; OMP inherits the same Pi path.

We are merging this with two small fixes on top, which the two bot reviews also flagged: a registry-level shadow-only unregister called from both harnesses when routing returns no shadow (a pre-existing OpenCode gap that this PR exposes on Pi), and memoizing deterministic no-probe routing outcomes while keeping the retry for an actually attempted unavailable Synapse lane. One consequence worth knowing for anyone who had a raw Pi Synapse registration: the pending identity is no longer reused, so the discovered or fallback identity becomes the active vector space on upgrade. Ships in v0.42.6.

@cubic-dev-ai cubic-dev-ai 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.

2 issues found across 4 files (changes from recent commits).

You’re at about 94% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/plugin/src/features/magic-context/project-embedding-registry.ts">

<violation number="1" location="packages/plugin/src/features/magic-context/project-embedding-registry.ts:2249">
P2: If unregister runs while a shadow batch is in flight, the worker can still write vectors for the retired shadow model after this disposal. Add cancellation or a registration-generation check before committing the batch, and suppress stale worker state updates.</violation>
</file>

<file name="packages/pi-plugin/src/embedding-bootstrap.test.ts">

<violation number="1" location="packages/pi-plugin/src/embedding-bootstrap.test.ts:147">
P3: The `expect(disposed).toBe(true)` assertion depends on a synchronous timing detail: `unregisterProjectShadowEmbedding` → `disposeProvider` calls `void provider.dispose()` without awaiting, and the test provider's async `dispose` body sets `disposed = true` before its first (nonexistent) await. This passes only because the async body runs synchronously. If `disposeProvider` ever starts deferring disposal or the provider discards returns control before setting the flag, the test would silently pass/fail based on timing rather than actual retirement. Since the disposal is fire-and-forget in the production code, assert on the deterministic observable (shadow cohort null, primary intact) and avoid relying on the fire-and-forget side effect timing for the disposal check.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

shadowBackfillStopReasons.delete(key);
shadowBackfillLastWriteOutcomes.delete(key);
}
disposeProvider(shadow?.provider ?? null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: If unregister runs while a shadow batch is in flight, the worker can still write vectors for the retired shadow model after this disposal. Add cancellation or a registration-generation check before committing the batch, and suppress stale worker state updates.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/plugin/src/features/magic-context/project-embedding-registry.ts, line 2249:

<comment>If unregister runs while a shadow batch is in flight, the worker can still write vectors for the retired shadow model after this disposal. Add cancellation or a registration-generation check before committing the batch, and suppress stale worker state updates.</comment>

<file context>
@@ -2232,6 +2232,23 @@ export function registerProjectInObservationMode(
+        shadowBackfillStopReasons.delete(key);
+        shadowBackfillLastWriteOutcomes.delete(key);
+    }
+    disposeProvider(shadow?.provider ?? null);
+}
+
</file context>

await ensureProjectRegisteredFromPiDirectory(directory, db);
expect(getShadowEmbeddingMeasurementCohort(identity)).toBeNull();
expect(getProjectEmbeddingSnapshot(identity)?.provider).toBe("off");
expect(disposed).toBe(true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The expect(disposed).toBe(true) assertion depends on a synchronous timing detail: unregisterProjectShadowEmbeddingdisposeProvider calls void provider.dispose() without awaiting, and the test provider's async dispose body sets disposed = true before its first (nonexistent) await. This passes only because the async body runs synchronously. If disposeProvider ever starts deferring disposal or the provider discards returns control before setting the flag, the test would silently pass/fail based on timing rather than actual retirement. Since the disposal is fire-and-forget in the production code, assert on the deterministic observable (shadow cohort null, primary intact) and avoid relying on the fire-and-forget side effect timing for the disposal check.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/pi-plugin/src/embedding-bootstrap.test.ts, line 147:

<comment>The `expect(disposed).toBe(true)` assertion depends on a synchronous timing detail: `unregisterProjectShadowEmbedding` → `disposeProvider` calls `void provider.dispose()` without awaiting, and the test provider's async `dispose` body sets `disposed = true` before its first (nonexistent) await. This passes only because the async body runs synchronously. If `disposeProvider` ever starts deferring disposal or the provider discards returns control before setting the flag, the test would silently pass/fail based on timing rather than actual retirement. Since the disposal is fire-and-forget in the production code, assert on the deterministic observable (shadow cohort null, primary intact) and avoid relying on the fire-and-forget side effect timing for the disposal check.</comment>

<file context>
@@ -92,4 +100,104 @@ describe("ensureProjectRegisteredFromPiDirectory", () => {
+			await ensureProjectRegisteredFromPiDirectory(directory, db);
+			expect(getShadowEmbeddingMeasurementCohort(identity)).toBeNull();
+			expect(getProjectEmbeddingSnapshot(identity)?.provider).toBe("off");
+			expect(disposed).toBe(true);
+		} finally {
+			_resetProjectEmbeddingRegistryForTests();
</file context>

ualtinok added a commit that referenced this pull request Sep 17, 2026
…ion; shadow-only unregister on both harnesses; no-probe memoization

Co-authored-by: Alfonso <alfonso-magic-context@users.noreply.github.com>
@ualtinok
ualtinok merged commit 3a98c5f into cortexkit:master Sep 17, 2026
7 checks passed
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.

3 participants