Skip to content

Staging#543

Merged
equationalapplications merged 14 commits into
mainfrom
staging
Jul 7, 2026
Merged

Staging#543
equationalapplications merged 14 commits into
mainfrom
staging

Conversation

@equationalapplications

Copy link
Copy Markdown
Owner

Description

Adds the Clanker side of the desktop vault bridge: persistent /agent/desktop
WebSocket with pairing-token auth, desktopPairings + desktopTasks Firestore
routing, and five vault_* ADK tools (vault_wiki_search, vault_get_ontology,
vault_traverse_graph, vault_semantic_search, vault_related_chunks) for
/agent/run and /agent/live. Wire protocol matches Curated Thoughts v1.9.0.

Specs:

Type of Change

  • New feature (non-breaking change which adds functionality)
  • Documentation update
  • Test update

Related Issue

Fixes #

Changes Made

  • POST /agent/desktop/pair and /agent/desktop/revoke (hash-only token storage)
  • wsDesktopAgentHandler: auth, pending-task listener, device-doc revoke/pause
    listener, 40s lastSeenAt refresh, generation-guarded desktopBridge
  • firestoreSession: getActiveDesktopDevice, desktop task queue, browser
    getActiveDevice excludes type: 'desktop'
  • vaultTools.ts: five ADK tools, shared dispatcher, 5-call cap with
    timeout/disconnect decay, same-instance shortcut, no credit spend
  • Wired into buildAgent (text) and buildLiveTools (voice)
  • firestore.rules: deny client access to users/{uid}/desktopTasks
  • Manual smoke script: cloud-agent/scripts/desktopBridgeSmoke.ts

Testing

Platforms Tested

  • iOS
  • Android
  • Web

Test Steps

  1. cd cloud-agent && npm run typecheck
  2. npm run build && NODE_ENV=test node --test --test-reporter spec dist/cloud-agent/src/handlers/wsDesktopAgentHandler.test.js dist/cloud-agent/src/tools/vaultTools.test.js dist/cloud-agent/src/services/firestoreSession.test.js dist/cloud-agent/src/services/desktopBridge.test.js dist/cloud-agent/src/services/desktopPairing.test.js dist/cloud-agent/src/tools/vaultToolsWiring.test.js — 53/53 pass
  3. Integration smoke (manual): docker compose -f docker-compose.local.yml up -d, pair via /agent/desktop/pair, run PAIRING_TOKEN=… npx tsx cloud-agent/scripts/desktopBridgeSmoke.ts, then /agent/run with a vault search prompt
  4. Cross-repo smoke: pair a real Curated Thoughts v1.9.0 build and run vault_wiki_search end to end

Screenshots

Before

N/A — new subsystem.

After

N/A — server-side only; mobile Settings UI for pair/revoke is a follow-up.

Checklist

  • My code follows the code style of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published

Additional Notes

  • Enable Firestore TTL on deploy: gcloud firestore fields ttls update expiresAt --collection-group=desktopTasks --enable-ttl
  • CT-side CloudBridgeClient is already shipped (v1.9.0); this PR is the matching Clanker contract.
  • Mobile Settings → Devices pair/revoke UI is out of scope here (routes only).

Made with Cursor

