Skip to content

feat(remote-bridge): standalone Go chat companion bridge (1/2)#1275

Merged
Astro-Han merged 24 commits into
devfrom
codex/i1188-remote-gateway
Jun 15, 2026
Merged

feat(remote-bridge): standalone Go chat companion bridge (1/2)#1275
Astro-Han merged 24 commits into
devfrom
codex/i1188-remote-gateway

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Jun 12, 2026

Copy link
Copy Markdown
Owner

Summary

Standalone Go chat companion bridge (packages/remote-bridge/) — PR 1 of 2, split out of the original combined remote-access branch for reviewability. This PR adds only the self-contained Go subsystem; the desktop integration + settings UI follow in PR 2 (built on this branch).

New module — own go.mod, depends only on github.com/chenhg5/cc-connect, zero imports back into the app/Electron:

  • internal/bridge — engine + persisted session pointers (message ↔ session ↔ agent); bounded sink-retry for user-visible deliveries
  • internal/gateway — wires platforms + engine + PawWork client; audience gating; hydrate-on-reconnect
  • internal/pawwork — HTTP client + SSE event stream/replay to the PawWork server
  • internal/platforms — platform registry (cc-connect adapters)
  • cmd/pawwork-remote-bridge — binary entrypoint
  • CI (.github/workflows/build.yml): setup-go + go test ./... + race tests on bridge/gateway/pawwork, with a meta-test in packages/opencode pinning those steps

~4.9k insertions, 0 deletions, all under packages/remote-bridge/ plus the CI step. Purely additive — touches no existing app runtime code, so it ships dormant until PR 2 wires it in.

Why

Issue #1188 (mobile companion): let users monitor and interact with running agents from chat platforms (Slack/Discord/QQ/Feishu) on their phone. Landing the backend bridge in isolation lets it be reviewed module-by-module without the desktop/UI surface.

Related Issue

Part of #1188 (PR 1 of 2; the desktop + UI half in PR 2 completes and closes it).

Human Review Status

Pending

Review Focus

  • Delivery/cursor model (internal/pawwork + internal/bridge): the global SSE cursor tracks ingestion and always advances. User-visible deliveries (assistant text, permission/question prompts) get a bounded sink-retry; permission/question are additionally reconciled by gateway hydrate on reconnect. The shared cursor is never held on a delivery failure — doing so would wedge every session's stream.
  • Error classification (internal/pawwork/client.go): fatal stream errors (401/403/404, non-SSE content-type) stop the reconnect loop; only genuinely transient hydration errors (408/429/5xx, timeouts) are skipped — JSON/protocol errors surface rather than silently dropping pending interactions.
  • Audience gating (internal/gateway): who is allowed to drive a session from chat.

Risk Notes

  • New third-party dependency, isolated to the module's own go.mod: github.com/chenhg5/cc-connect (chat-platform adapters). No change to the app's dependency tree.
  • Local file write: persists session pointers to a JSON file (SessionPointersStore) via plain path/filepath; cross-platform.
  • Ships dormant: no existing app code imports the module yet, so there is no runtime behavior change until PR 2.
  • Skipped (conditional) checklist items:
    • UI/copy — unticked: no visible UI or copy in this PR (UI lands in PR 2).
    • macOS/Windows platform — unticked: no packaging/updater/signing surface is wired in; the binary is not bundled yet.

How To Verify

go vet ./...                                 clean (packages/remote-bridge)
go test ./...                                ok — bridge, gateway, pawwork, platforms
go test -race ./internal/{bridge,gateway,pawwork}   ok — no data races
opencode meta-test (build-workflow.test.ts)  6 pass / 115 expect — pins the 2 new CI steps

Screenshots or Recordings

None — no visible UI in this PR.

Checklist

How to use this checklist:

  • Tick a box by replacing [ ] with [x]. Do not edit, add, or remove items.
  • The bot-applied label items can only be honestly ticked AFTER the PR is opened and the labeler / priority-triage bots have run — return to the PR description and tick them then.
  • Most items are required. The few that are conditional are explicitly marked (conditional); for those, leave unticked if they truly do not apply and explain why in Risk Notes. All other items must be ticked before requesting human review.
  • Type label — this PR carries exactly one of bug, enhancement, task, documentation. Type labels are author-added; the labeler bot does NOT assign them. Add the label in the GitHub UI, then tick this.
  • Routing labels — this PR carries at least one of app, ui, platform, harness, ci. The labeler bot assigns these on PR open based on changed paths. Confirm the bot's choice (or override if wrong), then tick this.
  • Priority label — this PR carries exactly one of P0, P1, P2, P3. The priority-triage bot suggests one on PR open. Confirm or override, then tick this.
  • Human Review Status above is set to Pending, Approved by @<reviewer>, or Not required: <reason> (default is Pending; "not required" is restricted to bot-authored low-risk PRs).
  • I linked the related issue, or stated in Summary why there is no issue.
  • I described the review focus and any meaningful risks.
  • I replaced the example block in How To Verify with the real verification steps and the key result for each.
  • I did not introduce unrelated refactors, dependencies, generated files, or file changes beyond the stated scope.
  • (conditional) I manually checked visible UI or copy changes when needed, with screenshots or recordings. Leave unticked only if no visible UI or copy changed.
  • (conditional) I considered macOS and Windows impact for platform, packaging, updater, signing, paths, shell, or permissions changes. Leave unticked only if no platform/packaging surface was touched.
  • (conditional) I called out docs, release notes, dependencies, permissions, credentials, deletion behavior, generated content, or local file changes when relevant. Leave unticked only if none of those surfaces was touched.
  • I reviewed the final diff for unrelated changes and suspicious dependency changes.
  • I am targeting dev, and my PR title and commit messages use Conventional Commits in English.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added pawwork-remote-bridge command with JSON config loading, -list-platforms output, and signal-aware execution.
    • Introduced the remote-bridge engine and gateway for session routing, pending permission/question flows, assistant text delivery, and state hydration.
    • Added a PawWork HTTP/SSE client with cursor persistence, reconnect replay handling, event repair, and resilient prompt/text delivery.
  • Tests

    • Expanded coverage for engine, gateway, client, SSE dispatch/reconciliation, session pointers, platform availability, and delivery retry/recovery behaviors.
  • Chores

    • Updated CI to set up Go, cache dependencies, and run unit + race tests while skipping finalize.

@Astro-Han Astro-Han added enhancement New feature or request ci Continuous integration / GitHub Actions P2 Medium priority app Application behavior and product flows platform Electron shell, OS integration, packaging, updater, signing, paths, and permissions labels Jun 12, 2026
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e300cf22-548a-4a77-b635-6badd11c2d39

📥 Commits

Reviewing files that changed from the base of the PR and between edceaa3 and 708d758.

📒 Files selected for processing (4)
  • packages/remote-bridge/internal/bridge/engine.go
  • packages/remote-bridge/internal/bridge/engine_test.go
  • packages/remote-bridge/internal/bridge/session_pointers.go
  • packages/remote-bridge/internal/bridge/session_pointers_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/remote-bridge/internal/bridge/session_pointers.go

📝 Walkthrough

Walkthrough

Introduces the complete packages/remote-bridge Go service: module definition with messaging SDK dependencies, in-memory and file-backed session pointer stores with root-binding conflict detection, a bridge engine for per-remote-key session routing and permission/question blocker resolution with delivery restoration, a PawWork HTTP client with SSE event streaming and per-directory error handling, a gateway orchestrator with hydration and event retry logic, a platform registry, and a CLI entrypoint, plus CI workflow steps for Go testing.

