Skip to content

DO-NOT-MERGE: Split #132 cloud workspace key work#146

Draft
kjgbot wants to merge 3 commits into
mainfrom
split/132-cloud-workspace-key
Draft

DO-NOT-MERGE: Split #132 cloud workspace key work#146
kjgbot wants to merge 3 commits into
mainfrom
split/132-cloud-workspace-key

Conversation

@kjgbot
Copy link
Copy Markdown
Contributor

@kjgbot kjgbot commented Jun 7, 2026

DO-NOT-MERGE: merge hold remains in force; this PR is opened for review only.

Split from #132 / fix/slack-integration-event-context-retry.

Summary:

Restart-branch decision note:

Verification:

  • npx --yes vitest run src/main/broker.test.ts src/main/cloud-agent.test.ts (57/57)
  • npm run build

Cross-link: #132

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 7, 2026

Important

Review skipped

Draft detected.

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

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f99ffe96-5492-4d82-ab94-58ea3e4abacc

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

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch split/132-cloud-workspace-key

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

❤️ Share

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

Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

Code Review

This pull request implements workspace key propagation from local brokers to cloud sandboxes to ensure they share a single relay workspace, adding a tripwire to detect stale sandbox brokers. Key feedback points out that workspaceKeyForProject should await revivePromises to avoid returning undefined during session revivals, and classifyWorkspaceJoinFailure should robustly extract messages from plain objects to handle serialized IPC errors. Additionally, request coalescing for spawnPersona should be restored using a dedicated map, and defensive optional chaining should be used on spawned agent results to prevent potential runtime TypeError crashes.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/main/broker.ts
Comment on lines +1501 to +1514
async workspaceKeyForProject(projectId: string): Promise<string | undefined> {
const normalizedProjectId = projectId.trim()
if (!normalizedProjectId) return undefined
const startPromise = this.startPromises.get(normalizedProjectId)
if (startPromise) await startPromise.catch(() => undefined)
const session = this.sessions.get(normalizedProjectId)
if (!session) return undefined
try {
const metadata = await session.client.getSession()
return metadata.workspace_key || undefined
} catch {
return undefined
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

In workspaceKeyForProject, only startPromises is awaited. However, if a local broker session is currently being revived (i.e., there is an active promise in revivePromises), this.sessions.get(normalizedProjectId) will return undefined because reviveSession drops the session immediately before restarting it.

This causes workspaceKeyForProject to prematurely return undefined instead of waiting for the revive to complete. Consequently, any concurrent cloud agent provisioning/warming will fail to obtain the local workspace key and will fallback to creating an isolated, keyless workspace.

We should await revivePromises just like we do in getOrAwaitSessionsForProject.

  async workspaceKeyForProject(projectId: string): Promise<string | undefined> {
    const normalizedProjectId = projectId.trim()
    if (!normalizedProjectId) return undefined
    const revivePromise = this.revivePromises.get(normalizedProjectId)
    if (revivePromise) await revivePromise.catch(() => undefined)
    const startPromise = this.startPromises.get(normalizedProjectId)
    if (startPromise) await startPromise.catch(() => undefined)
    const session = this.sessions.get(normalizedProjectId)
    if (!session) return undefined
    try {
      const metadata = await session.client.getSession()
      return metadata.workspace_key || undefined
    } catch {
      return undefined
    }
  }

Comment thread src/main/broker.ts
Comment on lines +147 to +152
export function classifyWorkspaceJoinFailure(err: unknown): 'rejected' | 'rate-limited' | null {
const message = toErrorMessage(err)
if (/explicit workspace key .* was rate-limited/iu.test(message)) return 'rate-limited'
if (/explicit workspace key .* was rejected/iu.test(message)) return 'rejected'
return 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.

medium

In Electron applications, errors passed across IPC or serialized/deserialized boundaries often lose their prototype and are no longer instances of Error, but plain objects containing a message property. Since toErrorMessage only extracts .message from instanceof Error and falls back to String(err) (which yields "[object Object]" for plain objects), the regex checks in classifyWorkspaceJoinFailure will fail for these serialized errors.

We should robustly extract the message property from plain objects if it exists.

export function classifyWorkspaceJoinFailure(err: unknown): 'rejected' | 'rate-limited' | null {
  const message = err && typeof err === 'object' && 'message' in err && typeof err.message === 'string'
    ? err.message
    : toErrorMessage(err)
  if (/explicit workspace key .* was rate-limited/iu.test(message)) return 'rate-limited'
  if (/explicit workspace key .* was rejected/iu.test(message)) return 'rejected'
  return null
}

Comment thread src/main/broker.ts Outdated
private startPromises = new Map<string, Promise<boolean | void>>()
private revivePromises = new Map<string, Promise<boolean>>()
private inFlightSpawnRequests = new Map<string, Promise<BrokerSpawnResult>>()
private inFlightSpawnRequests = new Map<string, Promise<{ name: string; runtime: string }>>()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

To support restoring the concurrent request coalescing/deduplication for spawnPersona (which was removed in this PR), we should declare a separate map specifically for in-flight persona spawn requests. This keeps the types clean and prevents duplicate concurrent spawns of the same persona.

  private inFlightSpawnRequests = new Map<string, Promise<{ name: string; runtime: string }>>()
  private inFlightPersonaSpawnRequests = new Map<string, Promise<{ name: string; runtime: string; cli?: string }>>()

Comment thread src/main/broker.ts Outdated
const spawned = await session.client.spawnPty(nextInput)
const safeSpawned = normalizeSpawnPtyResult(spawned, nextInput.name)
const spawnedName = safeSpawned.name
const spawnedName = spawned.name || nextInput.name
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Since the robust normalizeSpawnPtyResult helper was removed in this PR, we are now directly accessing spawned.name. If the broker returns an unexpected payload or if spawned is null/undefined at runtime, this will throw a TypeError and crash the spawn flow.

Using optional chaining spawned?.name is a safer, more defensive approach.

        const spawnedName = spawned?.name || nextInput.name

Comment thread src/main/broker.ts Outdated
const safeSpawned = normalizeSpawnPtyResult(spawned, nextInput.name, input.resolvedHarness)
this.rememberAgentSession(safeSpawned.name, sessionKeyFor(session))
return safeSpawned
this.rememberAgentSession(spawned.name || nextInput.name, sessionKeyFor(session))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Similar to spawnAgentOnce, we should use optional chaining spawned?.name here to defensively guard against potential null/undefined values or unexpected payloads returned by the broker.

        this.rememberAgentSession(spawned?.name || nextInput.name, sessionKeyFor(session))

@agent-relay-code
Copy link
Copy Markdown
Contributor

⚠️ pr-reviewer push failed (exit 128) — fixes were not applied to the PR. The notes below are advisory and were not pushed.

Reviewed PR #146 from .workforce/pr.diff and fixed a regression in the PR changes.

I restored clone-safe broker spawn results and persona spawn coalescing in broker.ts. The PR had removed normalization, which could return raw broker objects with functions across IPC. I also restored regression coverage in broker.test.ts, including duplicate persona spawn coalescing.

Validation run:

  • npm ci
  • npx vitest run src/main/broker.test.ts passed: 42 tests
  • npx vitest run src/main/cloud-agent.test.ts passed: 18 tests
  • npx electron-vite build passed

I also tried npx tsc -p tsconfig.node.json --noEmit, but that command is not currently a clean repo check in this checkout: it pulls in many existing test/config/type errors unrelated to this PR. I did not use git or gh, so I cannot claim remote CI or mergeability is verified.

@agent-relay-code
Copy link
Copy Markdown
Contributor

pr-reviewer applied fixes — committed and pushed cda07de to this PR. The notes below describe what changed.

Reviewed PR #146 and fixed a validated regression in the changed broker spawn path.

Changes made:

  • Restored clone-safe normalization for BrokerManager.spawnAgent() and persona spawns in src/main/broker.ts.
  • Restored keyed in-flight coalescing for duplicate spawnPersona() requests.
  • Re-added regression coverage in src/main/broker.test.ts for clone-safe spawn payloads and duplicate persona spawn suppression.

Verification run:

  • npx vitest run src/main/broker.test.ts src/main/cloud-agent.test.ts
  • npx tsc --noEmit
  • npm test
  • npx vitest run

All passed locally. I did not verify remote CI status or GitHub mergeability from here.

@agent-relay-code
Copy link
Copy Markdown
Contributor

pr-reviewer applied fixes — committed and pushed 83dc517 to this PR. The notes below describe what changed.

Reviewed PR #146 and fixed two validated issues in the current checkout:

  • src/main/broker.ts: pinned AGENT_RELAY_WORKSPACE_KEY now participates in existing-connection reuse. If a connection file points at a broker with a different workspace key, Pear disconnects it and starts through the pinned spawn path instead of silently joining the wrong workspace.
  • src/main/broker.ts: redacted the explicit workspace key in the broker startup log while still passing the full key to HarnessDriverClient.spawn.

Added regression coverage in src/main/broker.test.ts for mismatched pinned connection reuse, and extended the existing explicit-key test to assert the key is not logged in full.

Verification run:

  • npx vitest run src/main/broker.test.ts
  • npx vitest run src/main/cloud-agent.test.ts
  • npx tsc --noEmit
  • npm run build
  • npm run verify:mcp-resources-drift

Build passed with existing Vite dynamic/static import chunk warnings. I’m not printing READY because I cannot verify GitHub-side required checks, mergeability, or pending CI state from this sandbox.

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