equationalapplications and others added 5 commits July 6, 2026 22:22
Remove `.cjs` from Metro's assetExts configuration. It's already in Expo's
default sourceExts, so registering it as an asset extension caused Metro to
bundle CommonJS entry points (e.g., superstruct's dist/index.cjs) as static
assets instead of code. Their exports resolved to undefined, crashing the app
at module load with "undefined is not a function" → trailing "Cannot read
property 'ErrorBoundary' of undefined" in expo-router.

This affected OTA update 30.21.1 and any subsequent OTA until fixed. Never
add `.cjs` to assetExts.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Create new zero-dependency module `src/auth/devSandboxFlag.ts` exporting
`isDevSandboxEnabled()` to break synchronous circular import:
  bootstrapSession → ensureDevSandboxCharacter → characterDatabase →
  index → wikiService → wikiLlmProvider → apiClient → bootstrapSession

Two fixes:
1. Extract pure isDevSandboxEnabled() logic to devSandboxFlag.ts
2. Make characterMachine and bootstrapSession imports of ensureDevSandboxCharacter
   lazy via `await import()` inside conditionals (only load when actually needed)

Import cycles still exist in the static graph, but lazy imports break the
synchronous TDZ (Temporal Dead Zone) trap at module initialization that
caused the crash.

Update test mocks to import isDevSandboxEnabled from devSandboxFlag.
Regenerate docs/flowcharts to reflect new module structure.

Add --clear-cache to `npm run ota` script for deterministic bundling.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
fix: resolve OTA 30.21.1 crash - metro .cjs assetExts + auth cycle
Implements /agent/desktop persistent WebSocket, pairing routes, five
vault_* ADK tools, and Firestore-routed task queue per the approved
spec. Wire protocol matches CT v1.9.0; includes generation-guarded
desktopBridge, device-doc revoke listener, and cap decay on timeout.

Co-authored-by: Cursor <cursoragent@cursor.com>
…t-bridge

feat(cloud-agent): desktop vault bridge for Curated Thoughts
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added desktop pairing and revocation support for connecting a device to the app.
    • Introduced a desktop vault bridge so supported AI tools can query a user’s Curated Thoughts vault from a paired desktop device.
    • Added a new desktop WebSocket flow with live task dispatch, results, and connection health handling.
  • Bug Fixes

    • Improved dev-sandbox flag handling across the app for more consistent behavior.
    • Added safeguards so revoked or paused desktop connections close cleanly and don’t keep serving tasks.
  • Documentation

    • Expanded README and design docs with desktop vault bridge usage and local testing guidance.

Walkthrough

This PR adds a Desktop Vault Bridge to the cloud agent: pairing tokens, an in-memory WebSocket connection registry, a desktop WebSocket handler, Firestore-backed desktop task orchestration, and five vault_* ADK tools wired into the agent, live tools, and HTTP routes. Separately, it refactors isDevSandboxEnabled into a dedicated devSandboxFlag module used across the mobile app.

Changes

Desktop Vault Bridge Feature

Layer / File(s) Summary
Pairing service
cloud-agent/src/services/desktopPairing.ts, desktopPairing.test.ts
Adds token generation, device pairing, token resolution, and revocation against a Firestore-like abstraction.
Connection registry
cloud-agent/src/services/desktopBridge.ts, desktopBridge.test.ts
In-memory per-instance registry keyed by uid:deviceId with generation-based staleness guards.
WebSocket handler
cloud-agent/src/handlers/wsDesktopAgentHandler.ts, wsDesktopAgentHandler.test.ts
Handles auth, ping/pong, task dispatch/result, device-doc listener-driven revoke/pause closure, and disconnect cleanup.
Firestore orchestration
cloud-agent/src/services/firestoreSession.ts, firestoreSession.test.ts
Adds desktop device/task selection, lifecycle transitions, liveness tracking, and a query-preserving collection adapter.
Vault tools
cloud-agent/src/tools/vaultTools.ts, vaultTools.test.ts
Implements five vault_* FunctionTools dispatching via local socket or Firestore with call caps, decay, and billing hooks.
Wiring
cloud-agent/src/services/agentCore.ts, liveToolAdapter.ts, wsLiveAgentHandler.ts, index.ts, vaultToolsWiring.test.ts
Wires vault deps into buildAgent/buildLiveTools, adds /agent/desktop/pair, /agent/desktop/revoke routes and /agent/desktop upgrade handling.
Docs & security
README.md, docs/desktop-vault-bridge.md, docs/superpowers/..., firestore.rules, cloud-agent/scripts/desktopBridgeSmoke.ts
Adds bridge documentation, spec amendments, a deny-all rule for desktopTasks, and a manual smoke-test script.

Dev Sandbox Flag Refactor

Layer / File(s) Summary
New flag module
src/auth/devSandboxFlag.ts, src/auth/ensureDevSandboxCharacter.ts
Extracts isDevSandboxEnabled into its own module.
Consumer updates
src/auth/bootstrapSession.ts, src/machines/characterMachine.ts, liveVoiceMachine.ts, src/hooks/useAIChat.ts, useTabCharacterId.ts, src/services/aiChatService.ts, characterSyncService.ts, wikiLlmProvider.ts, app/_layout.tsx
Updates imports to the new module, with dynamic imports of ensureDevSandboxCharacter where used.
Test mocks
__tests__/*.test.ts(x)
Updates Jest mocks/imports to mock devSandboxFlag instead of ensureDevSandboxCharacter.
Docs
docs/flowcharts/HOOKS.md, MACHINES.md, SERVICES.md
Updates auto-generated dependency graph edges.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Poem

A vault, a socket, a token so bright,
Hop through the wires from morning to night 🐇
Desktop and cloud now speak as one,
While devSandboxFlag keeps mocking fun.
Carrots for tests that all turned green—
Thump-thump! The best bridge this burrow's seen.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title is too vague to describe the actual changes in this PR. Use a concise, specific title like 'Add desktop vault bridge and dev-sandbox flag split'.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The description matches the template and covers the required sections, with only minor omissions in related issue and test-platform details.
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

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

Refine vault tool timeout/cap logic, simplify test fixtures for
desktop pairing and task workflows. Update bridge documentation with
error codes and failure modes.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

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

Pull request overview

Adds the Clanker side of the Desktop Vault Bridge: a persistent /agent/desktop WebSocket (pairing-token auth) that routes Firestore desktopTasks to a socket-owning Cloud Run instance, plus five read-only vault_* ADK tools wired into /agent/run and /agent/live. This also includes supporting docs, Firestore security rules, tests, and a small client-side refactor to isolate the dev-sandbox enablement flag.

Changes:

  • Implemented desktop vault bridge server components in cloud-agent/: pairing + revoke, desktopBridge registry, wsDesktopAgentHandler, Firestore task/device helpers, and vault_* tools (incl. wiring + tests).
  • Updated Firestore rules and documentation/specs/plans (incl. architecture and testing plan updates).
  • Refactored dev sandbox gating into src/auth/devSandboxFlag.ts and updated call sites/tests; adjusted Metro config comment and OTA script flag.

Reviewed changes

Copilot reviewed 44 out of 44 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/services/wikiLlmProvider.ts Switch isDevSandboxEnabled import to shared flag module
src/services/characterSyncService.ts Switch isDevSandboxEnabled import to shared flag module
src/services/aiChatService.ts Switch isDevSandboxEnabled import to shared flag module
src/machines/liveVoiceMachine.ts Switch isDevSandboxEnabled import to shared flag module
src/machines/characterMachine.ts Lazy-load ensureDevSandboxCharacter; switch flag import
src/hooks/useTabCharacterId.ts Switch isDevSandboxEnabled import to shared flag module
src/hooks/useAIChat.ts Switch isDevSandboxEnabled import to shared flag module
src/auth/ensureDevSandboxCharacter.ts Move flag logic to devSandboxFlag
src/auth/devSandboxFlag.ts New: centralized dev-sandbox enablement predicate
src/auth/bootstrapSession.ts Lazy-load ensureDevSandboxCharacter; switch flag import
README.md Add Desktop Vault Bridge doc link
package.json Add --clear-cache to OTA update script
metro.config.js Document why .cjs must not be treated as an asset
firestore.rules Deny all client access to users/{uid}/desktopTasks
docs/superpowers/specs/2026-07-05-desktop-vault-bridge-design.md Spec amendments and expanded lifecycle/testing notes
docs/superpowers/plans/2026-07-05-desktop-vault-bridge.md Plan updated to incorporate architecture review amendments
docs/flowcharts/SERVICES.md Flowchart updates for dev-sandbox flag module
docs/flowcharts/MACHINES.md Flowchart updates for dev-sandbox flag module
docs/flowcharts/HOOKS.md Flowchart updates for dev-sandbox flag module
docs/desktop-vault-bridge.md New operator/system documentation for the vault bridge
cloud-agent/src/tools/vaultToolsWiring.test.ts New: tests tool presence gating in buildAgent/buildLiveTools
cloud-agent/src/tools/vaultTools.ts New: vault_* ADK tools + Firestore task dispatch logic
cloud-agent/src/tools/vaultTools.test.ts New: unit tests for vault tools + cap/decay/billing behaviors
cloud-agent/src/services/liveToolAdapter.ts Wire vault tools into live (voice) toolset when deps provided
cloud-agent/src/services/firestoreSession.ts Add desktop device/task APIs and snapshot support
cloud-agent/src/services/firestoreSession.test.ts Add tests for desktop device filtering and desktopTasks lifecycle
cloud-agent/src/services/desktopPairing.ts New: pairing token generation, resolve, and revoke logic
cloud-agent/src/services/desktopPairing.test.ts New: pairing service unit tests
cloud-agent/src/services/desktopBridge.ts New: per-instance desktopBridge registry w/ generation guard
cloud-agent/src/services/desktopBridge.test.ts New: desktopBridge generation/replace behavior tests
cloud-agent/src/services/agentCore.ts Wire vault tools into text agent when deps provided
cloud-agent/src/index.ts Add /agent/desktop routes (HTTP + WS) and vault deps creation
cloud-agent/src/handlers/wsLiveAgentHandler.ts Provide vault deps for live sessions; pass through to tool builder
cloud-agent/src/handlers/wsDesktopAgentHandler.ts New: /agent/desktop WS auth, liveness, task dispatch/results
cloud-agent/src/handlers/wsDesktopAgentHandler.test.ts New: handler auth/dispatch/liveness/revoke/pause tests
cloud-agent/scripts/desktopBridgeSmoke.ts New: manual smoke client for local desktop bridge testing
app/_layout.tsx Switch isDevSandboxEnabled import to shared flag module
tests/wikiLlmProvider.test.ts Update mocks/imports for new dev-sandbox flag module
tests/useAIChat.test.tsx Update mocks/imports for new dev-sandbox flag module
tests/liveVoiceMachine.test.ts Update mocks/imports for new dev-sandbox flag module
tests/ensureDevSandboxCharacter.test.ts Split imports across ensure + devSandboxFlag modules
tests/characterSyncWiki.test.ts Update mock path for isDevSandboxEnabled
tests/characterSyncIdempotentUpload.test.ts Update mock path for isDevSandboxEnabled
tests/bootstrapSession.test.ts Update mocks to match new devSandboxFlag + lazy ensure import

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread cloud-agent/src/tools/vaultTools.ts Outdated
Comment thread cloud-agent/src/tools/vaultTools.ts Outdated
Comment thread cloud-agent/src/handlers/wsDesktopAgentHandler.ts
Comment thread cloud-agent/src/handlers/wsDesktopAgentHandler.ts
Comment thread cloud-agent/src/handlers/wsDesktopAgentHandler.ts Outdated
Comment thread cloud-agent/src/handlers/wsDesktopAgentHandler.ts Outdated
Comment thread cloud-agent/src/handlers/wsDesktopAgentHandler.ts
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Kurt VanDusen <info@equationalapplications.com>
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 7, 2026
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Kurt VanDusen <info@equationalapplications.com>
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 7, 2026
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Kurt VanDusen <info@equationalapplications.com>
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 7, 2026
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Kurt VanDusen <info@equationalapplications.com>
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 7, 2026
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Kurt VanDusen <info@equationalapplications.com>
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 7, 2026
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Kurt VanDusen <info@equationalapplications.com>
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 7, 2026
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Kurt VanDusen <info@equationalapplications.com>
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 7, 2026

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

Pull request overview

Copilot reviewed 44 out of 44 changed files in this pull request and generated 5 comments.

Comment thread cloud-agent/src/handlers/wsDesktopAgentHandler.ts
Comment thread cloud-agent/src/tools/vaultTools.ts
Comment thread cloud-agent/src/tools/vaultTools.ts
Comment thread cloud-agent/src/tools/vaultTools.ts
Comment thread cloud-agent/src/services/firestoreSession.ts
- Use static WebSocket.OPEN constant instead of instance property
- Always unsubscribe listeners on disconnect; only mark offline for current generation
- Fix indentation and use proper constant in dispatchLocalIfConnected
- Allow task results for same-instance shortcut (warn on reject)
- Check device before applying per-turn cap
- Fix cap decay to not exceed maxCallsPerTurn
- Inline failDesktopTaskIfUnresolved to avoid this binding issues

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
@equationalapplications

Copy link
Copy Markdown
Owner Author

/fix-pr follow-up

Commit: 21763369

Review resolution

  • WebSocket.OPEN constant: Fixed wsDesktopAgentHandler.ts and vaultTools.ts to use static WebSocket.OPEN instead of instance property (files: wsDesktopAgentHandler.ts, vaultTools.ts)
  • Listener cleanup: Ensured listeners are always unsubscribed on disconnect, only mark device offline for current generation (file: wsDesktopAgentHandler.ts)
  • Error handler disconnect: Added runDisconnectPath() to error handler (file: wsDesktopAgentHandler.ts)
  • Task result handling: Allow all task results (same-instance shortcut support), warn on rejected writes (file: wsDesktopAgentHandler.ts)
  • Device check priority: Moved device availability check before per-turn cap (file: vaultTools.ts)
  • Cap decay fix: Ensure cap decay doesn't exceed maxCallsPerTurn (file: vaultTools.ts)
  • This binding: Inlined failDesktopTaskIfUnresolved to use db closures (file: firestoreSession.ts)

Verification

  • typecheck — pass (npm run typecheck)
  • build — pass (npm run build)
  • tests — in progress

🤖 Generated by /fix-pr

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
putComment timed out

@equationalapplications
equationalapplications merged commit e2e4dfb into main Jul 7, 2026
5 of 6 checks passed

@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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cloud-agent/src/handlers/wsLiveAgentHandler.ts (1)

148-241: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reset vault-call budget at turn boundaries
vaultDeps is created once per live WebSocket session, but the per-turn counters in createVaultToolDeps never reset. If a timeout/disconnect triggers capDecay in one model turn, later turns in the same voice session stay artificially capped. Handle serverContent.turnComplete and clear callsThisTurn/capDecay there.

🤖 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 `@cloud-agent/src/handlers/wsLiveAgentHandler.ts` around lines 148 - 241, Reset
the per-turn vault call budget in handleGeminiMessage by handling
serverContent.turnComplete, since vaultDeps persists across the live WebSocket
session and createVaultToolDeps currently keeps callsThisTurn/capDecay across
turns. Add turn-boundary handling alongside the existing serverContent
processing so that when turnComplete is received, those counters are cleared
before the next turn starts.
🧹 Nitpick comments (2)
cloud-agent/src/tools/vaultTools.ts (1)

82-133: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No error handling around Firestore calls in dispatchVaultCall.

fs.getActiveDesktopDevice and fs.createDesktopTask aren't wrapped in try/catch; unlike the crafted NO_DEVICE_MSG/TIMEOUT_MSG/CAP_MSG friendly errors, a transient Firestore failure here would reject the tool's promise instead of returning a graceful message to the model.

🤖 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 `@cloud-agent/src/tools/vaultTools.ts` around lines 82 - 133, Add explicit
error handling in dispatchVaultCall around the Firestore-dependent calls so
transient failures return a friendly tool message instead of rejecting. Wrap
fs.getActiveDesktopDevice and fs.createDesktopTask (and any immediate dependent
dispatch setup) in try/catch, and on failure return a stable Vault error string
similar to NO_DEVICE_MSG and TIMEOUT_MSG. Keep the existing control flow in
dispatchVaultCall and preserve billing pause/resume in the finally block.
cloud-agent/src/index.ts (1)

71-86: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant defaultFirestoreSession() call for vault.

bridge and vault are both gated on admin.apps.length, but vault calls defaultFirestoreSession() again instead of reusing bridge.firestoreSession (as wsLiveAgentHandler.ts does for its vaultDeps). Reusing the instance avoids constructing a redundant session object per /agent/run call.

♻️ Suggested fix
   const bridge = admin.apps.length ? {
     firebaseUid,
     userId,
     firestoreSession: defaultFirestoreSession(),
     fcmDispatcher: defaultFcmDispatcher(),
     creditService: createCreditService(db),
     instanceId: INSTANCE_ID,
   } : undefined
-  const vault = admin.apps.length ? createVaultToolDeps({
+  const vault = bridge ? createVaultToolDeps({
     firebaseUid,
-    firestoreSession: defaultFirestoreSession(),
+    firestoreSession: bridge.firestoreSession,
     desktopBridge,
   }) : undefined
🤖 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 `@cloud-agent/src/index.ts` around lines 71 - 86, The `runAgentReal` setup is
creating two separate `defaultFirestoreSession()` instances for `bridge` and
`vault`, which is redundant. Reuse the existing session from `bridge` when
building `vault` so the `createVaultToolDeps` call uses the same firestore
session object, matching the `wsLiveAgentHandler` pattern and avoiding an extra
session construction per request.
🤖 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 `@cloud-agent/src/handlers/wsDesktopAgentHandler.ts`:
- Around line 110-124: The task dispatch flow in wsDesktopAgentHandler’s
watchPendingDesktopTasks callback keeps task IDs in the dispatched set even when
fs.markDesktopTaskExecuting fails, which blocks retries on later watcher
emissions. Update the markDesktopTaskExecuting promise chain so that the taskId
is removed from dispatched inside the .catch path before logging the failure,
allowing the same socket to retry that task on subsequent updates.

In `@cloud-agent/src/services/firestoreSession.ts`:
- Around line 214-221: The instance guard in markDesktopDeviceOffline is still
using a non-atomic get() + update() flow, so a stale disconnect can overwrite a
newer markDesktopDeviceOnline state. Update the logic in firestoreSession.ts’s
markDesktopDeviceOffline to use an atomic Firestore precondition or transaction
based on connectedInstanceId, so the offline write only applies when the stored
instance still matches expectedInstanceId. Keep the existing guard semantics,
but ensure the final write cannot clobber a freshly updated online device.
- Around line 156-172: The terminal-state guard in writeDesktopTaskResult is
non-atomic and can be raced by failDesktopTaskIfUnresolved, causing a stale
failure or success to overwrite the other. Update writeDesktopTaskResult and the
matching unresolved-failure path to use a Firestore transaction via
db.runTransaction so the get/check/update happens atomically. You’ll likely need
to extend FirestoreLike and the test fake to support transactional reads and
writes for desktopTaskPath, while keeping the existing terminal-status checks in
place.

In `@cloud-agent/src/tools/vaultTools.ts`:
- Around line 68-115: The local dispatch path in dispatchLocalIfConnected and
dispatchVaultCall can race with watchPendingDesktopTasks because
createDesktopTask publishes a pending task before markDesktopTaskExecuting runs,
so the same task may be dispatched twice. Change the same-instance flow in
vaultTools to use a single deduped handoff or mark the task as executing before
it becomes visible as pending, and keep the logic anchored around
dispatchLocalIfConnected, dispatchVaultCall, and createDesktopTask so the local
bridge and watcher cannot both send it.

---

Outside diff comments:
In `@cloud-agent/src/handlers/wsLiveAgentHandler.ts`:
- Around line 148-241: Reset the per-turn vault call budget in
handleGeminiMessage by handling serverContent.turnComplete, since vaultDeps
persists across the live WebSocket session and createVaultToolDeps currently
keeps callsThisTurn/capDecay across turns. Add turn-boundary handling alongside
the existing serverContent processing so that when turnComplete is received,
those counters are cleared before the next turn starts.

---

Nitpick comments:
In `@cloud-agent/src/index.ts`:
- Around line 71-86: The `runAgentReal` setup is creating two separate
`defaultFirestoreSession()` instances for `bridge` and `vault`, which is
redundant. Reuse the existing session from `bridge` when building `vault` so the
`createVaultToolDeps` call uses the same firestore session object, matching the
`wsLiveAgentHandler` pattern and avoiding an extra session construction per
request.

In `@cloud-agent/src/tools/vaultTools.ts`:
- Around line 82-133: Add explicit error handling in dispatchVaultCall around
the Firestore-dependent calls so transient failures return a friendly tool
message instead of rejecting. Wrap fs.getActiveDesktopDevice and
fs.createDesktopTask (and any immediate dependent dispatch setup) in try/catch,
and on failure return a stable Vault error string similar to NO_DEVICE_MSG and
TIMEOUT_MSG. Keep the existing control flow in dispatchVaultCall and preserve
billing pause/resume in the finally block.
🪄 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

Run ID: a568b151-5eb5-4654-af60-d854b3ab5b68

📥 Commits

Reviewing files that changed from the base of the PR and between eea1572 and 2176336.

📒 Files selected for processing (44)
  • README.md
  • __tests__/bootstrapSession.test.ts
  • __tests__/characterSyncIdempotentUpload.test.ts
  • __tests__/characterSyncWiki.test.ts
  • __tests__/ensureDevSandboxCharacter.test.ts
  • __tests__/liveVoiceMachine.test.ts
  • __tests__/useAIChat.test.tsx
  • __tests__/wikiLlmProvider.test.ts
  • app/_layout.tsx
  • cloud-agent/scripts/desktopBridgeSmoke.ts
  • cloud-agent/src/handlers/wsDesktopAgentHandler.test.ts
  • cloud-agent/src/handlers/wsDesktopAgentHandler.ts
  • cloud-agent/src/handlers/wsLiveAgentHandler.ts
  • cloud-agent/src/index.ts
  • cloud-agent/src/services/agentCore.ts
  • cloud-agent/src/services/desktopBridge.test.ts
  • cloud-agent/src/services/desktopBridge.ts
  • cloud-agent/src/services/desktopPairing.test.ts
  • cloud-agent/src/services/desktopPairing.ts
  • cloud-agent/src/services/firestoreSession.test.ts
  • cloud-agent/src/services/firestoreSession.ts
  • cloud-agent/src/services/liveToolAdapter.ts
  • cloud-agent/src/tools/vaultTools.test.ts
  • cloud-agent/src/tools/vaultTools.ts
  • cloud-agent/src/tools/vaultToolsWiring.test.ts
  • docs/desktop-vault-bridge.md
  • docs/flowcharts/HOOKS.md
  • docs/flowcharts/MACHINES.md
  • docs/flowcharts/SERVICES.md
  • docs/superpowers/plans/2026-07-05-desktop-vault-bridge.md
  • docs/superpowers/specs/2026-07-05-desktop-vault-bridge-design.md
  • firestore.rules
  • metro.config.js
  • package.json
  • src/auth/bootstrapSession.ts
  • src/auth/devSandboxFlag.ts
  • src/auth/ensureDevSandboxCharacter.ts
  • src/hooks/useAIChat.ts
  • src/hooks/useTabCharacterId.ts
  • src/machines/characterMachine.ts
  • src/machines/liveVoiceMachine.ts
  • src/services/aiChatService.ts
  • src/services/characterSyncService.ts
  • src/services/wikiLlmProvider.ts

Comment on lines +110 to +124
unsubPending = fs.watchPendingDesktopTasks(uid, deviceId, (tasks) => {
for (const task of tasks) {
if (dispatched.has(task.taskId)) continue
dispatched.add(task.taskId)
void fs.markDesktopTaskExecuting(uid!, task.taskId)
.then(() => {
if (socketOpen()) {
ws.send(JSON.stringify({
type: 'task', taskId: task.taskId, tool: task.tool, params: task.params,
}))
}
})
.catch((err) => console.error('[desktop-bridge] dispatch failed:', task.taskId, err))
}
})

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the handler file and inspect the relevant region with line numbers.
sed -n '1,260p' cloud-agent/src/handlers/wsDesktopAgentHandler.ts | cat -n

printf '\n---\n'

# Search for the watcher and dispatch helpers involved in retry behavior.
rg -n "watchPendingDesktopTasks|markDesktopTaskExecuting|dispatched|pendingDesktopTasks|DesktopTask" cloud-agent/src -S

Repository: equationalapplications/clanker

Length of output: 20378


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the surrounding implementation of the desktop task store/watchers if present.
fd -a ".*desktop.*|.*task.*" cloud-agent/src | sed -n '1,200p'

Repository: equationalapplications/clanker

Length of output: 672


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== wsDesktopAgentHandler.ts (relevant section) ==="
nl -ba cloud-agent/src/handlers/wsDesktopAgentHandler.ts | sed -n '90,170p'

echo
echo "=== references to watchPendingDesktopTasks / markDesktopTaskExecuting / dispatched ==="
rg -n "watchPendingDesktopTasks|markDesktopTaskExecuting|dispatched" cloud-agent/src -S

echo
echo "=== candidate task-store files ==="
fd -a "desktop|task" cloud-agent/src | sed -n '1,200p'

Repository: equationalapplications/clanker

Length of output: 260


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== definitions/usages of watchPendingDesktopTasks and markDesktopTaskExecuting across repo ==="
rg -n "watchPendingDesktopTasks\(|markDesktopTaskExecuting\(" -S .

echo
echo "=== locate the module that provides fs in wsDesktopAgentHandler.ts ==="
sed -n '1,120p' cloud-agent/src/handlers/wsDesktopAgentHandler.ts | nl -ba | sed -n '1,120p'

Repository: equationalapplications/clanker

Length of output: 2874


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the implementation of watchPendingDesktopTasks / markDesktopTaskExecuting.
rg -n "function watchPendingDesktopTasks|const watchPendingDesktopTasks|watchPendingDesktopTasks:|markDesktopTaskExecuting" cloud-agent -S

echo
# Show the file(s) containing those definitions, if any, with line numbers.
FILES="$(rg -l "watchPendingDesktopTasks|markDesktopTaskExecuting" cloud-agent -S || true)"
for f in $FILES; do
  echo "--- $f ---"
  nl -ba "$f" | sed -n '1,260p'
done

Repository: equationalapplications/clanker

Length of output: 1528


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== cloud-agent/src/services/firestoreSession.ts (relevant sections) ==="
sed -n '130,220p' cloud-agent/src/services/firestoreSession.ts | cat -n

echo
echo "=== cloud-agent/src/services/firestoreSession.test.ts (task-state tests) ==="
sed -n '330,430p' cloud-agent/src/services/firestoreSession.test.ts | cat -n

echo
echo "=== docs/superpowers/plans/2026-07-05-desktop-vault-bridge.md (dispatch sections) ==="
sed -n '400,470p' docs/superpowers/plans/2026-07-05-desktop-vault-bridge.md | cat -n
sed -n '970,1005p' docs/superpowers/plans/2026-07-05-desktop-vault-bridge.md | cat -n

Repository: equationalapplications/clanker

Length of output: 16671


Release dispatched when markDesktopTaskExecuting fails. A rejected update leaves the task pending in Firestore, but the local dispatched guard prevents the same socket from trying it again on later watcher emissions. Delete the taskId in .catch so it can be retried.

🤖 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 `@cloud-agent/src/handlers/wsDesktopAgentHandler.ts` around lines 110 - 124,
The task dispatch flow in wsDesktopAgentHandler’s watchPendingDesktopTasks
callback keeps task IDs in the dispatched set even when
fs.markDesktopTaskExecuting fails, which blocks retries on later watcher
emissions. Update the markDesktopTaskExecuting promise chain so that the taskId
is removed from dispatched inside the .catch path before logging the failure,
allowing the same socket to retry that task on subsequent updates.

Comment on lines +156 to +172
async writeDesktopTaskResult(
uid: string,
taskId: string,
outcome: { status: 'complete'; result: unknown } | { status: 'failed'; error: DesktopTaskError },
): Promise<boolean> {
const ref = db.doc(desktopTaskPath(uid, taskId))
const snap = await ref.get()
if (!snap.exists) return false
const current = snap.data() as unknown as DesktopTaskDoc
if (current.status === 'complete' || current.status === 'failed') return false
await ref.update(
outcome.status === 'complete'
? { status: 'complete', result: outcome.result, updatedAt: now() }
: { status: 'failed', error: outcome.error, updatedAt: now() },
)
return 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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Non-atomic terminal-state guard allows lost/overwritten task results.

Both writeDesktopTaskResult and failDesktopTaskIfUnresolved do a plain get() then conditionally update(). These are called from independent triggers that can legitimately race: CT's task_result/task_error frame (via writeDesktopTaskResult in the WS handler) vs. the caller's callTimeoutMs timer (via failDesktopTaskIfUnresolved in vaultTools.ts), often on different Cloud Run instances. If both read the doc while it's still executing, both pass the terminal check and both write — the last update() wins, so a real complete result can be silently overwritten by a stale DESKTOP_TIMEOUT/DESKTOP_DISCONNECTED failure (or vice versa). The in-flight caller may already have resolved via the first snapshot it observed, but the persisted Firestore doc — which the docs explicitly say should be monitored for DESKTOP_TIMEOUT frequency — ends up inconsistent.

🔧 Suggested direction

Wrap the read-check-write in a Firestore transaction (db.runTransaction), which requires extending the FirestoreLike abstraction (and the test fake) to expose transactional reads/writes for this doc path, so the terminal-state check is atomic with the write.

Also applies to: 180-188

🤖 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 `@cloud-agent/src/services/firestoreSession.ts` around lines 156 - 172, The
terminal-state guard in writeDesktopTaskResult is non-atomic and can be raced by
failDesktopTaskIfUnresolved, causing a stale failure or success to overwrite the
other. Update writeDesktopTaskResult and the matching unresolved-failure path to
use a Firestore transaction via db.runTransaction so the get/check/update
happens atomically. You’ll likely need to extend FirestoreLike and the test fake
to support transactional reads and writes for desktopTaskPath, while keeping the
existing terminal-status checks in place.

Comment on lines +214 to +221
async markDesktopDeviceOffline(uid: string, deviceId: string, expectedInstanceId?: string): Promise<void> {
const ref = db.doc(`${devicesPath(uid)}/${deviceId}`)
const snap = await ref.get()
if (!snap.exists) return
const data = snap.data() as { connectedInstanceId?: string | null } | undefined
if (expectedInstanceId !== undefined && data?.connectedInstanceId !== expectedInstanceId) return
await ref.update({ online: false, connectedInstanceId: 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.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

markDesktopDeviceOffline's instance-guard has the same TOCTOU gap.

The check-then-update here is exactly what the design spec's "Replacement race guard" (§5) is meant to prevent, but get()+update() isn't atomic: if a new connection's markDesktopDeviceOnline (writing the new connectedInstanceId) lands between this get() and update(), the stale disconnect's update() still fires unconditionally and clobbers the freshly-online device back to online: false, connectedInstanceId: null. Since nothing re-marks the device online until its next actual reconnect, getActiveDesktopDevice's online === true filter would incorrectly treat a live, functioning desktop as unavailable.

🤖 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 `@cloud-agent/src/services/firestoreSession.ts` around lines 214 - 221, The
instance guard in markDesktopDeviceOffline is still using a non-atomic get() +
update() flow, so a stale disconnect can overwrite a newer
markDesktopDeviceOnline state. Update the logic in firestoreSession.ts’s
markDesktopDeviceOffline to use an atomic Firestore precondition or transaction
based on connectedInstanceId, so the offline write only applies when the stored
instance still matches expectedInstanceId. Keep the existing guard semantics,
but ensure the final write cannot clobber a freshly updated online device.

Comment on lines +68 to +115
async function dispatchLocalIfConnected(
deps: VaultToolDeps,
deviceId: string,
taskId: string,
wireTool: string,
params: Record<string, unknown>,
): Promise<void> {
const conn = deps.desktopBridge?.get(deps.firebaseUid, deviceId)
const ws = conn?.ws
if (!ws || ws.readyState !== WebSocket.OPEN) return
await deps.firestoreSession.markDesktopTaskExecuting(deps.firebaseUid, taskId)
ws.send(JSON.stringify({ type: 'task', taskId, tool: wireTool, params }))
}

async function dispatchVaultCall(
deps: VaultToolDeps,
adkName: VaultAdkName,
params: Record<string, unknown>,
): Promise<string> {
const fs = deps.firestoreSession
const device = await fs.getActiveDesktopDevice(deps.firebaseUid)
if (!device) return NO_DEVICE_MSG

const cap = effectiveCap(deps)
if (deps.callsThisTurn.count >= cap) return CAP_MSG
deps.callsThisTurn.count++

const wireTool = VAULT_WIRE_TOOL[adkName]
try {
deps.pauseBilling?.()
const taskId = crypto.randomUUID()
await fs.createDesktopTask(deps.firebaseUid, taskId, device.deviceId, wireTool, params)
await dispatchLocalIfConnected(deps, device.deviceId, taskId, wireTool, params)

const task = await new Promise<DesktopTaskDoc | null>((resolve) => {
const timeout = setTimeout(() => {
unsub()
void fs.failDesktopTaskIfUnresolved(deps.firebaseUid, taskId, {
code: 'DESKTOP_TIMEOUT', message: `No result within ${deps.callTimeoutMs}ms`,
}).catch(() => { /* TTL backstop */ })
resolve(null)
}, deps.callTimeoutMs)
const unsub = fs.watchDesktopTask(deps.firebaseUid, taskId, (t) => {
if (t.status === 'complete' || t.status === 'failed') {
clearTimeout(timeout); unsub(); resolve(t)
}
})
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the full desktop WS handler to confirm dedup coverage across
# the same-instance shortcut and the pending-task listener.
fd wsDesktopAgentHandler.ts -x cat -n {}