Changes

Remote-bridge Go service

Layer / File(s) Summary
Go module bootstrap and platform registry
packages/remote-bridge/go.mod, packages/remote-bridge/internal/platforms/platforms.go, packages/remote-bridge/internal/platforms/platforms_test.go
Module path and dependencies (cc-connect plus indirect messaging SDKs) are declared; Available() returns sorted platform names from blank-import-registered platforms, tested for expected set and reply-context reconstruction capability.
Session pointer persistence and conflict handling
packages/remote-bridge/internal/bridge/session_pointers.go, packages/remote-bridge/internal/bridge/session_pointers_test.go
NewMemorySessionPointers() and NewFileSessionPointers(path) store remoteKey→sessionID and sessionID→parentID with root-binding conflict detection (rejecting updates that bind multiple remote keys to the same root), event cursor tracking, and atomic JSON persist via temp-file rename; tests verify duplicate-root rejection, ambiguous-reload guarding, event cursor persistence, and concurrent-write safety.
Bridge engine contracts, blocker flow, and delivery
packages/remote-bridge/internal/bridge/engine.go
Exports data model types (Session, PendingPermission, PendingQuestion, Question, QuestionOption, etc.) and Sidecar/SessionPointers/EventCursorStore interfaces; Engine.HandleMessage routes /new, /sessions, /stop, /help commands, resolves pending permission/question blockers in insertion order with delivery tracking, forwards ordinary prompts to the sidecar, manages active delivery targets per session, and supports delivery restoration via core.ReplyContextReconstructor.
Bridge engine behavioral tests
packages/remote-bridge/internal/bridge/engine_test.go
fakeSidecar and fakePlatform test doubles validate session lifecycle (new vs continue), command routing, permission/question blocker arrival order and resolution, delivery target switching and restoration, session switching validation across different remote roots, resolved-event state clearing, prompt retry tolerance with bounded backoff, and reply-target reconstruction with proactive context across restarts.
PawWork HTTP client and API
packages/remote-bridge/internal/pawwork/client.go, packages/remote-bridge/internal/pawwork/client_test.go
HTTPStatusError with fatal-stream classification; Client constructors with optional auth/directory; session operations (CreateSession, SendPrompt, ListSessions, AbortSession); pending-interaction operations (ReplyPermission, SubmitQuestion, ListPermissions, ListQuestions) with directory-aware x-opencode-directory headers, session→directory caching, and per-directory skip logic (502 BadGateway skipped with warning; other errors fatal); tests cover endpoints, auth header emission, timeout behavior, cursor persistence across restarts, SSE Last-Event-ID reconnect, replay-refresh error retention, and query parameter escaping.
SSE event streaming and dispatch
packages/remote-bridge/internal/pawwork/events.go, packages/remote-bridge/internal/pawwork/events_test.go
EventHandler, ReplayRefreshHandler, StreamReadyHandler interfaces; StreamEvents SSE connection with Last-Event-ID header, text/event-stream content-type validation, and reconnect-aware replay refresh gating; DispatchEvent routes message.part.updated (assistant text, ignoring reasoning), permission.asked/replied, external-result (tool questions with pending/resolved classification), and session.created events; parseSSE scanner persists last-event-id, propagates only replay-refresh errors, and triggers reconciliation for undecodable critical events; tests cover all event types, non-SSE rejection, handler error continuation, large payload integrity, external-result-ready gating, and incomplete-event reconciliation.
Gateway config, app lifecycle, and hydration
packages/remote-bridge/internal/gateway/gateway.go, packages/remote-bridge/internal/gateway/gateway_test.go
Config and PlatformConfig with LoadConfig/DecodeConfig; New validates audience options per platform (generic allow_from wildcard rejection vs feishu/lark-specific allow_chat+group_only requirement); App.Run starts event-stream goroutine with exponential-backoff reconnect retry after fixed delay, gates platform startup on stream-ready signal, hydrates sessions/permissions/questions with bounded session limit (warn-and-continue per item; fail-fast on list errors), starts all platforms concurrently with shared inbound-message handler, blocks until cancellation or fatal error; tests verify startup ordering (stream before platforms, before hydrate), transient retry, replay-gap Last-Event-ID, hydration failure tolerance, and engine error logging.
Remote-bridge CLI entrypoint
packages/remote-bridge/cmd/pawwork-remote-bridge/main.go
CLI parses -config and -list-platforms flags, outputs JSON platform list on -list-platforms, loads config from stdin (-config=-) or file path, creates signal-aware cancellable context (SIGINT, SIGTERM), runs gateway via gateway.Run, and exits with stderr and code 1 on error.
CI workflow Go test integration
.github/workflows/build.yml, packages/opencode/test/github/build-workflow.test.ts
build-electron job now sets up Go toolchain keyed on packages/remote-bridge/go.mod and go.sum; adds standard go test ./... and race-detector go test -race ./internal/bridge ./internal/gateway ./internal/pawwork steps in packages/remote-bridge, both conditional on non-finalize phase; workflow test validates step presence, conditions, and command structure.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as pawwork-remote-bridge
    participant Gateway as gateway.App.Run
    participant EventStream as pawwork.Client.StreamEvents
    participant Engine as bridge.Engine
    participant Sidecar as PawWork Sidecar
    participant Platform as registered platforms

    CLI->>Gateway: Run(ctx, config)
    Gateway->>EventStream: goroutine StreamEvents(ctx, replayHandler)
    EventStream-->>Sidecar: GET /global/event with Last-Event-ID
    EventStream-->>Gateway: handleStreamReady() → unblock hydrate
    Gateway->>Engine: hydrate sessions (ListSessions)
    Gateway->>Engine: hydrate permissions (ListPermissions)
    Gateway->>Engine: hydrate questions (ListQuestions)
    par Start all registered platforms
        Gateway->>Platform: Start(ctx, messageHandler)
    end
    
    loop until ctx cancel or fatal error
        Sidecar-->>EventStream: SSE events (text, permissions, questions, sessions)
        EventStream->>Engine: DispatchEvent / HandleAssistantText / HandlePermission / HandleQuestion / HandleSession
        Platform-->>Engine: inbound message → HandleMessage
        Engine-->>Platform: Reply/Send (permission/question prompt, answer, assistant text)
        Engine-->>Sidecar: SendPrompt / ReplyPermission / SubmitQuestion
    end
    
    Gateway->>Platform: stopPlatforms()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Suggested labels

task

Poem

🐇 A bridge built with channels and care,
Sessions and blockers flow everywhere—
Permissions asked, answers awaited with grace,
SSE streams at a steady, reliable pace.
From Discord to Feishu, the platforms all blend,
The rabbit's remote bridge shall never end! 🌉✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.60% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly identifies the main change: adding a standalone Go chat companion bridge module.
Description check ✅ Passed The PR description is comprehensive and covers all required template sections with detailed information about changes, rationale, linked issue, review focus, and risks.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 codex/i1188-remote-gateway

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


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.

@github-actions github-actions Bot added ui Design system and user interface harness Model harness, prompts, tool descriptions, and session mechanics labels Jun 12, 2026

@github-actions github-actions 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.

Suggested priority: P2 (includes user-path files (packages/app/src/app.tsx, packages/app/src/desktop-api-contract.ts, packages/app/src/desktop-api.ts, packages/app/src/i18n/en.ts, packages/app/src/i18n/zh.ts, packages/app/src/pages/settings/remote-source.test.ts, packages/app/src/pages/settings/remote.tsx, packages/app/src/pages/settings/settings-shell.tsx, packages/desktop-electron/src/main/index.ts, packages/desktop-electron/src/main/ipc.ts, packages/desktop-electron/src/main/remote-access-ipc-source.test.ts, packages/desktop-electron/src/main/remote-bridge.test.ts, packages/desktop-electron/src/main/remote-bridge.ts, packages/desktop-electron/src/preload/index.ts, packages/desktop-electron/src/preload/types.ts)).

P1/P0 are reserved for maintainer confirmation. Please relabel manually if this is a release blocker, security issue, data-loss risk, or updater/runtime failure.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request implements a remote access bridge feature to connect chat platforms (such as Slack, Feishu, and Discord) to desktop PawWork sessions. It introduces a Go-based remote bridge binary, integrates a controller and IPC handlers in the Electron main process, and adds a dedicated 'Remote access' settings page in the frontend. The code review feedback highlights several key improvement opportunities: in the Go bridge, only known commands starting with / should be intercepted to prevent blocking normal prompts like file paths; in the frontend, SolidJS state management can be simplified by utilizing the built-in mutate function from createResource; and in the Go client, error handling during permission and question listing should be made more resilient by logging warnings and continuing rather than aborting on a single directory failure.

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 packages/remote-bridge/internal/bridge/engine.go Outdated
Comment thread packages/app/src/pages/settings/remote.tsx Outdated
Comment thread packages/app/src/pages/settings/remote.tsx Outdated
Comment thread packages/app/src/pages/settings/remote.tsx Outdated
Comment thread packages/app/src/pages/settings/remote.tsx Outdated
Comment thread packages/app/src/pages/settings/remote.tsx Outdated
Comment thread packages/remote-bridge/internal/pawwork/client.go
Comment thread packages/remote-bridge/internal/pawwork/client.go

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 10

Caution

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

⚠️ Outside diff range comments (1)
packages/desktop-electron/src/main/index.ts (1)

698-703: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Await remote bridge shutdown on quit/relaunch paths.

remoteAccess.stop() is async, but killSidecar() drops the promise. All of the quit/install/relaunch/signal handlers call this helper and then continue exiting immediately, so the Electron process can terminate before the bridge handles SIGTERM. That can leave remote access running after the desktop app closes.

🤖 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 `@packages/desktop-electron/src/main/index.ts` around lines 698 - 703,
killSidecar currently calls remoteAccess.stop() without awaiting it, allowing
the process to exit before the bridge shuts down; make killSidecar async, await
remoteAccess.stop(), and still stop the server (await server.stop(true) if it
returns a promise) and null it; update all callers (quit/install/relaunch/signal
handlers) to await killSidecar() so the Electron process waits for shutdown, and
wrap awaits in a try/catch (or timeout) to avoid hanging if shutdown fails.
🧹 Nitpick comments (1)
.github/workflows/build.yml (1)

281-284: ⚡ Quick win

Run the Go race suite here too.

Line 283 only runs go test ./..., so CI still misses the race coverage for the new remote-bridge concurrency paths. Add a separate go test -race ./... step here, or make this step include it, so the workflow matches the PR’s stated Go unit/race verification scope.

🧪 Suggested CI update
       - name: Test remote bridge
         if: ${{ inputs.phase != 'finalize' }}
         run: go test ./...
         working-directory: packages/remote-bridge
+
+      - name: Race test remote bridge
+        if: ${{ inputs.phase != 'finalize' }}
+        run: go test -race ./...
+        working-directory: packages/remote-bridge
🤖 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 @.github/workflows/build.yml around lines 281 - 284, The CI step named "Test
remote bridge" currently runs "go test ./..." in the packages/remote-bridge
working-directory but omits the race detector; update this step to also execute
the race-enabled tests (either replace the run command with a sequence that runs
both "go test ./..." and "go test -race ./..." or add a new step that runs "go
test -race ./..." with the same name/working-directory) so the remote-bridge
concurrency paths are covered by the Go race suite.
🤖 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 `@packages/app/src/pages/settings/remote.tsx`:
- Around line 107-114: The start() handler persists the config with enabled:
true but doesn't update the local enabled() signal, so the UI can still show
disabled; after a successful remoteAccessStart (where next is returned) or
whenever start forces enabled: true, set the local enabled() signal to true
(e.g. call enabled(true)) and ensure the form state/mutate(next) reflects that
change before calling refetch() so the checkbox stays in sync with the running
bridge.

In `@packages/app/src/pages/settings/settings-shell.tsx`:
- Around line 19-21: The remote-access tab is rendered even when the Electron
preload bridge is absent; update TAB_VALUES (and any tab-rendering logic in this
file, e.g., the tab list at lines ~27-35 and the RemotePage rendering at
~201-203) to only include "remoteAccess" when the preload bridge exists (check
window.api?.remoteAccess or a central isDesktop/isBridgeAvailable flag).
Specifically, change the static TAB_VALUES array to be built conditionally
(e.g., baseTabs plus remoteAccess if window.api?.remoteAccess) and guard
rendering of RemotePage so that if the bridge is missing you either hide the tab
or render an explicit unsupported-state panel instead of relying on
optional-chained no-ops; reference SettingsTab, TAB_VALUES, and RemotePage when
applying the changes.

In `@packages/desktop-electron/src/main/index.ts`:
- Around line 206-214: serverReady.promise is being resolved before the
sidecar's health.wait completes, allowing autoStartRemoteAccess (and IPC callers
that rely on serverReady.promise) to call remoteAccess.start against a
still-booting sidecar; update the logic so the "safe to talk to PawWork" gate
waits for health.wait to succeed. Concretely: either move the resolution of
serverReady.promise inside initialize() to occur only after health.wait()
completes, or change autoStartRemoteAccess and the IPC callers to await
health.wait() (or a new serverHealthy promise) before calling
remoteAccess.start/IPC methods; reference the functions serverReady.promise,
initialize(), health.wait(), autoStartRemoteAccess(), and remoteAccess.start()
when making the change. Ensure errors from health.wait propagate to the existing
catch so startup failures are logged consistently.

In `@packages/desktop-electron/src/main/remote-bridge.ts`:
- Around line 221-226: In readUserConfig, don't blanket-catch all errors; only
return defaultConfig when the readFile/JSON.parse failure is due to a missing
file (ENOENT) and rethrow or propagate other errors (malformed JSON,
permission/I/O). Update the catch to inspect the thrown error (from
readFile/JSON.parse), if err.code === 'ENOENT' return defaultConfig otherwise
throw the error so callers can surface the failure; keep usage of
normalizeConfig and defaultConfig intact and ensure TypeScript error typing/null
checks as needed.