Repository: equationalapplications/clanker

Length of output: 8804


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the Firestore task lifecycle methods and watch behavior.
fd firestoreSession.ts -x sh -c '
  echo "===== {} ====="
  cat -n "{}" | sed -n "1,260p"
'

# Also inspect the vault tool file around the dispatch path for any local guards.
fd vaultTools.ts -x sh -c '
  echo "===== {} ====="
  cat -n "{}" | sed -n "1,220p"
'

Repository: equationalapplications/clanker

Length of output: 22705


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for any coordination between vault direct dispatch and the desktop handler's dispatched set.
rg -n "dispatched\.|watchPendingDesktopTasks|dispatchLocalIfConnected|markDesktopTaskExecuting|desktopBridge\.get" cloud-agent/src

Repository: equationalapplications/clanker

Length of output: 3301


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "dispatched\.|watchPendingDesktopTasks|dispatchLocalIfConnected|markDesktopTaskExecuting|desktopBridge\.get" cloud-agent/src

Repository: equationalapplications/clanker

Length of output: 3301


Avoid the local dispatch race in cloud-agent/src/tools/vaultTools.ts:68-100.
desktopBridge.get() puts this code on the same instance that runs watchPendingDesktopTasks, so createDesktopTask(...status: 'pending') can be picked up in the gap before markDesktopTaskExecuting(), causing the same task to be sent twice. Route same-instance dispatches through one dedup path or otherwise mark them before they become visible as pending.

🤖 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 `@cloud-agent/src/tools/vaultTools.ts` around lines 68 - 115, The local
dispatch path in dispatchLocalIfConnected and dispatchVaultCall can race with
watchPendingDesktopTasks because createDesktopTask publishes a pending task
before markDesktopTaskExecuting runs, so the same task may be dispatched twice.
Change the same-instance flow in vaultTools to use a single deduped handoff or
mark the task as executing before it becomes visible as pending, and keep the
logic anchored around dispatchLocalIfConnected, dispatchVaultCall, and
createDesktopTask so the local bridge and watcher cannot both send it.

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.

2 participants