In `@packages/remote-bridge/internal/bridge/engine.go`:
- Around line 563-567: setCurrent(...) updates the pointer store but doesn't
populate the in-memory delivery pointer e.active, causing
HandleAssistantText/replyToActive to fall back or drop messages; after
successfully calling e.setCurrent(sessionID, ...) in both the `/new` and
`/sessions N` branches, call e.setActive(sessionID, platform, msg.ReplyCtx) so
the in-memory delivery is seeded immediately; update the success paths around
setCurrent in engine.go (the blocks that currently return "Started a new PawWork
session." and the similar block at the other location) to invoke
e.setActive(...) right after the successful setCurrent call.

In `@packages/remote-bridge/internal/bridge/session_pointers.go`:
- Around line 47-56: RemoteKeyForSession currently scans p.sessions and returns
the first remoteKey whose stored session shares the same root, which is
nondeterministic and can restore replies to the wrong chat; fix by maintaining
an explicit, locked inverse map (e.g., rootToRemoteKey) inside
MemorySessionPointers that is updated whenever session mappings change (the same
places that call Set/Unset/Bind), then change RemoteKeyForSession to look up the
remote key directly from rootToRemoteKey (use p.rootLocked as needed) and, when
detecting conflicting bindings for the same root, either reject the second
binding at bind time or surface/log an error so engine.restoreDelivery cannot
pick an arbitrary map iteration result.

In `@packages/remote-bridge/internal/gateway/gateway.go`:
- Around line 133-170: The bootstrap currently calls hydrate() before the SSE
subscription, so events that occur between hydrate() and the first StreamEvents
connection are missed; fix by starting the StreamEvents goroutine (the one that
runs handler := replayRefreshHandler{...} and calls a.client.StreamEvents) and
block on the streamReady channel before calling a.hydrate; alternatively, ensure
replayRefreshHandler signals streamReady as soon as the first
subscription/connection is established (even if reconnecting is false) so that
hydrate() runs only after the SSE subscription is live; update references to
hydrate, StreamEvents, replayRefreshHandler, streamReady and streamReadyOnce to
implement this ordering change.
- Around line 191-194: The current App.messageHandler swallows errors from
a.engine.HandleMessage; change it to capture the returned error and handle it
(e.g., log the failure with context and the error, emit a metric, and/or enqueue
a retry). Concretely, in function App.messageHandler wrap the call to
a.engine.HandleMessage(ctx, platform, msg) in an if err := ...; err != nil { ...
} block, invoke the service logger (e.g., a.logger.Errorf or similar) including
platform and message identifiers and err, and optionally increment a failure
metric or requeue the message for retry so inbound messages are not silently
dropped.

In `@packages/remote-bridge/internal/pawwork/client.go`:
- Around line 184-186: The loop currently swallows all errors from
c.doJSONWithDirectory while hydrating pending interactions; change it to only
continue on transient errors (network timeouts, temporary 5xx responses) and
return or propagate non-transient/fatal errors (401, 403, 404, etc.) so restart
recovery can't silently drop data. Modify the calls to c.doJSONWithDirectory in
the pending-interaction hydration loop (the block using http.MethodGet
"/permission" and the similar block at the other occurrence) to inspect the
returned error or response status, implement a small helper (e.g.,
isTransientError) that checks for transient network errors or 5xx status codes,
and only continue on those cases — otherwise return the error up the call stack
immediately.

In `@packages/remote-bridge/internal/pawwork/events.go`:
- Around line 105-120: The code calls HandleStreamReady on handler whenever
res.StatusCode is 2xx without validating the response is actually an SSE stream;
change the flow in the function that currently calls
StreamReadyHandler.HandleStreamReady and parseSSE to first validate the response
Content-Type (res.Header.Get("Content-Type")) contains "text/event-stream" (or
"text/event-stream;") and only then invoke HandleStreamReady and parseSSE; if
the content-type is absent or not an SSE type, return an HTTPStatusError (or a
new descriptive error) similar to the existing non-2xx branch so callers don’t
treat non-SSE bodies as ready. Ensure you reference handler, StreamReadyHandler,
HandleStreamReady, parseSSE, and res in the updated logic.

---

Outside diff comments:
In `@packages/desktop-electron/src/main/index.ts`:
- Around line 698-703: killSidecar currently calls remoteAccess.stop() without
awaiting it, allowing the process to exit before the bridge shuts down; make
killSidecar async, await remoteAccess.stop(), and still stop the server (await
server.stop(true) if it returns a promise) and null it; update all callers
(quit/install/relaunch/signal handlers) to await killSidecar() so the Electron
process waits for shutdown, and wrap awaits in a try/catch (or timeout) to avoid
hanging if shutdown fails.

---

Nitpick comments:
In @.github/workflows/build.yml:
- Around line 281-284: The CI step named "Test remote bridge" currently runs "go
test ./..." in the packages/remote-bridge working-directory but omits the race
detector; update this step to also execute the race-enabled tests (either
replace the run command with a sequence that runs both "go test ./..." and "go
test -race ./..." or add a new step that runs "go test -race ./..." with the
same name/working-directory) so the remote-bridge concurrency paths are covered
by the Go race suite.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9e3b0bb1-0d28-4559-918e-d63b95ea9b96

📥 Commits

Reviewing files that changed from the base of the PR and between 6db4738 and a2641c1.

⛔ Files ignored due to path filters (1)
  • packages/remote-bridge/go.sum is excluded by !**/*.sum
📒 Files selected for processing (37)
  • .github/workflows/build.yml
  • .github/workflows/desktop-smoke.yml
  • packages/app/e2e/snap/settings-shell.snap.ts
  • packages/app/src/app.tsx
  • packages/app/src/desktop-api-contract.ts
  • packages/app/src/desktop-api.ts
  • packages/app/src/i18n/en.ts
  • packages/app/src/i18n/zh.ts
  • packages/app/src/pages/settings/remote-behavior.test.ts
  • packages/app/src/pages/settings/remote-source.test.ts
  • packages/app/src/pages/settings/remote.tsx
  • packages/app/src/pages/settings/settings-shell.tsx
  • packages/desktop-electron/scripts/build-remote-bridge.ts
  • packages/desktop-electron/scripts/desktop-smoke-workflow-contract.test.ts
  • packages/desktop-electron/scripts/prebuild.ts
  • packages/desktop-electron/src/main/index.ts
  • packages/desktop-electron/src/main/ipc.ts
  • packages/desktop-electron/src/main/remote-access-ipc-source.test.ts
  • packages/desktop-electron/src/main/remote-bridge.test.ts
  • packages/desktop-electron/src/main/remote-bridge.ts
  • packages/desktop-electron/src/preload/index.ts
  • packages/desktop-electron/src/preload/types.ts
  • packages/opencode/src/server/event-replay.test.ts
  • packages/opencode/src/server/event-replay.ts
  • packages/remote-bridge/cmd/pawwork-remote-bridge/main.go
  • packages/remote-bridge/go.mod
  • packages/remote-bridge/internal/bridge/engine.go
  • packages/remote-bridge/internal/bridge/engine_test.go
  • packages/remote-bridge/internal/bridge/session_pointers.go
  • packages/remote-bridge/internal/gateway/gateway.go
  • packages/remote-bridge/internal/gateway/gateway_test.go
  • packages/remote-bridge/internal/pawwork/client.go
  • packages/remote-bridge/internal/pawwork/client_test.go
  • packages/remote-bridge/internal/pawwork/events.go
  • packages/remote-bridge/internal/pawwork/events_test.go
  • packages/remote-bridge/internal/platforms/platforms.go
  • packages/remote-bridge/internal/platforms/platforms_test.go

Comment thread packages/app/src/pages/settings/remote.tsx Outdated
Comment thread packages/app/src/pages/settings/settings-shell.tsx Outdated
Comment thread packages/desktop-electron/src/main/index.ts Outdated
Comment thread packages/desktop-electron/src/main/remote-bridge.ts Outdated
Comment thread packages/remote-bridge/internal/bridge/engine.go Outdated
Comment thread packages/remote-bridge/internal/bridge/session_pointers.go Outdated
Comment thread packages/remote-bridge/internal/gateway/gateway.go Outdated
Comment thread packages/remote-bridge/internal/gateway/gateway.go
Comment thread packages/remote-bridge/internal/pawwork/client.go
Comment thread packages/remote-bridge/internal/pawwork/events.go

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (2)
packages/remote-bridge/internal/bridge/session_pointers.go (1)

48-51: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject parent cycles before committing parent links.

SetParent can currently create s1 -> s2 -> s1; after that, rootSession returns the starting session for each node, so duplicate-root checks can be bypassed and two remote conversations can bind into the same cyclic parent component.

Proposed fix
 func (p *MemorySessionPointers) SetParent(sessionID string, parentID string) error {
 	if sessionID == "" || parentID == "" {
 		return nil
 	}
 	p.mu.Lock()
 	defer p.mu.Unlock()
-	if hasAnyRootConflict(p.sessions, withParent(p.parents, sessionID, parentID)) {
+	nextParents := withParent(p.parents, sessionID, parentID)
+	if hasParentCycle(nextParents, sessionID) {
+		return fmt.Errorf("session parent relationship would create a cycle")
+	}
+	if hasAnyRootConflict(p.sessions, nextParents) {
 		return fmt.Errorf("session root is already bound to another remote conversation")
 	}
-	p.parents[sessionID] = parentID
+	p.parents = nextParents
 	return nil
 }
 
 func (p *FileSessionPointers) SetParent(sessionID string, parentID string) error {
 	if sessionID == "" || parentID == "" {
 		return nil
 	}
 	p.mu.Lock()
 	defer p.mu.Unlock()
-	if hasAnyRootConflict(p.sessions, withParent(p.parents, sessionID, parentID)) {
+	nextParents := withParent(p.parents, sessionID, parentID)
+	if hasParentCycle(nextParents, sessionID) {
+		return fmt.Errorf("session parent relationship would create a cycle")
+	}
+	if hasAnyRootConflict(p.sessions, nextParents) {
 		return fmt.Errorf("session root is already bound to another remote conversation")
 	}
-	p.parents[sessionID] = parentID
+	p.parents = nextParents
 	return p.saveLocked()
 }
+
+func hasParentCycle(parents map[string]string, sessionID string) bool {
+	seen := map[string]bool{}
+	current := sessionID
+	for current != "" {
+		if seen[current] {
+			return true
+		}
+		seen[current] = true
+		current = parents[current]
+	}
+	return false
+}

Also applies to: 166-170

🤖 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 `@packages/remote-bridge/internal/bridge/session_pointers.go` around lines 48 -
51, The SetParent function in
packages/remote-bridge/internal/bridge/session_pointers.go does not validate
that adding a parent link would not create a cycle before committing the change
to p.parents[sessionID] = parentID. Add cycle detection logic after the
hasAnyRootConflict check but before assigning the parent link to ensure that
parentID is not already a descendant of sessionID (which would create a cycle).
This check needs to be applied at both occurrences of the parent assignment: the
primary location around lines 48-51 in the SetParent function and the secondary
location around lines 166-170, ensuring parent cycles are rejected before they
are stored in the parents map.
packages/app/src/pages/settings/remote-behavior.test.ts (1)

167-170: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Strengthen the post-stop status assertion.

assert(statusCalls > 0) is already satisfied by initial page load, so this won’t catch regressions in stop-path status refresh behavior.

Suggested patch
+const callsBeforeStop = statusCalls
 root.querySelector('[data-action="settings-remote-stop"]').click()
 await waitFor(() => stopped === 1, "stop should call desktop API")
-assert(statusCalls > 0, "settings should refresh bridge status through desktop API")
+assert(statusCalls > callsBeforeStop, "stop flow should trigger a fresh status read")
🤖 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 `@packages/app/src/pages/settings/remote-behavior.test.ts` around lines 167 -
170, The assertion `assert(statusCalls > 0)` after the stop action click is
insufficient because the statusCalls counter is already incremented by the
initial page load, so this assertion will pass regardless of whether the
stop-path actually triggers a status refresh. Replace this assertion with a
stronger check that captures the statusCalls value before clicking the stop
button, then asserts that statusCalls has increased after the stop action is
clicked and the waitFor condition completes. This ensures the test catches
regressions where the stop-path fails to refresh the bridge status.
♻️ Duplicate comments (1)
packages/app/src/pages/settings/remote.tsx (1)

185-185: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a safe fallback when mapping status to translation keys.

If backend state ever includes an unexpected value, STATUS_LABELS[state()] becomes undefined, and language.t(...) gets an invalid key.

Suggested patch
-  const statusLabel = createMemo(() => language.t(STATUS_LABELS[state()]))
+  const statusLabel = createMemo(() => {
+    const key = STATUS_LABELS[state() as keyof typeof STATUS_LABELS] ?? STATUS_LABELS.idle
+    return language.t(key)
+  })
🤖 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 `@packages/app/src/pages/settings/remote.tsx` at line 185, The statusLabel
createMemo accesses STATUS_LABELS using state() as a key without validation,
which results in passing undefined to language.t() if state() contains an
unexpected value. Add a fallback mechanism to the statusLabel createMemo that
checks if STATUS_LABELS[state()] exists and provides a sensible default
translation key (such as a fallback status key or a default "unknown" key) when
the state value is not found in the STATUS_LABELS mapping. This ensures
language.t() always receives a valid key regardless of what state() returns.
🤖 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 `@packages/opencode/test/github/build-workflow.test.ts`:
- Around line 197-203: Add a missing assertion to validate the if condition on
the remoteBridgeTestStep. After the existing assertions on remoteBridgeTestStep
(which validate run and working-directory), add an expectation that
remoteBridgeTestStep?.if equals "${{ inputs.phase != 'finalize' }}" to ensure
consistency with the race test step and prevent regressions if this condition is
removed from the actual workflow.

---

Outside diff comments:
In `@packages/app/src/pages/settings/remote-behavior.test.ts`:
- Around line 167-170: The assertion `assert(statusCalls > 0)` after the stop
action click is insufficient because the statusCalls counter is already
incremented by the initial page load, so this assertion will pass regardless of
whether the stop-path actually triggers a status refresh. Replace this assertion
with a stronger check that captures the statusCalls value before clicking the
stop button, then asserts that statusCalls has increased after the stop action
is clicked and the waitFor condition completes. This ensures the test catches
regressions where the stop-path fails to refresh the bridge status.

In `@packages/remote-bridge/internal/bridge/session_pointers.go`:
- Around line 48-51: The SetParent function in
packages/remote-bridge/internal/bridge/session_pointers.go does not validate
that adding a parent link would not create a cycle before committing the change
to p.parents[sessionID] = parentID. Add cycle detection logic after the
hasAnyRootConflict check but before assigning the parent link to ensure that
parentID is not already a descendant of sessionID (which would create a cycle).
This check needs to be applied at both occurrences of the parent assignment: the
primary location around lines 48-51 in the SetParent function and the secondary
location around lines 166-170, ensuring parent cycles are rejected before they
are stored in the parents map.

---

Duplicate comments:
In `@packages/app/src/pages/settings/remote.tsx`:
- Line 185: The statusLabel createMemo accesses STATUS_LABELS using state() as a
key without validation, which results in passing undefined to language.t() if
state() contains an unexpected value. Add a fallback mechanism to the
statusLabel createMemo that checks if STATUS_LABELS[state()] exists and provides
a sensible default translation key (such as a fallback status key or a default
"unknown" key) when the state value is not found in the STATUS_LABELS mapping.
This ensures language.t() always receives a valid key regardless of what state()
returns.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8d86ae67-9407-4e42-9a7a-63ebcd69f5cf

📥 Commits

Reviewing files that changed from the base of the PR and between 691bb3c and 8e4e6dd.

📒 Files selected for processing (22)
  • .github/workflows/build.yml
  • .github/workflows/desktop-smoke.yml
  • packages/app/src/i18n/en.ts
  • packages/app/src/i18n/zh.ts
  • packages/app/src/pages/settings/remote-behavior.test.ts
  • packages/app/src/pages/settings/remote-platform-icons.ts
  • packages/app/src/pages/settings/remote-platforms.tsx
  • packages/app/src/pages/settings/remote.tsx
  • packages/app/src/pages/settings/settings-shell.tsx
  • packages/desktop-electron/scripts/predev.ts
  • packages/desktop-electron/src/main/remote-bridge.test.ts
  • packages/desktop-electron/src/main/remote-bridge.ts
  • packages/opencode/test/github/build-workflow.test.ts
  • packages/opencode/test/github/desktop-smoke-workflow.test.ts
  • packages/remote-bridge/cmd/pawwork-remote-bridge/main.go
  • packages/remote-bridge/internal/bridge/engine.go
  • packages/remote-bridge/internal/bridge/session_pointers.go
  • packages/remote-bridge/internal/bridge/session_pointers_test.go
  • packages/remote-bridge/internal/gateway/gateway.go
  • packages/remote-bridge/internal/pawwork/client.go
  • packages/remote-bridge/internal/pawwork/client_test.go
  • packages/remote-bridge/internal/pawwork/events.go
✅ Files skipped from review due to trivial changes (1)
  • packages/app/src/pages/settings/remote-platform-icons.ts
🚧 Files skipped from review as they are similar to previous changes (10)
  • .github/workflows/build.yml
  • packages/remote-bridge/internal/pawwork/client.go
  • packages/remote-bridge/internal/gateway/gateway.go
  • .github/workflows/desktop-smoke.yml
  • packages/remote-bridge/internal/pawwork/client_test.go
  • packages/desktop-electron/src/main/remote-bridge.test.ts
  • packages/desktop-electron/src/main/remote-bridge.ts
  • packages/remote-bridge/internal/bridge/engine.go
  • packages/remote-bridge/internal/pawwork/events.go
  • packages/app/src/pages/settings/settings-shell.tsx

Comment thread packages/opencode/test/github/build-workflow.test.ts Outdated
Self-contained Go module (own go.mod, no app/electron deps) wrapping
github.com/chenhg5/cc-connect platform adapters: bridge engine, gateway,
pawwork client + event stream, session pointers, platform registry.
~51% tests. Split out of #1275 as PR 1/2 for reviewability.
@Astro-Han Astro-Han force-pushed the codex/i1188-remote-gateway branch from 8e4e6dd to 61ad3b5 Compare June 15, 2026 10:21
@Astro-Han Astro-Han changed the title feat(remote-access): add chat companion bridge feat(remote-bridge): standalone Go chat companion bridge (1/2) Jun 15, 2026
@github-actions github-actions Bot removed app Application behavior and product flows ui Design system and user interface platform Electron shell, OS integration, packaging, updater, signing, paths, and permissions harness Model harness, prompts, tool descriptions, and session mechanics labels Jun 15, 2026

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (2)
packages/remote-bridge/internal/pawwork/events_test.go (2)

315-406: ⚡ Quick win

Add positive test case for HandleStreamReady invocation.

The test suite includes TestClientRejectsNonSSEEventStreamBeforeReady, which verifies that HandleStreamReady is NOT called when the content-type is incorrect. However, there's no positive test verifying that HandleStreamReady IS called when:

  1. The content-type is text/event-stream, and
  2. The handler implements StreamReadyHandler.

Consider adding a test case that uses streamReadyEventHandler with a valid SSE response and asserts that handler.ready == 1 after StreamEvents completes successfully.

🤖 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 `@packages/remote-bridge/internal/pawwork/events_test.go` around lines 315 -
406, Add a new positive test case to verify that HandleStreamReady is called
when the server returns a valid SSE response. Create a test function that sets
up an httptest server returning content-type text/event-stream with valid SSE
data (similar to TestClientStreamsGlobalEvents), uses a streamReadyEventHandler
instance, calls New(server.URL).StreamEvents(t.Context(), handler), and asserts
that handler.ready equals 1 after successful completion, contrasting with the
existing TestClientRejectsNonSSEEventStreamBeforeReady which verifies the
handler.ready remains 0 when content-type is invalid.

80-313: Add test coverage for server.connected event.

The DispatchEvent tests cover most event types, but the server.connected event type is implemented in events.go (line 141-146) to trigger HandleReplayRefresh. A test case for this event routing is missing.

🤖 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 `@packages/remote-bridge/internal/pawwork/events_test.go` around lines 80 -
313, Add a new test function following the existing pattern in events_test.go to
cover the server.connected event routing. Create a test that calls DispatchEvent
with a JSON payload containing the server.connected event type and verify that
it triggers the HandleReplayRefresh callback on the fakeEventHandler. This test
should follow the same structure as the other TestDispatchEvent* functions,
checking that the handler properly processes the connected event.
🤖 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.

Nitpick comments:
In `@packages/remote-bridge/internal/pawwork/events_test.go`:
- Around line 315-406: Add a new positive test case to verify that
HandleStreamReady is called when the server returns a valid SSE response. Create
a test function that sets up an httptest server returning content-type
text/event-stream with valid SSE data (similar to
TestClientStreamsGlobalEvents), uses a streamReadyEventHandler instance, calls
New(server.URL).StreamEvents(t.Context(), handler), and asserts that
handler.ready equals 1 after successful completion, contrasting with the
existing TestClientRejectsNonSSEEventStreamBeforeReady which verifies the
handler.ready remains 0 when content-type is invalid.
- Around line 80-313: Add a new test function following the existing pattern in
events_test.go to cover the server.connected event routing. Create a test that
calls DispatchEvent with a JSON payload containing the server.connected event
type and verify that it triggers the HandleReplayRefresh callback on the
fakeEventHandler. This test should follow the same structure as the other
TestDispatchEvent* functions, checking that the handler properly processes the
connected event.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 533f5ee9-80dc-4122-be07-a5186165912a

📥 Commits

Reviewing files that changed from the base of the PR and between 8e4e6dd and 61ad3b5.

⛔ Files ignored due to path filters (1)
  • packages/remote-bridge/go.sum is excluded by !**/*.sum
📒 Files selected for processing (15)
  • .github/workflows/build.yml
  • packages/remote-bridge/cmd/pawwork-remote-bridge/main.go
  • packages/remote-bridge/go.mod
  • packages/remote-bridge/internal/bridge/engine.go
  • packages/remote-bridge/internal/bridge/engine_test.go
  • packages/remote-bridge/internal/bridge/session_pointers.go
  • packages/remote-bridge/internal/bridge/session_pointers_test.go
  • packages/remote-bridge/internal/gateway/gateway.go
  • packages/remote-bridge/internal/gateway/gateway_test.go
  • packages/remote-bridge/internal/pawwork/client.go
  • packages/remote-bridge/internal/pawwork/client_test.go
  • packages/remote-bridge/internal/pawwork/events.go
  • packages/remote-bridge/internal/pawwork/events_test.go
  • packages/remote-bridge/internal/platforms/platforms.go
  • packages/remote-bridge/internal/platforms/platforms_test.go
🚧 Files skipped from review as they are similar to previous changes (14)
  • packages/remote-bridge/internal/platforms/platforms_test.go
  • packages/remote-bridge/internal/platforms/platforms.go
  • packages/remote-bridge/internal/bridge/session_pointers_test.go
  • packages/remote-bridge/go.mod
  • .github/workflows/build.yml
  • packages/remote-bridge/cmd/pawwork-remote-bridge/main.go
  • packages/remote-bridge/internal/pawwork/events.go
  • packages/remote-bridge/internal/bridge/session_pointers.go
  • packages/remote-bridge/internal/gateway/gateway.go
  • packages/remote-bridge/internal/bridge/engine_test.go
  • packages/remote-bridge/internal/gateway/gateway_test.go
  • packages/remote-bridge/internal/pawwork/client_test.go
  • packages/remote-bridge/internal/pawwork/client.go
  • packages/remote-bridge/internal/bridge/engine.go

Behavior-preserving simplifications surfaced by two independent reviews.
Full test suite + race detector green; per-package coverage unchanged.

- session_pointers: merge Memory/File dual impl into one store with an
  optional path (saveLocked is a no-op when path is empty)
- engine: collapse permissionOrder/questionOrder/blockerOrder into a single
  blockerOrder; derive oldest-of-kind by scanning it directly
- engine: drop the unreachable "Unknown command" branch; handleCommand now
  returns (handled, err) and isRemoteCommand is gone
- engine: inline the single-field Prompt wrapper to a plain string arg
- engine: slices.DeleteFunc for blocker filtering; inline single-use
  Handle*Resolved forwards and pendingBlocker alias locals
- gateway: defer stopPlatforms once instead of repeating it on every return
- client: drop the ListQuestions JSON marshal round-trip
- events: one flush closure for the parseSSE dispatch path

Net -187 lines.
Review P3. The question prompt only said "Reply with a number or answer
text", but the parser requires one line per question (multi-question) and
comma-separated numbers (multi-select) — users couldn't know the format.

- questionReplyHint: prompt now states the exact format per question shape
  (single / multi-select / multi-question), with an example
- accept full-width "," and ideographic "、" commas in multi-select replies
  so replies typed on a Chinese keyboard parse like ASCII ones
- tests for the per-shape hint text and the comma variants

Single-question prompt text unchanged.
Review P2. The client used http.DefaultClient (no timeout) and JSON calls
rode only the outer ctx, so a stalled PawWork sidecar could hang startup and
hydration indefinitely.

- doJSONWithDirectory (the single JSON funnel) wraps ctx with a 30s timeout
- the SSE stream builds its own request and stays intentionally unbounded
- test: a slow server that never responds makes ListSessions return promptly
Review P1. parseSSE advances the global SSE cursor even when a chat-platform
delivery fails, so a transient blip silently drops the assistant message. The
cursor is global and shared, so holding it (the reviewer's proposed replay)
would wedge every session's stream on any permanent failure.

First principle: the cursor tracks ingestion, not delivery. Keep it advancing;
make delivery best-effort-good by retrying at the sink.

- deliverWithRetry: assistant text gets bounded retries (3) with short backoff,
  then gives up (the caller logs it) — the cursor is never held
- scoped to assistant text only: permissions/questions are already reconciled
  by the gateway's hydrate on every reconnect, and a test pins their delivery
  as single-shot, so they must not be retried here
- test: transient failure recovers; sustained failure stops after N attempts

The global-cursor wedge risk was confirmed by an independent review.
@github-actions github-actions Bot added the harness Model harness, prompts, tool descriptions, and session mechanics label Jun 15, 2026

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/opencode/test/github/build-workflow.test.ts (1)

200-205: ⚡ Quick win

Consider validating the setup-go step for completeness.

The test validates setup-node (line 172) but doesn't validate the setup-go step, which is equally critical for the remote-bridge Go tests to run. Adding validation would:

  • Ensure go-version-file points to packages/remote-bridge/go.mod
  • Ensure cache-dependency-path points to packages/remote-bridge/go.sum
  • Prevent regressions in Go toolchain configuration
  • Maintain consistency with other setup step validations in this test
📋 Suggested additions

After line 105, add:

 const runtimeImportGuardStep = steps.find((step) => step.name === "Check desktop runtime imports")
+const setupGoStep = steps.find(
+  (step) => step.uses === "actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff",
+)
 const remoteBridgeTestStep = steps.find((step) => step.name === "Test remote bridge")

After line 199, add:

 expect(runtimeImportGuardStep?.["working-directory"]).toBe("packages/desktop-electron")
+expect(setupGoStep?.with).toEqual({
+  "go-version-file": "packages/remote-bridge/go.mod",
+  "cache-dependency-path": "packages/remote-bridge/go.sum",
+})
 expect(remoteBridgeTestStep?.if).toBe("${{ inputs.phase != 'finalize' }}")

Optionally, add step ordering assertions after line 205:

 expect(remoteBridgeRaceStep?.["working-directory"]).toBe("packages/remote-bridge")
+expect(steps.indexOf(remoteBridgeTestStep!)).toBeGreaterThan(steps.indexOf(setupGoStep!))
+expect(steps.indexOf(remoteBridgeRaceStep!)).toBeGreaterThan(steps.indexOf(setupGoStep!))
 expect(steps.indexOf(runtimeImportGuardStep!)).toBeGreaterThan(steps.indexOf(buildElectronAppStep!))
🤖 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 `@packages/opencode/test/github/build-workflow.test.ts` around lines 200 - 205,
The test is missing validation for the setup-go step even though it validates
setup-node, which is inconsistent. Add validation assertions for the setup-go
step to verify that go-version-file is set to packages/remote-bridge/go.mod and
cache-dependency-path is set to packages/remote-bridge/go.sum. This should be
added alongside the existing setup-node validations to ensure the Go toolchain
is properly configured for the remote-bridge tests, consistent with how
remoteBridgeTestStep and remoteBridgeRaceStep are currently validated.
Optionally add step ordering assertions to ensure setup-go runs before the test
steps.
🤖 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 `@packages/remote-bridge/internal/pawwork/client.go`:
- Around line 267-278: The canSkipHydrationDirectoryError function currently
skips all context.DeadlineExceeded errors indiscriminately, including when the
caller's own context has already expired, which causes hydration to incorrectly
return partial results after shutdown or upstream timeouts. Modify
canSkipHydrationDirectoryError to accept the caller's context as a parameter and
add a check that returns false if the caller's context is already done (by
calling ctx.Err()). This way, the function will distinguish between deadline
exceeded errors from the hydration operation itself versus timeouts caused by
the caller's context being expired, ensuring that caller-initiated cancellations
are not treated as skippable errors.

---

Nitpick comments:
In `@packages/opencode/test/github/build-workflow.test.ts`:
- Around line 200-205: The test is missing validation for the setup-go step even
though it validates setup-node, which is inconsistent. Add validation assertions
for the setup-go step to verify that go-version-file is set to
packages/remote-bridge/go.mod and cache-dependency-path is set to
packages/remote-bridge/go.sum. This should be added alongside the existing
setup-node validations to ensure the Go toolchain is properly configured for the
remote-bridge tests, consistent with how remoteBridgeTestStep and
remoteBridgeRaceStep are currently validated. Optionally add step ordering
assertions to ensure setup-go runs before the test steps.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b6f999bd-b0cd-45cc-9f04-6e8c7d6027bf

📥 Commits

Reviewing files that changed from the base of the PR and between 5c5cf77 and e1fa7b4.

📒 Files selected for processing (8)
  • packages/opencode/test/github/build-workflow.test.ts
  • packages/remote-bridge/internal/bridge/engine.go
  • packages/remote-bridge/internal/bridge/engine_test.go
  • packages/remote-bridge/internal/gateway/gateway_test.go
  • packages/remote-bridge/internal/pawwork/client.go
  • packages/remote-bridge/internal/pawwork/client_test.go
  • packages/remote-bridge/internal/pawwork/events.go
  • packages/remote-bridge/internal/pawwork/events_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/remote-bridge/internal/pawwork/client_test.go
  • packages/remote-bridge/internal/gateway/gateway_test.go
  • packages/remote-bridge/internal/bridge/engine_test.go
  • packages/remote-bridge/internal/pawwork/events_test.go

Comment thread packages/remote-bridge/internal/pawwork/client.go Outdated

@coderabbitai coderabbitai Bot 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.

Caution

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

⚠️ Outside diff range comments (1)
packages/remote-bridge/internal/bridge/engine.go (1)

802-808: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Potential key collision if channel or user IDs contain colons.

strings.Join(parts, ":") can produce identical keys for distinct entities when any component contains a colon. For example, platform "p" with channel "a:b" and user "c" collides with channel "a" and user "b:c". Colliding keys would cause unrelated users to share the same session pointer.

Consider using a non-printable separator (as questionKey does with \x00) or URL-encoding each component before joining.

Suggested fix using NUL separator
 func remoteKey(platform core.Platform, msg *core.Message) string {
 	if strings.TrimSpace(msg.SessionKey) != "" {
 		return msg.SessionKey
 	}
-	parts := []string{platform.Name(), msg.ChannelID, msg.UserID}
-	return strings.Join(parts, ":")
+	return platform.Name() + "\x00" + msg.ChannelID + "\x00" + msg.UserID
 }

Note: This requires updating restoreDelivery line 321 to use strings.Cut(remoteKey, "\x00").

🤖 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 `@packages/remote-bridge/internal/bridge/engine.go` around lines 802 - 808, The
remoteKey function uses a colon separator in strings.Join which can cause key
collisions if any of the component parts (platform name, channel ID, or user ID)
contain colons themselves. Replace the colon separator with a non-printable
separator like the NUL character \x00 (as already used in the questionKey
function). You must also update the restoreDelivery function to parse the
remoteKey using strings.Cut with the matching \x00 separator instead of its
current parsing approach.
🤖 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.

Outside diff comments:
In `@packages/remote-bridge/internal/bridge/engine.go`:
- Around line 802-808: The remoteKey function uses a colon separator in
strings.Join which can cause key collisions if any of the component parts
(platform name, channel ID, or user ID) contain colons themselves. Replace the
colon separator with a non-printable separator like the NUL character \x00 (as
already used in the questionKey function). You must also update the
restoreDelivery function to parse the remoteKey using strings.Cut with the
matching \x00 separator instead of its current parsing approach.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1f10e6d9-07aa-409b-89a5-73ab13a9212f

📥 Commits

Reviewing files that changed from the base of the PR and between e1fa7b4 and 7150a50.

📒 Files selected for processing (4)
  • packages/remote-bridge/internal/bridge/engine.go
  • packages/remote-bridge/internal/bridge/engine_test.go
  • packages/remote-bridge/internal/pawwork/client.go
  • packages/remote-bridge/internal/pawwork/client_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/remote-bridge/internal/pawwork/client_test.go
  • packages/remote-bridge/internal/pawwork/client.go
  • packages/remote-bridge/internal/bridge/engine_test.go

Astro-Han added 10 commits June 15, 2026 21:30
After answering the current blocker, handlePendingReply auto-surfaces the
next queued prompt. A persistently failing delivery dropped that prompt
and discarded the error, leaving no log signal until the next hydrate.
Propagate surfaceActiveBlocker's error up to HandleMessage so the
gateway's existing warn logging catches it.
When the next queued prompt failed to deliver, the blocker was dropped and
only recovered at the next reconnect hydrate. Keep it undelivered instead:
it stays unanswerable so it cannot intercept an ordinary message, and the
next inbound message retries surfacing it now that the user is reachable.
This deletes the drop/loop in surfaceActiveBlocker (the delivered flag
already prevents interception).
…ging the cursor

A permission/question/session event that failed to decode was logged and
skipped, recovered only at the next reconnect hydrate — an unbounded blind
window for a safety-critical confirmation on a healthy long-lived stream.
Mark such decode failures repairable; parseSSE advances the cursor past the
bad frame first (so a reconnect can never replay it and wedge the global
stream), then hydrates immediately to reconcile from the REST list endpoints.
Assistant text (no REST backstop) and resolved events (hydrate cannot clear
them) stay non-repairable.
Replace the single wildcard-Feishu case with a table covering the
hasRemoteAudience contract: specific audience accepted, wildcard/empty/blank
rejected, Feishu/Lark group_only both ways, allow_chat ignored for other
platforms. Also make the run-loop tests' request counters atomic — they were
written in the httptest handler goroutine and read in the test body, a
pre-existing data race that -race intermittently catches.
A permission.asked without id/sessionID, a session.created without info.id,
and a question tool event without sessionID/messageID/callID decoded fine but
were silently skipped, so their state waited for the next reconnect hydrate.
Treat these missing-field cases as repairable like a decode failure: advance
the cursor past the frame, then hydrate immediately. Rename the marker type
to repairableEventError since it now covers validation, not just decoding.
A bad SetParent that points a session's ancestry back at itself would
form a cycle. rootSession then returns the starting node instead of a
true root, letting the cycled session bypass the duplicate-root guard
and bind the wrong remote conversation on restore/delivery.

Walk the prospective parent chain before mutating; reject and leave
state unchanged when it already reaches the child.
hydrate fetched ListSessions(ctx, 0), which the client treats as
unbounded and pulls the full session history on every startup and
reconnect. Users with many sessions pay a slow warm-up.

Parent links are restored from the persisted pointer store and live
event replay, so the scan only needs the most recently active
sessions. Cap it at hydrateSessionLimit (100).
The picker cached the listed sessions and /sessions N reused that cache
whenever it was non-empty. A delayed pick then switched to a session
from a stale snapshot instead of the current recent list.

Drop the picker cache entirely and re-fetch the bounded session list in
switchSession, so N always resolves against what is live now. This also
removes per-conversation in-memory state that was never cleared.
saveLocked always wrote a fixed <path>.tmp before renaming. Two bridge
processes sharing one state path then race on that single temp: one
renames it away before the other's rename, failing the second write with
ENOENT (clobbering cursor/session updates).

Write each snapshot through os.CreateTemp in the same directory and
rename it into place, with a deferred cleanup that is a no-op once the
rename succeeds. The in-process mutex is unchanged.
@Astro-Han Astro-Han merged commit 9dc42a7 into dev Jun 15, 2026
35 checks passed
@Astro-Han Astro-Han deleted the codex/i1188-remote-gateway branch June 15, 2026 17:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci Continuous integration / GitHub Actions enhancement New feature or request harness Model harness, prompts, tool descriptions, and session mechanics P2 Medium priority

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant