Skip to content

Add OpenClaw bridge runtime and provisioning support#7

Open
batuhan wants to merge 44 commits into
mainfrom
batuhan/oc-2
Open

Add OpenClaw bridge runtime and provisioning support#7
batuhan wants to merge 44 commits into
mainfrom
batuhan/oc-2

Conversation

@batuhan
Copy link
Copy Markdown
Member

@batuhan batuhan commented May 24, 2026

Summary

  • Introduce the openclaw package with CLI, runtime, bridge-agent, registry, setup, approval, and event-mapping flows.
  • Add protocol coverage and integration tests to lock down bridge and provisioning behavior.
  • Update the native pickle appservice/auth path to align with the new OpenClaw bridge integration.
  • Refresh package metadata, workspace config, and documentation for the new plugin.

Testing

  • Added and updated unit/integration coverage across packages/openclaw and packages/bridge.
  • Added native appservice tests in packages/pickle/native/internal/core.
  • Not run (not requested).

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 24, 2026

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • New Features

    • Added OpenClaw bridge connector for Beeper integration with contact discovery, session management, and message forwarding
    • Introduced contact listing provisioning endpoint
    • Added support for custom room creation settings
    • Expanded Matrix event handling for message edits, reactions, and state changes
    • Implemented Beeper AI run lifecycle management
    • Added session discovery and history backfill capabilities
    • Introduced approval request and response handling for tool calls
  • Documentation

    • Added comprehensive OpenClaw bridge documentation with CLI usage and TypeScript integration examples

Walkthrough

Adds the OpenClaw Beeper plugin/package, connector, runtime adapters, streaming/approval/backfill/registry/rooms, and CLI. Expands bridge Matrix dispatch and provisioning. Introduces Go AI-run lifecycle operations and updates client/types/tests across packages.

Changes

OpenClaw Beeper channel and bridge/runtime changes

Layer / File(s) Summary
End-to-end plugin, bridge, runtime, and native ops
packages/openclaw/*, packages/bridge/*, packages/pickle/*, packages/state-file/*, pnpm-workspace.yaml, package.json, CONTRIBUTING.md
Complete addition of OpenClaw plugin, connector/runtime, streaming/approvals/backfill/registry/rooms/CLI; bridge dispatch/provisioning/types updates; Go core AI-run lifecycle and corresponding client/types/tests.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~150 minutes

✨ 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 batuhan/oc-2

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (5)
packages/openclaw/src/backfill.test.ts (1)

165-165: ⚡ Quick win

Use per-test temporary registry paths for isolation.

Lines 165, 220, 269, and 307 use fixed /tmp/*.json files. Re-runs/watch mode can inherit stale state and make these tests flaky. Prefer mkdtemp + unique registry.json per test case.

♻️ Suggested pattern
+import { mkdtemp } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
...
-const registry = new OpenClawBridgeRegistry("/tmp/openclaw-backfill-test.json");
+const registryDir = await mkdtemp(join(tmpdir(), "openclaw-backfill-"));
+const registry = new OpenClawBridgeRegistry(join(registryDir, "registry.json"));

Also applies to: 220-220, 269-269, 307-307

🤖 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/openclaw/src/backfill.test.ts` at line 165, Replace fixed temp file
paths used when constructing OpenClawBridgeRegistry in the tests (e.g., the
registry variable created with new
OpenClawBridgeRegistry("/tmp/openclaw-backfill-test.json")) with a per-test
unique path generated via a temporary directory (use mkdtemp or equivalent) and
a deterministic filename like "registry.json" inside that dir, and ensure
cleanup after the test; update all occurrences referenced in this file (the
registry instantiations around the tests at lines that create registry) so each
test gets an isolated registry file.
packages/openclaw/src/bridge-agent.ts (1)

113-122: 💤 Low value

Event normalization logic may duplicate payload data.

When event.event and event.payload are both present (line 114-116), the result includes both the spread payload fields and payload: event.payload. This creates a structure where the same data appears twice under different keys, which may cause confusion or increased memory usage in long-running streams.

Consider whether the payload property in the result is intentional or if only the spread fields are needed.

🤖 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/openclaw/src/bridge-agent.ts` around lines 113 - 122, The
normalization in openClawEventFromGateway duplicates payload when both
event.event and event.payload are present by spreading event.payload and also
adding payload: event.payload; update openClawEventFromGateway so it does one or
the other: either spread the payload fields into the returned object and omit
the payload property, or return an object with type and a single payload
property (no spreading). Ensure the returned shape is consistent with downstream
consumers (adjust callers if they expect payload vs flattened fields) and keep
the branch for when only payload exists unchanged.
packages/openclaw/src/backfill.ts (1)

103-156: ⚖️ Poor tradeoff

Consider adding error handling for individual session backfill failures.

The loop at lines 111-154 processes sessions sequentially, but if one session fails (e.g., buildBackfillImport throws), the entire backfill operation will abort and subsequent sessions won't be processed.

Consider wrapping individual session processing in try-catch to log failures and continue with remaining sessions, or document that partial failures are intentional.

🤖 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/openclaw/src/backfill.ts` around lines 103 - 156, The
backfillAllOpenClawSessions loop currently lets any thrown error (e.g., from
buildBackfillImport, options.bridge.createPortal, or
options.bridge.backfillPortal) abort the whole operation; wrap the per-session
processing inside a try-catch so one failing session is logged and skipped while
the loop continues. Specifically, in backfillAllOpenClawSessions surround the
body that creates the agent/portal, calls options.bridge.createPortal,
buildBackfillImport, options.registry.upsertBinding, and
options.bridge.backfillPortal with try { ... } catch (err) {
processLogger.error(...) or options.runtime.logger.error(...);
skipped.push(session); continue; } and ensure any resources (like created
portals or partial bindings) are accounted for or rolled back if needed; keep
existing behavior of pushing successful sessions into importedSessions and
portals into portals.
packages/openclaw/src/cli.ts (1)

398-410: 💤 Low value

Type cast of io.stderr may fail for minimal streams.

The CliIO.stderr type is Pick<typeof process.stderr, "write">, but createInterface expects a full NodeJS.WritableStream for the output parameter. If a minimal mock is passed (only implementing write), this cast could cause runtime errors when readline tries to use other stream methods.

Consider using process.stderr as a fallback or documenting that stderr must be a full writable stream when stdin is provided.

🤖 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/openclaw/src/cli.ts` around lines 398 - 410, The code passes
io.stderr to createInterface by casting to NodeJS.WritableStream which can fail
for minimal mocks (CliIO.stderr). Change the call around createInterface to
detect whether io.stderr implements the full writable stream surface (e.g., has
write and on/emit methods) and if not fall back to process.stderr; update the
createInterface invocation (the rl creation that uses input and output) to use
that fallback so readline always gets a proper NodeJS.WritableStream, and ensure
io.stdin/io.stderr checks happen before rl.question/rl.close.
packages/openclaw/src/setup.ts (1)

443-481: ⚖️ Poor tradeoff

Gateway start/stop may race on rapid abort.

In startBeeperGatewayAccount, the bridge is added to startedBridges at line 462, then waitForAbort is called. If abortSignal is already aborted when the function is called, waitForAbort resolves immediately (line 668), but bridge.stop?.() is still called in the finally block. This is correct behavior.

However, if stopBeeperGatewayAccount is called concurrently while startBeeperGatewayAccount is in the finally block, both could attempt to stop the same bridge and delete from the map. Consider adding a guard or using a more explicit state machine if concurrent stop calls are expected.

🤖 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/openclaw/src/setup.ts` around lines 443 - 481,
startBeeperGatewayAccount can race with stopBeeperGatewayAccount causing both to
call bridge.stop and delete startedBridges; to fix, make stop idempotent by
tying deletion/stop to the exact instance you started: when you set
startedBridges with key = gatewayAccountKey(ctx.accountId) store the bridge
object (StartedBeeperBridge) and in the finally block only call
startedBridges.delete(key) and await bridge.stop() if startedBridges.get(key)
=== bridge (i.e., compare by reference) — likewise update
stopBeeperGatewayAccount to read the bridge from startedBridges, delete it (or
mark it) and call stop only for that retrieved instance; alternatively use a
per-bridge mutex/boolean flag on the stored entry to ensure stop is executed
once.
🤖 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/openclaw/README.md`:
- Line 25: Update the README text to capitalize "Mac" where it refers to Apple's
platform: change "mac app" to "Mac app" (and any other occurrences like the one
noted around line 70) so all references to Apple's macOS/Mac apps use "Mac" with
an uppercase M.

In `@packages/openclaw/src/registry.test.ts`:
- Around line 8-46: The test creates a temp dir via mkdtemp and never removes
it; update the test that uses mkdtemp/dir/path/OpenClawBridgeRegistry to delete
the temporary directory when done (use a finally block or afterEach/teardown) so
the temp folder is removed even on failures; ensure you call the appropriate
async removal (e.g., fs.rm/fs.rmdir with recursive/force) on dir after
registry.save() and after the loaded assertions to prevent accumulating temp
files.

In `@packages/openclaw/src/registry.ts`:
- Around line 37-39: The temp filename for atomic saves can collide when
multiple save() calls run in the same process; change the tmp name generation in
the save routine to include a per-operation unique token (e.g., add
crypto.randomUUID() or a timestamp+random suffix) so each save uses its own temp
file (use the same this.path and this.#data write/rename flow but build tmp as
`${this.path}.${process.pid}.${uniqueToken}.tmp` and import/use
crypto.randomUUID() or a safe fallback).

In `@packages/openclaw/src/rooms.ts`:
- Around line 18-27: The helper functions (agentGhostUserId, userGhostUserId,
serviceBotUserId and the other similar helpers around lines 86-100) currently
always compute the domain with matrixDomainFromHomeserver(config.homeserver) and
therefore ignore an explicit config.homeserverDomain; update each function to
prefer config.homeserverDomain when present and only fall back to
matrixDomainFromHomeserver(config.homeserver) otherwise (e.g., use domain =
config.homeserverDomain ?? matrixDomainFromHomeserver(config.homeserver)); make
this change for agentGhostUserId, userGhostUserId, serviceBotUserId and the
other ghost/service MXID helpers so generated MXIDs use the configured
homeserverDomain when provided.

---

Nitpick comments:
In `@packages/openclaw/src/backfill.test.ts`:
- Line 165: Replace fixed temp file paths used when constructing
OpenClawBridgeRegistry in the tests (e.g., the registry variable created with
new OpenClawBridgeRegistry("/tmp/openclaw-backfill-test.json")) with a per-test
unique path generated via a temporary directory (use mkdtemp or equivalent) and
a deterministic filename like "registry.json" inside that dir, and ensure
cleanup after the test; update all occurrences referenced in this file (the
registry instantiations around the tests at lines that create registry) so each
test gets an isolated registry file.

In `@packages/openclaw/src/backfill.ts`:
- Around line 103-156: The backfillAllOpenClawSessions loop currently lets any
thrown error (e.g., from buildBackfillImport, options.bridge.createPortal, or
options.bridge.backfillPortal) abort the whole operation; wrap the per-session
processing inside a try-catch so one failing session is logged and skipped while
the loop continues. Specifically, in backfillAllOpenClawSessions surround the
body that creates the agent/portal, calls options.bridge.createPortal,
buildBackfillImport, options.registry.upsertBinding, and
options.bridge.backfillPortal with try { ... } catch (err) {
processLogger.error(...) or options.runtime.logger.error(...);
skipped.push(session); continue; } and ensure any resources (like created
portals or partial bindings) are accounted for or rolled back if needed; keep
existing behavior of pushing successful sessions into importedSessions and
portals into portals.

In `@packages/openclaw/src/bridge-agent.ts`:
- Around line 113-122: The normalization in openClawEventFromGateway duplicates
payload when both event.event and event.payload are present by spreading
event.payload and also adding payload: event.payload; update
openClawEventFromGateway so it does one or the other: either spread the payload
fields into the returned object and omit the payload property, or return an
object with type and a single payload property (no spreading). Ensure the
returned shape is consistent with downstream consumers (adjust callers if they
expect payload vs flattened fields) and keep the branch for when only payload
exists unchanged.

In `@packages/openclaw/src/cli.ts`:
- Around line 398-410: The code passes io.stderr to createInterface by casting
to NodeJS.WritableStream which can fail for minimal mocks (CliIO.stderr). Change
the call around createInterface to detect whether io.stderr implements the full
writable stream surface (e.g., has write and on/emit methods) and if not fall
back to process.stderr; update the createInterface invocation (the rl creation
that uses input and output) to use that fallback so readline always gets a
proper NodeJS.WritableStream, and ensure io.stdin/io.stderr checks happen before
rl.question/rl.close.

In `@packages/openclaw/src/setup.ts`:
- Around line 443-481: startBeeperGatewayAccount can race with
stopBeeperGatewayAccount causing both to call bridge.stop and delete
startedBridges; to fix, make stop idempotent by tying deletion/stop to the exact
instance you started: when you set startedBridges with key =
gatewayAccountKey(ctx.accountId) store the bridge object (StartedBeeperBridge)
and in the finally block only call startedBridges.delete(key) and await
bridge.stop() if startedBridges.get(key) === bridge (i.e., compare by reference)
— likewise update stopBeeperGatewayAccount to read the bridge from
startedBridges, delete it (or mark it) and call stop only for that retrieved
instance; alternatively use a per-bridge mutex/boolean flag on the stored entry
to ensure stop is executed once.
🪄 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: 15a4fe7d-cf6d-4994-9088-6f5db04da9ae

📥 Commits

Reviewing files that changed from the base of the PR and between b662c4a and 514a1a9.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (60)
  • PLAN.md
  • packages/bridge/src/bridge.test.ts
  • packages/bridge/src/bridge.ts
  • packages/bridge/src/provisioning.test.ts
  • packages/bridge/src/provisioning.ts
  • packages/bridge/src/types.ts
  • packages/openclaw/LICENSE
  • packages/openclaw/README.md
  • packages/openclaw/openclaw.plugin.json
  • packages/openclaw/package.json
  • packages/openclaw/src/approval.test.ts
  • packages/openclaw/src/approval.ts
  • packages/openclaw/src/appservice.test.ts
  • packages/openclaw/src/appservice.ts
  • packages/openclaw/src/backfill.test.ts
  • packages/openclaw/src/backfill.ts
  • packages/openclaw/src/beeper-setup.test.ts
  • packages/openclaw/src/beeper-setup.ts
  • packages/openclaw/src/beeper-stream.test.ts
  • packages/openclaw/src/beeper-stream.ts
  • packages/openclaw/src/bridge-agent.test.ts
  • packages/openclaw/src/bridge-agent.ts
  • packages/openclaw/src/cli.test.ts
  • packages/openclaw/src/cli.ts
  • packages/openclaw/src/config.test.ts
  • packages/openclaw/src/config.ts
  • packages/openclaw/src/connector.test.ts
  • packages/openclaw/src/connector.ts
  • packages/openclaw/src/index.ts
  • packages/openclaw/src/integration.test.ts
  • packages/openclaw/src/openclaw-event-map.test.ts
  • packages/openclaw/src/openclaw-event-map.ts
  • packages/openclaw/src/openclaw-extension.test.ts
  • packages/openclaw/src/openclaw-extension.ts
  • packages/openclaw/src/openclaw-runtime.test.ts
  • packages/openclaw/src/openclaw-runtime.ts
  • packages/openclaw/src/plugin-entry.ts
  • packages/openclaw/src/protocol-coverage.test.ts
  • packages/openclaw/src/protocol-coverage.ts
  • packages/openclaw/src/registration.test.ts
  • packages/openclaw/src/registration.ts
  • packages/openclaw/src/registry.test.ts
  • packages/openclaw/src/registry.ts
  • packages/openclaw/src/rooms.test.ts
  • packages/openclaw/src/rooms.ts
  • packages/openclaw/src/serial.ts
  • packages/openclaw/src/setup-entry.ts
  • packages/openclaw/src/setup.test.ts
  • packages/openclaw/src/setup.ts
  • packages/openclaw/src/stream-map.ts
  • packages/openclaw/src/types.ts
  • packages/openclaw/tsconfig.json
  • packages/openclaw/tsdown.config.ts
  • packages/openclaw/vitest.config.ts
  • packages/pickle/native/internal/core/appservice.go
  • packages/pickle/native/internal/core/appservice_test.go
  • packages/pickle/src/beeper/auth.test.ts
  • packages/pickle/src/beeper/auth.ts
  • packages/pickle/src/generated-runtime-types.ts
  • pnpm-workspace.yaml
📜 Review details
🧰 Additional context used
🪛 GitHub Actions: CI / 0_Typecheck, test, build, and package.txt
packages/openclaw/package.json

[error] 1-1: Dependency specifier mismatch versus pnpm-lock.yaml: openclaw@>=2026.5.24 appears to have been added in package.json but not reflected in the lockfile.

🪛 GitHub Actions: CI / Typecheck, test, build, and package
packages/openclaw/package.json

[error] 1-1: Dependency specifiers added/changed in package.json require updating pnpm-lock.yaml (openclaw@>=2026.5.24).

🪛 LanguageTool
packages/openclaw/README.md

[grammar] ~18-~18: Ensure spelling is correct
Context: ...e login for existing accounts. - Beeper appservice registration for the OpenClaw bridge. -...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[uncategorized] ~25-~25: Did you mean Apple’s computer “Mac” (= trademark, capitalized)?
Context: ...ation. - Backfill helpers for terminal, mac app, and external one-to-one OpenClaw s...

(APPLE_PRODUCTS)


[grammar] ~46-~46: Ensure spelling is correct
Context: ...-code 123456 Register the OpenClaw appservice with Beeper: sh pickle-openclaw bee...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~54-~54: Ensure spelling is correct
Context: ...BRIDGE_MANAGER_TOKEN" Do login and appservice registration in one step: sh pickle...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[uncategorized] ~70-~70: Did you mean Apple’s computer “Mac” (= trademark, capitalized)?
Context: ...to-one OpenClaw sessions from terminal, mac app, and channel surfaces: ```sh pickl...

(APPLE_PRODUCTS)

PLAN.md

[grammar] ~5-~5: Ensure spelling is correct
Context: ...nto Beeper through a self-hosted Beeper appservice. The plugin owns Beeper login, appservi...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~5-~5: Ensure spelling is correct
Context: ...pservice. The plugin owns Beeper login, appservice registration, settings/setup, contact d...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~44-~44: Ensure spelling is correct
Context: ...nit tests: - Beeper OTP/setup config, appservice registration, ClawHub/package metadata,...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~58-~58: Ensure spelling is correct
Context: ...build` - Focused Pickle bridge stream/appservice tests - Package validation for OpenCl...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🪛 OpenGrep (1.21.0)
packages/openclaw/src/backfill.ts

[ERROR] 208-208: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)

packages/openclaw/src/connector.ts

[ERROR] 922-922: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)

🔇 Additional comments (58)
pnpm-workspace.yaml (1)

6-6: LGTM!

packages/openclaw/tsdown.config.ts (1)

3-8: LGTM!

packages/openclaw/src/setup-entry.ts (1)

3-8: LGTM!

packages/openclaw/src/rooms.test.ts (1)

15-98: LGTM!

packages/openclaw/src/openclaw-extension.ts (1)

3-21: LGTM!

packages/openclaw/vitest.config.ts (1)

3-23: LGTM!

packages/openclaw/LICENSE (1)

1-1: ⚡ Quick win

Check MPL-2.0 notice/availability for packages/openclaw

packages/openclaw/LICENSE is SPDX-only (MPL-2.0, 1 line). This aligns with the repo’s monorepo licensing approach for other MPL packages (e.g., multiple packages/*/LICENSE files are also SPDX-only or contain a brief MPL notice + link rather than embedding the full MPL text).

Verify for the published OpenClaw artifacts:

  1. The required MPL 2.0 attribution/notice is included (and users are given a way to obtain the MPL text).
  2. The root ./LICENSE and the relevant package-level LICENSE files are included in what’s actually distributed.
  3. Whether you specifically need to ship the full MPL 2.0 text in packages/openclaw/LICENSE, or whether the repo’s current SPDX-only/pointer pattern satisfies your distribution model.
packages/openclaw/src/config.test.ts (1)

1-69: LGTM!

packages/pickle/src/beeper/auth.test.ts (1)

70-102: LGTM!

PLAN.md (1)

1-67: LGTM!

packages/openclaw/src/index.ts (1)

1-22: LGTM!

packages/openclaw/tsconfig.json (1)

1-9: LGTM!

packages/openclaw/src/registration.test.ts (1)

1-69: LGTM!

packages/bridge/src/bridge.ts (2)

292-292: LGTM!


851-860: LGTM!

packages/openclaw/src/plugin-entry.ts (1)

1-4: LGTM!

packages/openclaw/src/beeper-setup.test.ts (1)

1-150: LGTM!

packages/openclaw/src/openclaw-runtime.test.ts (1)

1-326: LGTM!

packages/openclaw/src/beeper-setup.ts (1)

1-177: LGTM!

packages/openclaw/src/openclaw-extension.test.ts (1)

1-110: LGTM!

packages/openclaw/src/serial.ts (1)

1-9: LGTM!

packages/pickle/src/generated-runtime-types.ts (1)

77-77: LGTM!

packages/pickle/native/internal/core/appservice_test.go (1)

31-34: LGTM!

Also applies to: 56-58

packages/openclaw/openclaw.plugin.json (1)

1-176: LGTM!

packages/openclaw/src/approval.test.ts (1)

1-92: LGTM!

packages/openclaw/src/types.ts (1)

1-93: LGTM!

packages/openclaw/src/appservice.test.ts (1)

1-81: LGTM!

packages/bridge/src/types.ts (1)

163-165: LGTM!

Also applies to: 655-656, 894-902

packages/bridge/src/provisioning.test.ts (1)

35-35: LGTM!

Also applies to: 49-98, 112-117

packages/pickle/src/beeper/auth.ts (1)

12-12: LGTM!

Also applies to: 40-43, 87-89, 93-93, 102-104, 107-107

packages/openclaw/src/protocol-coverage.test.ts (1)

1-72: LGTM!

packages/bridge/src/bridge.test.ts (1)

248-253: LGTM!

Also applies to: 264-269

packages/pickle/native/internal/core/appservice.go (1)

89-95: LGTM!

Also applies to: 356-361, 709-716

packages/openclaw/src/registration.ts (1)

9-63: LGTM!

packages/bridge/src/provisioning.ts (1)

11-11: LGTM!

Also applies to: 23-23, 46-55, 101-101, 159-164, 234-239

packages/openclaw/src/cli.test.ts (1)

9-412: LGTM!

packages/openclaw/src/protocol-coverage.ts (1)

1-226: LGTM!

packages/openclaw/package.json (1)

185-191: ⚡ Quick win

Remove the pnpm-lock.yaml update request for the openclaw peerDependency spec change
pnpm-lock.yaml does not record peerDependencies/peerDependenciesMeta for the packages/openclaw workspace importer (it contains no openclaw specifier anywhere beyond the importer key), so regenerating the lockfile shouldn’t be required for this change.

			> Likely an incorrect or invalid review comment.
packages/openclaw/src/integration.test.ts (1)

13-266: LGTM!

packages/openclaw/src/beeper-stream.test.ts (1)

7-263: LGTM!

packages/openclaw/src/config.ts (1)

8-151: LGTM!

packages/openclaw/src/appservice.ts (1)

23-97: LGTM!

packages/openclaw/src/approval.ts (1)

24-127: LGTM!

packages/openclaw/src/openclaw-event-map.ts (1)

212-216: ⚡ Quick win

Fix overreach: circular payloads are unlikely on the run.failed path

errorText() is called for run.failed with data.error ?? data.message ?? data, where data originates from gateway frames parsed via JSON.parse (SSE parseEventFrame and WebSocket #handleFrame). That means circular object graphs and non-JSON Error instances shouldn’t reach errorText() from the runtime, so this is not a reliability issue for run.failed handling. Keep the fallback only as optional hardening for non-wire callers.

			> Likely an incorrect or invalid review comment.
packages/openclaw/src/bridge-agent.test.ts (1)

1-276: LGTM!

packages/openclaw/src/cli.ts (2)

28-205: LGTM!


207-416: LGTM!

packages/openclaw/src/openclaw-event-map.test.ts (1)

1-297: LGTM!

packages/openclaw/src/beeper-stream.ts (2)

52-203: LGTM!


205-389: LGTM!

packages/openclaw/src/setup.test.ts (1)

1-548: LGTM!

packages/openclaw/src/bridge-agent.ts (1)

1-112: LGTM!

packages/openclaw/src/backfill.ts (1)

1-102: LGTM!

Also applies to: 157-271

packages/openclaw/src/setup.ts (1)

1-442: LGTM!

Also applies to: 482-707

packages/openclaw/src/connector.ts (1)

1-935: LGTM!

The static analysis hint about "command injection" on line 922 is a false positive - the code uses RegExp.prototype.exec() for pattern matching, not child_process.exec/execSync.

packages/openclaw/src/connector.test.ts (1)

1-1087: LGTM!

packages/openclaw/src/openclaw-runtime.ts (1)

1-721: LGTM!

packages/openclaw/src/stream-map.ts (1)

1-284: LGTM!

Comment thread packages/openclaw/README.md Outdated
Comment on lines +8 to +46
it("persists agent contacts, user contacts, session bindings, and dedupe keys", async () => {
const dir = await mkdtemp(resolve(tmpdir(), "pickle-openclaw-"));
const path = resolve(dir, "registry.json");
const registry = new OpenClawBridgeRegistry(path);
await registry.load();
registry.upsertAgent({
agentId: "codex",
displayName: "Codex",
ghostUserId: "@openclaw_agent_codex:example.com",
});
registry.upsertUser({
displayName: "Alice",
ghostUserId: "@openclaw_user_alice:example.com",
source: "whatsapp",
userId: "alice",
});
registry.upsertBinding({
agentId: "codex",
createdAt: 1,
ghostUserId: "@openclaw_agent_codex:example.com",
id: "binding",
kind: "session",
owner: "bridge",
roomId: "!room:example.com",
sessionKey: "agent:codex:main",
updatedAt: 1,
});
registry.markDedupe("$event");
await registry.save();

const loaded = new OpenClawBridgeRegistry(path);
await loaded.load();
expect(loaded.getAgent("codex")?.displayName).toBe("Codex");
expect(loaded.getUser("alice")?.ghostUserId).toBe("@openclaw_user_alice:example.com");
expect(loaded.getBindingByRoom("!room:example.com")?.sessionKey).toBe("agent:codex:main");
expect(loaded.getBindingBySessionKey("agent:codex:main")?.id).toBe("binding");
expect(loaded.getBindingsByAgent("codex")).toHaveLength(1);
expect(loaded.hasDedupe("$event")).toBe(true);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add cleanup for temporary test directory.

The test creates a temporary directory but never removes it, which will accumulate temp files over multiple test runs.

🧹 Proposed fix to add cleanup
+import { rm } from "node:fs/promises";
+
 describe("OpenClawBridgeRegistry", () => {
   it("persists agent contacts, user contacts, session bindings, and dedupe keys", async () => {
     const dir = await mkdtemp(resolve(tmpdir(), "pickle-openclaw-"));
-    const path = resolve(dir, "registry.json");
-    const registry = new OpenClawBridgeRegistry(path);
-    await registry.load();
+    try {
+      const path = resolve(dir, "registry.json");
+      const registry = new OpenClawBridgeRegistry(path);
+      await registry.load();
     
-    // ... rest of test
+      // ... rest of test
     
-    expect(loaded.hasDedupe("$event")).toBe(true);
+      expect(loaded.hasDedupe("$event")).toBe(true);
+    } finally {
+      await rm(dir, { recursive: true, force: true });
+    }
   });
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/openclaw/src/registry.test.ts` around lines 8 - 46, The test creates
a temp dir via mkdtemp and never removes it; update the test that uses
mkdtemp/dir/path/OpenClawBridgeRegistry to delete the temporary directory when
done (use a finally block or afterEach/teardown) so the temp folder is removed
even on failures; ensure you call the appropriate async removal (e.g.,
fs.rm/fs.rmdir with recursive/force) on dir after registry.save() and after the
loaded assertions to prevent accumulating temp files.

Comment on lines +37 to +39
const tmp = `${this.path}.${process.pid}.tmp`;
await writeFile(tmp, `${JSON.stringify(this.#data, null, 2)}\n`, { mode: 0o600 });
await rename(tmp, this.path);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use a unique temp filename per save operation.

Using only process.pid for the temp path can collide under concurrent save() calls in the same process.

🛠️ Proposed fix
+import { randomUUID } from "node:crypto";
 import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
@@
-    const tmp = `${this.path}.${process.pid}.tmp`;
+    const tmp = `${this.path}.${process.pid}.${randomUUID()}.tmp`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const tmp = `${this.path}.${process.pid}.tmp`;
await writeFile(tmp, `${JSON.stringify(this.#data, null, 2)}\n`, { mode: 0o600 });
await rename(tmp, this.path);
import { randomUUID } from "node:crypto";
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
const tmp = `${this.path}.${process.pid}.${randomUUID()}.tmp`;
await writeFile(tmp, `${JSON.stringify(this.#data, null, 2)}\n`, { mode: 0o600 });
await rename(tmp, this.path);
🤖 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/openclaw/src/registry.ts` around lines 37 - 39, The temp filename
for atomic saves can collide when multiple save() calls run in the same process;
change the tmp name generation in the save routine to include a per-operation
unique token (e.g., add crypto.randomUUID() or a timestamp+random suffix) so
each save uses its own temp file (use the same this.path and this.#data
write/rename flow but build tmp as
`${this.path}.${process.pid}.${uniqueToken}.tmp` and import/use
crypto.randomUUID() or a safe fallback).

Comment thread packages/openclaw/src/rooms.ts Outdated
@batuhan batuhan marked this pull request as ready for review May 27, 2026 14:37
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 16

Caution

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

⚠️ Outside diff range comments (1)
packages/openclaw/src/backfill.ts (1)

278-293: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle "agent" transcript roles as agent-authored messages.

The new runtime normalizes assistant history entries to role: "agent", but this mapper only treats "assistant" and "tool" as agent output. Those records will be backfilled as human-authored m.notice events instead of agent messages.

Suggested fix
 function normalizeHistoryMessage(message: OpenClawChatHistoryMessage, index: number): OpenClawBackfillMessage {
   const role = typeof message.role === "string" ? message.role : "assistant";
   const text = contentText(message.content);
+  const isAgentSender = role === "assistant" || role === "agent" || role === "tool";
+  const isAgentText = role === "assistant" || role === "agent";
   const normalized: OpenClawBackfillMessage = {
     content: {
       body: text || JSON.stringify(message.content ?? message),
-      msgtype: role === "assistant" ? "m.text" : "m.notice",
+      msgtype: isAgentText ? "m.text" : "m.notice",
       "com.beeper.openclaw.backfill": {
         messageSeq: message.messageSeq ?? index,
         role,
       },
     },
     id: typeof message.id === "string" ? message.id : `history_${index}`,
     role,
-    sender: role === "assistant" || role === "tool" ? "agent" : role === "system" ? "system" : "human",
+    sender: isAgentSender ? "agent" : role === "system" ? "system" : "human",
     seq: typeof message.messageSeq === "number" ? message.messageSeq : index,
   };
🤖 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/openclaw/src/backfill.ts` around lines 278 - 293, The mapper
normalizeHistoryMessage currently treats only "assistant" and "tool" as agent
outputs; update it to also consider "agent" as an agent-authored role so those
entries are emitted as agent messages. Specifically, in normalizeHistoryMessage
adjust the msgtype calculation (now msgtype should be "m.text" for role ===
"assistant" || role === "agent" || role === "tool") and update the sender
assignment to treat role === "agent" the same as "assistant" or "tool" (i.e.,
set sender to "agent"); preserve existing fallback behavior for "system" and
human roles and keep seq/id logic unchanged.
♻️ Duplicate comments (1)
packages/openclaw/src/rooms.ts (1)

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

Use config-aware domain when creating session rooms.

createSessionRoom still defaults to matrixDomainFromHomeserver(config.homeserver), which bypasses config.homeserverDomain and can generate the service-bot MXID on the wrong domain.

🔧 Proposed fix
-  const domain = options.domain ?? matrixDomainFromHomeserver(config.homeserver);
+  const domain = options.domain ?? matrixDomainFromConfig(config);
🤖 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/openclaw/src/rooms.ts` at line 90, The domain fallback in
createSessionRoom uses matrixDomainFromHomeserver(config.homeserver) which
ignores config.homeserverDomain; change the domain resolution (the const domain
in createSessionRoom) to prefer options.domain, then config.homeserverDomain,
and only finally call matrixDomainFromHomeserver(config.homeserver) so the
service-bot MXID is created on the configured homeserver domain.
🧹 Nitpick comments (1)
packages/openclaw/src/openclaw-runtime.test.ts (1)

154-183: ⚡ Quick win

Wrap global runtime registration in try/finally.

These tests clean up setBeeperChannelRuntimeForHost(..., undefined) only on the happy path. A failing assertion can leak global state into later tests.

🧪 Suggested pattern
 setBeeperChannelRuntimeForHost(hostRuntime, new BeeperChannelRuntime({
   client: {
     beeper: { aiRuns: createTestBeeperAIRuns(), streams: beeperStreams },
     media: { upload: vi.fn() },
   } as never,
   userId: "`@sh-openclaw-bot`:example",
 }));
-const runtime = new OpenClawPluginRuntimeAdapter({
-  config: createDefaultConfig({ dataDir: "/tmp/openclaw" }),
-  transport: createOpenClawHostRuntimeAdapter(hostRuntime),
-});
-
-// assertions...
-
-setBeeperChannelRuntimeForHost(hostRuntime, undefined);
+try {
+  const runtime = new OpenClawPluginRuntimeAdapter({
+    config: createDefaultConfig({ dataDir: "/tmp/openclaw" }),
+    transport: createOpenClawHostRuntimeAdapter(hostRuntime),
+  });
+
+  // assertions...
+} finally {
+  setBeeperChannelRuntimeForHost(hostRuntime, undefined);
+}

Also applies to: 312-394, 443-483, 550-596

🤖 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/openclaw/src/openclaw-runtime.test.ts` around lines 154 - 183, The
test registers a global runtime via setBeeperChannelRuntimeForHost(hostRuntime,
new BeeperChannelRuntime(...)) but only clears it on the happy path; wrap the
registration and test actions in a try/finally so cleanup always runs.
Specifically, after calling setBeeperChannelRuntimeForHost(...) and before
awaiting runtime.sendMessage / assertions (in the test that constructs
OpenClawPluginRuntimeAdapter and calls runtime.sendMessage), add a try block for
the sendMessage call and all expects, and move the
setBeeperChannelRuntimeForHost(hostRuntime, undefined) call into the finally
block to guarantee global state is reset even if assertions fail; apply the same
pattern to the other similar test regions mentioned.
🤖 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/bridge/src/bridge.ts`:
- Around line 1756-1760: The helper `#matchingRemoteTarget` currently ignores the
partId and always returns by index, which causes multipart edits/removals to
target the wrong SentEvent; update `#matchingRemoteTarget`(SentEvent[] existing,
partId, index) to first search existing for an entry whose partId equals the
provided partId and return that SentEvent if found, otherwise fall back to
existing[index] or existing[0]; apply the same change to the other occurrence
that uses the same index-only fallback (the similar logic around the earlier
occurrence referenced in the comment) so both places consistently prefer partId
matching before index fallback.

In `@packages/openclaw/src/approval.ts`:
- Around line 60-67: defaultBeeperApprovalActions currently advertises
"allow_session" and "allow_room" but the resolver
(toOpenClawApprovalResolvePayload) collapses those into always-approve, so the
UI promises a narrower scope than is actually granted; change
defaultBeeperApprovalActions to only expose decisions that the resolver can
preserve (e.g., replace "allow_session" and "allow_room" with a single
"allow_always" and keep "allow_once" and "deny"), update the produced
id/title/reactionKey strings accordingly (use approvalReactionKey and
approvalActionTitle helpers with the new decision names) so UI labels match the
actual serialized decisions and no click will be misrepresented as a broader
grant.

In `@packages/openclaw/src/beeper-channel-config.schema.json`:
- Around line 69-71: The backfillLimit JSON schema currently allows fractional
and negative numbers; update the backfillLimit property (in
beeper-channel-config.schema.json) to enforce non-negative integers by changing
its type from "number" to "integer" and adding "minimum": 0 so only integers >=
0 are valid; keep the existing description unchanged.

In `@packages/openclaw/src/beeper-channel-runtime.ts`:
- Around line 70-95: sendText() and sendMedia() accept threadRoot but never
serialize it into the Matrix event, so messages intended for threads are sent as
top-level events; update sendText (before calling
`#queueRemoteText/withReplyRelation`) and sendMedia (before calling
`#queueRemoteMedia`) to attach an m.thread relation when options.threadRoot is
present (e.g., merge { "m.relates_to": { "m.thread": { "event_id":
options.threadRoot } } } into the content or into the media payload), ensuring
the thread relation is combined with any existing reply relation produced by
withReplyRelation so both reply and thread relations are preserved.

In `@packages/openclaw/src/bridge-agent.test.ts`:
- Line 53: The test calls runtimeWith without the required responses option;
update the test where runtime is constructed (the runtime variable using
runtimeWith) to include a responses property (e.g. responses: {} or the specific
mock response map needed for this test) so it matches the runtimeWith(options: {
responses: Record<string, unknown> }) signature; modify the runtimeWith
invocation (runtimeWith({...})) to pass responses accordingly.

In `@packages/openclaw/src/cli.test.ts`:
- Around line 179-183: The test hardcodes "/tmp/pickle-openclaw-empty" which can
cause flakes; modify the test to create a unique temporary directory (using
os.tmpdir() + fs.mkdtempSync or fs.mkdtemp) and pass that path to runCli instead
of the hardcoded string, referencing the existing test that calls
runCli(["whoami", "--data-dir", ...], io) and using captureIO() for IO;
optionally remove/cleanup the temp dir after the test to avoid residue.

In `@packages/openclaw/src/connector.ts`:
- Around line 204-223: loadUserLogin creates a runtime via
this.#runtimeFactory(this.config) then hands that runtime (indirectly) to
OpenClawNetworkAPI, but `#sendTurn` calls this.#runtimeFactory again on every turn
which can produce a different runtime; change the implementation so the runtime
created in loadUserLogin is reused by `#sendTurn` (e.g., store the runtime
instance created in loadUserLogin on the connector instance and have `#sendTurn`
use that stored runtime instead of calling `#runtimeFactory` again), ensuring
runtime.transport and OpenClawHostRuntimeAdapter logic continues to work as
before.

In `@packages/openclaw/src/integration.test.ts`:
- Around line 135-138: The current assertion checks for a specific payload and
can miss calls to "exec.approval.resolve" with extra fields; change the
assertion to assert on the method name only by using a wildcard for the payload:
replace the
expect(transport.request).not.toHaveBeenCalledWith("exec.approval.resolve", {
... }) check with
expect(transport.request).not.toHaveBeenCalledWith("exec.approval.resolve",
expect.any(Object)) (or expect.anything() if payload may be null/undefined) so
transport.request is never called with the "exec.approval.resolve" method
regardless of payload; update the test around transport.request and keep
reference to the method string "exec.approval.resolve" and the transport.request
mock.

In `@packages/openclaw/src/matrix-parser.ts`:
- Around line 36-39: The code currently always strips Matrix reply fallback
which removes leading '>' lines from ordinary blockquotes; change the logic to
only strip reply fallback when the event is actually a reply by detecting reply
status (use the result of extractMatrixReplyFallback(effectiveText) — e.g.,
check fallback.isReply or fallback.repliedTo — or fall back to checking
messageContent?.m.relates_to?.m.in_reply_to?.event_id). Only call
stripLeadingMatrixMention and stripMatrixHtmlReplyFallback when that
reply-detection is true; otherwise leave body and formattedBody as the original
(stringValue(effectiveText) or stringValue(msg?.event.html)) so non-reply
blockquotes are preserved.

In `@packages/openclaw/src/openclaw-runtime.ts`:
- Around line 348-363: The current events() implementation returns early and
only wires one host hook, dropping other sources (onAgentEvent,
onSessionTranscriptUpdate, runtime.events()/subscribe(), and
this.#localEvents.events). Change it to always collect all applicable
async-iterables: if this.#runtime.events is an object and has onAgentEvent
include agentRuntimeEvents(this.#runtime.events.onAgentEvent, filter); if it has
onSessionTranscriptUpdate include
transcriptUpdateEvents(this.#runtime.events.onSessionTranscriptUpdate, filter);
if this.#runtime.events is a function use it (events = this.#runtime.events)
else fallback to this.#runtime.subscribe and include events(filter) when
present; always include this.#localEvents.events(filter). Finally return
mergeEvents([...allCollectedStreams]) and only fall back to
this.#localEvents.events(filter) when no host streams exist.
- Around line 1520-1523: nextVisibleText currently appends a replacement delta
onto the old buffer (returning previous + delta) when the payload rewrites text;
instead, when the new text is a replacement (i.e., next does not
startWith(previous)), reset the visible-text buffer by returning next directly.
Update the function nextVisibleText to return next in the replacement branch
(instead of previous + delta), keeping the existing checks for empty delta and
the normal extension case (next.startsWith(previous)) intact.

In `@packages/openclaw/src/setup.ts`:
- Around line 186-204: The media and payload send wrappers (media and payload in
setup.ts) do not forward the replyToId through to beeperOutboundAdapter—ensure
that when calling beeperOutboundAdapter.sendMedia and
beeperOutboundAdapter.sendPayload from the media and payload async functions you
include the replyToId (and threadId) fields from ctx in the forwarded context
object; update both the media wrapper and the payload wrapper (and any path
where payload routes through media) to pass ctx.replyToId (and ctx.threadId) so
replied media/payload messages are sent as replies rather than top-level posts.
- Around line 487-500: The advertised actions omit "mark_unread" though
handleAction implements it; update the action set and support checks to include
"mark_unread": add "mark_unread" to the beeperMessageToolActions array (used by
describeMessageTool) and include it in the supportsAction predicate inside
beeperMessageActions (and repeat the same change for the other identical block
later in the file). Ensure you update both describeMessageTool()’s actions list
and supportsAction() to recognize "mark_unread" so callers that pre-flight-check
capabilities won’t reject the action.
- Around line 1094-1105: The finally block in startBeeperGatewayAccount (which
awaits waitForAbort(ctx.abortSignal)) can be skipped because
stopBeeperGatewayAccount stops the bridge but never resolves/aborts
ctx.abortSignal; update the stop path to signal the start wait so cleanup runs:
in stopBeeperGatewayAccount / stopAccount invoke the AbortController tied to
ctx.abortSignal (e.g., ctx.abortController?.abort() or otherwise trigger the
same abort signal) or otherwise resolve the waitForAbort promise before/after
stopping the bridge; ensure you reference and use the same ctx object
(ctx.abortSignal / ctx.abortController) so startedBridges.delete(key) and the
host/runtime cleanup in the startBeeperGatewayAccount finally block always
execute.

In `@packages/pickle/native/internal/core/beeper_ai_run.go`:
- Around line 63-67: When beginning a new Beeper AI run, the code currently
unconditionally overwrites c.beeperAIRuns[run.RunID], dropping in-flight data;
modify the Begin logic around aistream.NewRun / aistream.NewWriter /
writer.Start so it first checks c.beeperAIRuns for an existing entry for
run.RunID and if present rejects the duplicate (return an error or the existing
snapshot via c.marshalBeeperAIRunSnapshot and outboundEventsFromAGUI) instead of
replacing the map entry; ensure you only call NewWriter/Start and insert into
c.beeperAIRuns when no existing runID is found and preserve beeperAIRunState for
concurrent/retry calls.

In `@packages/pickle/native/internal/core/core.go`:
- Line 26: The beeperAIRuns map is not cleared on shutdown; modify the core
shutdown logic in handleClose (in core.go) to reset or reinitialize the
beeperAIRuns field (e.g., assign an empty map or nil) to avoid leaking stale run
state across re-inits; ensure you do this for the same cleanup path referenced
at both occurrences around beeperAIRuns so any existing entries are dropped
during close.

---

Outside diff comments:
In `@packages/openclaw/src/backfill.ts`:
- Around line 278-293: The mapper normalizeHistoryMessage currently treats only
"assistant" and "tool" as agent outputs; update it to also consider "agent" as
an agent-authored role so those entries are emitted as agent messages.
Specifically, in normalizeHistoryMessage adjust the msgtype calculation (now
msgtype should be "m.text" for role === "assistant" || role === "agent" || role
=== "tool") and update the sender assignment to treat role === "agent" the same
as "assistant" or "tool" (i.e., set sender to "agent"); preserve existing
fallback behavior for "system" and human roles and keep seq/id logic unchanged.

---

Duplicate comments:
In `@packages/openclaw/src/rooms.ts`:
- Line 90: The domain fallback in createSessionRoom uses
matrixDomainFromHomeserver(config.homeserver) which ignores
config.homeserverDomain; change the domain resolution (the const domain in
createSessionRoom) to prefer options.domain, then config.homeserverDomain, and
only finally call matrixDomainFromHomeserver(config.homeserver) so the
service-bot MXID is created on the configured homeserver domain.

---

Nitpick comments:
In `@packages/openclaw/src/openclaw-runtime.test.ts`:
- Around line 154-183: The test registers a global runtime via
setBeeperChannelRuntimeForHost(hostRuntime, new BeeperChannelRuntime(...)) but
only clears it on the happy path; wrap the registration and test actions in a
try/finally so cleanup always runs. Specifically, after calling
setBeeperChannelRuntimeForHost(...) and before awaiting runtime.sendMessage /
assertions (in the test that constructs OpenClawPluginRuntimeAdapter and calls
runtime.sendMessage), add a try block for the sendMessage call and all expects,
and move the setBeeperChannelRuntimeForHost(hostRuntime, undefined) call into
the finally block to guarantee global state is reset even if assertions fail;
apply the same pattern to the other similar test regions mentioned.
🪄 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: 5b545f82-e5c0-4fb1-9022-8a2a7a19e9b1

📥 Commits

Reviewing files that changed from the base of the PR and between 514a1a9 and 7274fad.

⛔ Files ignored due to path filters (2)
  • packages/pickle/native/go.sum is excluded by !**/*.sum
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (89)
  • CONTRIBUTING.md
  • PLAN_OPENCLAW.md
  • package.json
  • packages/bridge/src/appservice-websocket.test.ts
  • packages/bridge/src/appservice-websocket.ts
  • packages/bridge/src/beeper.test.ts
  • packages/bridge/src/beeper.ts
  • packages/bridge/src/bridge.test.ts
  • packages/bridge/src/bridge.ts
  • packages/bridge/src/provisioning.test.ts
  • packages/bridge/src/provisioning.ts
  • packages/bridge/src/store.test.ts
  • packages/bridge/src/store.ts
  • packages/bridge/src/types.ts
  • packages/openclaw/.npmignore
  • packages/openclaw/README.md
  • packages/openclaw/openclaw.plugin.json
  • packages/openclaw/package.json
  • packages/openclaw/scripts/copy-runtime-assets.mjs
  • packages/openclaw/scripts/sync-manifest-schema.mjs
  • packages/openclaw/src/approval.test.ts
  • packages/openclaw/src/approval.ts
  • packages/openclaw/src/appservice.test.ts
  • packages/openclaw/src/appservice.ts
  • packages/openclaw/src/backfill.test.ts
  • packages/openclaw/src/backfill.ts
  • packages/openclaw/src/beeper-channel-config.schema.json
  • packages/openclaw/src/beeper-channel-runtime.test.ts
  • packages/openclaw/src/beeper-channel-runtime.ts
  • packages/openclaw/src/beeper-setup.test.ts
  • packages/openclaw/src/beeper-setup.ts
  • packages/openclaw/src/beeper-stream.test.ts
  • packages/openclaw/src/beeper-stream.ts
  • packages/openclaw/src/beeper-turn-events.ts
  • packages/openclaw/src/bridge-agent.test.ts
  • packages/openclaw/src/bridge-agent.ts
  • packages/openclaw/src/cli.test.ts
  • packages/openclaw/src/cli.ts
  • packages/openclaw/src/config.test.ts
  • packages/openclaw/src/config.ts
  • packages/openclaw/src/connector.test.ts
  • packages/openclaw/src/connector.ts
  • packages/openclaw/src/ids.ts
  • packages/openclaw/src/integration.test.ts
  • packages/openclaw/src/matrix-parser.ts
  • packages/openclaw/src/openclaw-extension.test.ts
  • packages/openclaw/src/openclaw-extension.ts
  • packages/openclaw/src/openclaw-identity.ts
  • packages/openclaw/src/openclaw-runtime.test.ts
  • packages/openclaw/src/openclaw-runtime.ts
  • packages/openclaw/src/protocol-coverage.test.ts
  • packages/openclaw/src/protocol-coverage.ts
  • packages/openclaw/src/registration.test.ts
  • packages/openclaw/src/registration.ts
  • packages/openclaw/src/registry.test.ts
  • packages/openclaw/src/registry.ts
  • packages/openclaw/src/rooms.test.ts
  • packages/openclaw/src/rooms.ts
  • packages/openclaw/src/setup-entry.ts
  • packages/openclaw/src/setup.test.ts
  • packages/openclaw/src/setup.ts
  • packages/openclaw/src/types.ts
  • packages/openclaw/tsdown.config.ts
  • packages/pickle/native/go.mod
  • packages/pickle/native/internal/core/appservice.go
  • packages/pickle/native/internal/core/appservice_test.go
  • packages/pickle/native/internal/core/beeper_ai_run.go
  • packages/pickle/native/internal/core/beeper_ai_run_test.go
  • packages/pickle/native/internal/core/core.go
  • packages/pickle/native/internal/core/messages.go
  • packages/pickle/native/internal/core/operations.go
  • packages/pickle/native/internal/core/persistent_crypto_load.go
  • packages/pickle/native/internal/core/persistent_crypto_methods.go
  • packages/pickle/native/internal/core/persistent_crypto_snapshot.go
  • packages/pickle/native/internal/core/persistent_crypto_store.go
  • packages/pickle/package.json
  • packages/pickle/src/beeper/auth.test.ts
  • packages/pickle/src/beeper/auth.ts
  • packages/pickle/src/client-types.ts
  • packages/pickle/src/client.test.ts
  • packages/pickle/src/client.ts
  • packages/pickle/src/generated-runtime-operations.ts
  • packages/pickle/src/generated-runtime-types.ts
  • packages/pickle/src/index.ts
  • packages/pickle/src/runtime-types.ts
  • packages/pickle/src/streams/beeper-message.ts
  • packages/state-file/src/index.test.ts
  • packages/state-file/src/index.ts
  • scripts/audit-package-surface.mjs
💤 Files with no reviewable changes (3)
  • packages/pickle/native/internal/core/persistent_crypto_snapshot.go
  • packages/pickle/native/internal/core/persistent_crypto_load.go
  • packages/pickle/native/internal/core/persistent_crypto_store.go
✅ Files skipped from review due to trivial changes (4)
  • CONTRIBUTING.md
  • packages/openclaw/.npmignore
  • packages/pickle/src/generated-runtime-types.ts
  • packages/openclaw/src/protocol-coverage.ts
🚧 Files skipped from review as they are similar to previous changes (13)
  • packages/openclaw/README.md
  • packages/openclaw/src/registration.test.ts
  • packages/openclaw/src/setup-entry.ts
  • packages/openclaw/openclaw.plugin.json
  • packages/openclaw/tsdown.config.ts
  • packages/openclaw/src/beeper-setup.test.ts
  • packages/openclaw/src/protocol-coverage.test.ts
  • packages/openclaw/src/registry.test.ts
  • packages/openclaw/src/registry.ts
  • packages/openclaw/src/types.ts
  • packages/openclaw/src/rooms.test.ts
  • packages/pickle/src/beeper/auth.test.ts
  • packages/openclaw/src/beeper-setup.ts
📜 Review details
🧰 Additional context used
🪛 LanguageTool
PLAN_OPENCLAW.md

[style] ~57-~57: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...dinator plus Go-backed AI run bridge. - Replace connector-local /help, /tools, `/mo...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[grammar] ~57-~57: Ensure spelling is correct
Context: ... with OpenClaw SDK command and approval surfaces. - Keep the Pickle bridge/appservice me...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~58-~58: Ensure spelling is correct
Context: ...oval surfaces. - Keep the Pickle bridge/appservice mechanics for Matrix transport, portals...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~58-~58: Ensure spelling is correct
Context: ...or Matrix transport, portals, contacts, appservice registration, media, reactions, receipt...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🪛 OpenGrep (1.22.0)
packages/openclaw/src/matrix-parser.ts

[ERROR] 136-136: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)


[ERROR] 149-149: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)

packages/openclaw/src/openclaw-runtime.ts

[ERROR] 1650-1650: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)

🔇 Additional comments (43)
packages/bridge/src/store.ts (1)

141-147: LGTM!

packages/openclaw/scripts/sync-manifest-schema.mjs (1)

1-17: LGTM!

packages/openclaw/scripts/copy-runtime-assets.mjs (1)

1-19: LGTM!

packages/state-file/src/index.test.ts (1)

1-1: LGTM!

Also applies to: 30-41

packages/openclaw/src/beeper-channel-config.schema.json (1)

1-68: LGTM!

Also applies to: 72-88

packages/pickle/package.json (1)

66-67: LGTM!

packages/pickle/native/internal/core/operations.go (1)

72-81: LGTM!

packages/bridge/src/beeper.test.ts (1)

53-54: LGTM!

Also applies to: 114-137

packages/pickle/native/go.mod (1)

6-8: LGTM!

Also applies to: 16-18, 23-27, 29-31

packages/pickle/src/index.ts (1)

38-43: LGTM!

packages/openclaw/src/openclaw-identity.ts (1)

5-33: LGTM!

packages/state-file/src/index.ts (1)

1-2: LGTM!

Also applies to: 61-64, 77-83

packages/openclaw/src/ids.ts (1)

1-9: LGTM!

package.json (1)

18-18: LGTM!

packages/pickle/src/client.ts (1)

89-95: LGTM!

packages/pickle/native/internal/core/persistent_crypto_methods.go (1)

74-74: LGTM!

packages/openclaw/src/openclaw-extension.ts (1)

1-3: LGTM!

Also applies to: 5-22, 26-28

packages/openclaw/src/approval.test.ts (1)

3-4: LGTM!

Also applies to: 35-142, 203-245

packages/bridge/src/store.test.ts (1)

1-29: LGTM!

packages/bridge/src/types.ts (1)

163-165: LGTM!

Also applies to: 653-656, 894-902, 1044-1046, 1102-1102

packages/openclaw/src/openclaw-extension.test.ts (1)

3-3: LGTM!

Also applies to: 20-27, 35-44, 48-65, 80-90, 95-98, 112-119, 131-136, 147-165, 167-200, 204-228

packages/openclaw/src/config.test.ts (1)

5-7: LGTM!

Also applies to: 9-17, 22-25, 27-32, 58-72, 74-87, 93-93

packages/pickle/src/client.test.ts (1)

918-919: LGTM!

Also applies to: 960-1000, 1043-1047, 1081-1082, 1095-1096

packages/bridge/src/beeper.ts (1)

115-117: LGTM!

Also applies to: 125-126, 130-130, 139-139

packages/pickle/src/runtime-types.ts (1)

28-28: LGTM!

Also applies to: 31-33, 37-37, 46-46, 58-58

packages/pickle/src/beeper/auth.ts (1)

134-140: LGTM!

packages/openclaw/src/appservice.test.ts (1)

5-7: LGTM!

Also applies to: 29-29, 31-33, 54-86, 88-128, 130-255, 276-298

packages/bridge/src/provisioning.ts (1)

14-16: LGTM!

Also applies to: 28-32, 62-70, 181-206, 283-326

packages/pickle/native/internal/core/beeper_ai_run_test.go (1)

11-193: LGTM!

packages/openclaw/package.json (1)

175-178: Verify openclaw publish output bundles @beeper/pickle* despite moving them to devDependencies.

@beeper/pickle* are in packages/openclaw/package.json devDependencies only, and the source uses runtime value imports/re-exports from:

  • @beeper/pickle-bridge (e.g., src/connector.ts, src/beeper-channel-runtime.ts)
  • @beeper/pickle-ag-ui (e.g., src/beeper-turn-events.ts)

packages/openclaw/tsdown.config.ts sets deps.alwaysBundle: [/^@beeper\//], so the build is intended to bundle @beeper/* into dist. However, packages/openclaw/dist isn’t present in this snapshot, so the “no bare imports left in dist/*.mjs” part can’t be checked here—re-run after building and confirm dist contains no remaining @beeper/pickle* bare imports (or move required packages to dependencies/peerDependencies if any remain).

packages/openclaw/src/beeper-stream.test.ts (1)

3-353: LGTM!

packages/bridge/src/bridge.test.ts (1)

36-1423: LGTM!

packages/pickle/src/client-types.ts (1)

81-159: LGTM!

packages/openclaw/src/cli.ts (1)

4-189: LGTM!

packages/openclaw/src/beeper-channel-runtime.test.ts (1)

1-285: LGTM!

packages/pickle/src/generated-runtime-operations.ts (1)

5-5: LGTM!

Also applies to: 19-20, 25-25, 32-32, 44-44, 132-136, 310-328

packages/openclaw/src/bridge-agent.ts (1)

2-2: LGTM!

Also applies to: 7-7, 23-24, 28-30, 33-33, 52-56, 59-59, 79-80

packages/pickle/src/streams/beeper-message.ts (1)

50-50: LGTM!

Also applies to: 75-82, 89-90, 108-108, 125-125, 167-167, 177-177, 255-257, 263-267, 269-276, 303-303, 310-310, 320-320, 339-359, 393-395

packages/bridge/src/appservice-websocket.test.ts (1)

19-19: LGTM!

Also applies to: 25-25, 58-58, 66-73

packages/openclaw/src/registration.ts (1)

13-17: LGTM!

Also applies to: 26-29, 33-35, 38-45, 48-49, 52-53, 62-62, 68-83

packages/openclaw/src/backfill.test.ts (1)

1-3: LGTM!

Also applies to: 7-7, 17-17, 39-50, 55-56, 82-82, 95-97, 104-104, 153-159, 165-178, 190-193, 222-224, 234-241, 329-363, 365-395, 397-517, 520-531

packages/pickle/native/internal/core/appservice_test.go (1)

13-13: LGTM!

Also applies to: 43-51, 109-168, 275-333, 335-419, 545-560

packages/openclaw/src/appservice.ts (1)

1-9: LGTM!

Also applies to: 11-16, 24-24, 30-30, 40-55, 63-67, 71-110, 112-138, 158-158, 162-185, 193-199, 201-241

Comment on lines +1756 to +1760
#matchingRemoteTarget(existing: SentEvent[], partId: string | undefined, index: number): SentEvent | undefined {
if (partId) {
return existing[index] ?? existing[0];
}
return existing[index] ?? existing[0];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Match remote edit targets by partId instead of index-only fallback.

#matchingRemoteTarget ignores partId, so multipart edits/removals can update the wrong Matrix event when ordering differs from stored parts.

🛠️ Proposed fix
-      const target = this.#matchingRemoteTarget(existing.sent, part.id, index);
+      const target = this.#matchingRemoteTarget(existing, part.id, index);

-  `#matchingRemoteTarget`(existing: SentEvent[], partId: string | undefined, index: number): SentEvent | undefined {
-    if (partId) {
-      return existing[index] ?? existing[0];
-    }
-    return existing[index] ?? existing[0];
+  `#matchingRemoteTarget`(
+    existing: { db: Message[]; sent: SentEvent[] },
+    partId: string | undefined,
+    index: number,
+  ): SentEvent | undefined {
+    if (partId) {
+      const partIndex = existing.db.findIndex((message) => message.partId === partId);
+      if (partIndex >= 0) return existing.sent[partIndex];
+    }
+    return existing.sent[index] ?? existing.sent[0];
   }

Also applies to: 1529-1530

🤖 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/bridge/src/bridge.ts` around lines 1756 - 1760, The helper
`#matchingRemoteTarget` currently ignores the partId and always returns by index,
which causes multipart edits/removals to target the wrong SentEvent; update
`#matchingRemoteTarget`(SentEvent[] existing, partId, index) to first search
existing for an entry whose partId equals the provided partId and return that
SentEvent if found, otherwise fall back to existing[index] or existing[0]; apply
the same change to the other occurrence that uses the same index-only fallback
(the similar logic around the earlier occurrence referenced in the comment) so
both places consistently prefer partId matching before index fallback.

Comment on lines +60 to +67
export function defaultBeeperApprovalActions(decisions: readonly ApprovalDecision[] = ["allow_once", "allow_session", "allow_room", "deny"]): Record<string, unknown>[] {
return decisions.map((decision) => ({
decision: decision.replace(/_/gu, "-"),
id: decision.replace(/_/gu, "-"),
reactionKey: approvalReactionKey(decision),
title: approvalActionTitle(decision),
variant: decision === "deny" ? "destructive" : "secondary",
}));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't advertise approval scopes the resolver can't preserve.

defaultBeeperApprovalActions() exposes allow_session and allow_room, but toOpenClawApprovalResolvePayload() later collapses everything to approve / approve_always / deny. That means a click on "Allow This Room" is serialized as an always-approve, which widens the grant beyond what the UI promised.

Suggested fix
-export function defaultBeeperApprovalActions(decisions: readonly ApprovalDecision[] = ["allow_once", "allow_session", "allow_room", "deny"]): Record<string, unknown>[] {
+export function defaultBeeperApprovalActions(decisions: readonly ApprovalDecision[] = ["allow_once", "allow_always", "deny"]): Record<string, unknown>[] {
🤖 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/openclaw/src/approval.ts` around lines 60 - 67,
defaultBeeperApprovalActions currently advertises "allow_session" and
"allow_room" but the resolver (toOpenClawApprovalResolvePayload) collapses those
into always-approve, so the UI promises a narrower scope than is actually
granted; change defaultBeeperApprovalActions to only expose decisions that the
resolver can preserve (e.g., replace "allow_session" and "allow_room" with a
single "allow_always" and keep "allow_once" and "deny"), update the produced
id/title/reactionKey strings accordingly (use approvalReactionKey and
approvalActionTitle helpers with the new decision names) so UI labels match the
actual serialized decisions and no click will be misrepresented as a broader
grant.

Comment on lines +69 to +71
"backfillLimit": {
"type": "number",
"description": "Maximum OpenClaw messages to backfill per imported 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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Constrain backfillLimit to non-negative integers.

backfillLimit currently accepts fractional and negative values. For a message-count limit, schema should enforce integer >= 0 to prevent invalid runtime inputs.

Suggested schema change
-    "backfillLimit": {
-      "type": "number",
-      "description": "Maximum OpenClaw messages to backfill per imported session."
-    },
+    "backfillLimit": {
+      "type": "integer",
+      "minimum": 0,
+      "description": "Maximum OpenClaw messages to backfill per imported session."
+    },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"backfillLimit": {
"type": "number",
"description": "Maximum OpenClaw messages to backfill per imported session."
"backfillLimit": {
"type": "integer",
"minimum": 0,
"description": "Maximum OpenClaw messages to backfill per imported session."
🤖 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/openclaw/src/beeper-channel-config.schema.json` around lines 69 -
71, The backfillLimit JSON schema currently allows fractional and negative
numbers; update the backfillLimit property (in
beeper-channel-config.schema.json) to enforce non-negative integers by changing
its type from "number" to "integer" and adding "minimum": 0 so only integers >=
0 are valid; keep the existing description unchanged.

Comment on lines +70 to +95
async sendText(options: {
content?: Record<string, unknown>;
replyToId?: string | null;
roomId: string;
text: string;
threadRoot?: string | number | null;
}): Promise<SentEvent> {
const content = {
body: options.text,
msgtype: "m.text",
...options.content,
};
return await this.#queueRemoteText(options.roomId, withReplyRelation(content, options.replyToId));
}

async sendMedia(options: BeeperOutboundMedia & { roomId: string }): Promise<SentEvent> {
const bytes = options.bytes ?? (options.path ? await readFile(options.path) : undefined);
if (!bytes) {
throw new Error("Beeper media send requires bytes or a local file path.");
}
return await this.#queueRemoteMedia(options.roomId, {
bytes,
kind: options.kind ?? "file",
...(options.caption !== undefined ? { caption: options.caption } : {}),
...(options.filename !== undefined ? { filename: options.filename } : {}),
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

threadRoot is accepted but never serialized.

Both sendText() and sendMedia() take threadRoot, but the queued Matrix content only handles replies. Any caller trying to send into a thread will post a top-level message instead.

🤖 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/openclaw/src/beeper-channel-runtime.ts` around lines 70 - 95,
sendText() and sendMedia() accept threadRoot but never serialize it into the
Matrix event, so messages intended for threads are sent as top-level events;
update sendText (before calling `#queueRemoteText/withReplyRelation`) and
sendMedia (before calling `#queueRemoteMedia`) to attach an m.thread relation when
options.threadRoot is present (e.g., merge { "m.relates_to": { "m.thread": {
"event_id": options.threadRoot } } } into the content or into the media
payload), ensuring the thread relation is combined with any existing reply
relation produced by withReplyRelation so both reply and thread relations are
preserved.

it("uses an injected Beeper turn sender for live Matrix room turns", async () => {
const registry = await tempRegistry();
registry.upsertBinding(testBinding());
const runtime = runtimeWith({});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify call sites and function signature in bridge-agent test file
rg -n 'runtimeWith\(\{\}\)' packages/openclaw/src/bridge-agent.test.ts
rg -n 'function runtimeWith\(options' -A8 packages/openclaw/src/bridge-agent.test.ts

Repository: beeper/pickle

Length of output: 562


🏁 Script executed:

#!/bin/bash
rg -n 'runtimeWith\(\{\}\)' packages/openclaw/src/bridge-agent.test.ts
rg -n 'runtimeWith\(' packages/openclaw/src/bridge-agent.test.ts

Repository: beeper/pickle

Length of output: 373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show context around the problematic call and other runtimeWith usages
nl -ba packages/openclaw/src/bridge-agent.test.ts | sed -n '35,90p'
nl -ba packages/openclaw/src/bridge-agent.test.ts | sed -n '90,170p'
nl -ba packages/openclaw/src/bridge-agent.test.ts | sed -n '200,250p'

Repository: beeper/pickle

Length of output: 100


🏁 Script executed:

#!/bin/bash
set -euo pipefail

cat -n packages/openclaw/src/bridge-agent.test.ts | sed -n '35,90p'
cat -n packages/openclaw/src/bridge-agent.test.ts | sed -n '90,170p'
cat -n packages/openclaw/src/bridge-agent.test.ts | sed -n '200,250p'

Repository: beeper/pickle

Length of output: 7006


🏁 Script executed:

#!/bin/bash
rg -n 'runtimeWith\(\s*\{\s*\}\s*\)' packages/openclaw/src/bridge-agent.test.ts

Repository: beeper/pickle

Length of output: 97


Pass responses into runtimeWith in this test (missing required option at line 53)

runtimeWith requires responses: Record<string, unknown> (runtimeWith(options: { events?: ...; responses: ... })), but runtimeWith({}) omits it at packages/openclaw/src/bridge-agent.test.ts:53.

💡 Proposed fix
-    const runtime = runtimeWith({});
+    const runtime = runtimeWith({ responses: {} });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const runtime = runtimeWith({});
const runtime = runtimeWith({ responses: {} });
🤖 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/openclaw/src/bridge-agent.test.ts` at line 53, The test calls
runtimeWith without the required responses option; update the test where runtime
is constructed (the runtime variable using runtimeWith) to include a responses
property (e.g. responses: {} or the specific mock response map needed for this
test) so it matches the runtimeWith(options: { responses: Record<string,
unknown> }) signature; modify the runtimeWith invocation (runtimeWith({...})) to
pass responses accordingly.

Comment on lines +186 to +204
media: async (ctx: {
cfg: OpenClawSetupConfig;
to: string;
text?: string;
mediaUrl?: string;
mediaReadFile?: (filePath: string) => Promise<Buffer>;
replyToId?: string | null;
threadId?: string | number | null;
}) => beeperMessageSendResult(await beeperOutboundAdapter.sendMedia(ctx)),
payload: async (ctx: {
cfg: OpenClawSetupConfig;
to: string;
text?: string;
mediaUrl?: string;
mediaReadFile?: (filePath: string) => Promise<Buffer>;
payload?: unknown;
replyToId?: string | null;
threadId?: string | number | null;
}) => beeperMessageSendResult(await beeperOutboundAdapter.sendPayload(ctx)),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Media sends currently drop replyToId.

replyTo is advertised in the message capabilities and send.media accepts replyToId, but the media path never forwards it. sendPayload() inherits the same bug when it routes through sendMedia(), so replied media messages are sent as top-level posts.

Suggested fix
   send: {
@@
     media: async (ctx: {
       cfg: OpenClawSetupConfig;
       to: string;
       text?: string;
       mediaUrl?: string;
       mediaReadFile?: (filePath: string) => Promise<Buffer>;
       replyToId?: string | null;
       threadId?: string | number | null;
     }) => beeperMessageSendResult(await beeperOutboundAdapter.sendMedia(ctx)),
@@
   sendMedia: async (ctx: {
     to: string;
     text?: string;
     mediaUrl?: string;
     mediaReadFile?: (filePath: string) => Promise<Buffer>;
+    replyToId?: string | null;
     threadId?: string | number | null;
   }) => {
@@
     const mediaOptions = {
       roomId: resolveBeeperRoomTarget(ctx.to),
       ...(bytes !== undefined ? { bytes } : {}),
       ...(ctx.text !== undefined ? { caption: ctx.text } : {}),
       ...(filename ? { filename } : {}),
       ...(bytes === undefined ? { path: mediaUrl } : {}),
+      ...(ctx.replyToId ? { replyToId: ctx.replyToId } : {}),
       ...(ctx.threadId != null ? { threadRoot: String(ctx.threadId) } : {}),
     };
@@
       return await beeperOutboundAdapter.sendMedia({
         mediaUrl,
         text,
         to: ctx.to,
         ...(ctx.mediaReadFile !== undefined ? { mediaReadFile: ctx.mediaReadFile } : {}),
+        ...(ctx.replyToId ? { replyToId: ctx.replyToId } : {}),
         ...(ctx.threadId != null ? { threadId: ctx.threadId } : {}),
       });

Also applies to: 225-279

🤖 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/openclaw/src/setup.ts` around lines 186 - 204, The media and payload
send wrappers (media and payload in setup.ts) do not forward the replyToId
through to beeperOutboundAdapter—ensure that when calling
beeperOutboundAdapter.sendMedia and beeperOutboundAdapter.sendPayload from the
media and payload async functions you include the replyToId (and threadId)
fields from ctx in the forwarded context object; update both the media wrapper
and the payload wrapper (and any path where payload routes through media) to
pass ctx.replyToId (and ctx.threadId) so replied media/payload messages are sent
as replies rather than top-level posts.

Comment on lines +487 to +500
const beeperMessageToolActions = ["send", "react", "read"] as const satisfies readonly ChannelMessageActionName[];

function beeperToolTextResult(text: string) {
return { content: [{ type: "text" as const, text }], details: {} };
}

export const beeperMessageActions = {
resolveExecutionMode: () => "gateway" as const,
describeMessageTool: () => ({
actions: beeperMessageToolActions,
capabilities: [],
}),
supportsAction: ({ action }: { action: string }) =>
action === "send" || action === "react" || action === "read",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Expose mark_unread in the advertised action set.

handleAction() implements mark_unread, but describeMessageTool() and supportsAction() omit it. Any caller that checks capabilities first will reject the action before it reaches the handler.

Suggested fix
-const beeperMessageToolActions = ["send", "react", "read"] as const satisfies readonly ChannelMessageActionName[];
+const beeperMessageToolActions = ["send", "react", "read", "mark_unread"] as const satisfies readonly ChannelMessageActionName[];
@@
   supportsAction: ({ action }: { action: string }) =>
-    action === "send" || action === "react" || action === "read",
+    action === "send" || action === "react" || action === "read" || action === "mark_unread",

Also applies to: 530-534

🤖 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/openclaw/src/setup.ts` around lines 487 - 500, The advertised
actions omit "mark_unread" though handleAction implements it; update the action
set and support checks to include "mark_unread": add "mark_unread" to the
beeperMessageToolActions array (used by describeMessageTool) and include it in
the supportsAction predicate inside beeperMessageActions (and repeat the same
change for the other identical block later in the file). Ensure you update both
describeMessageTool()’s actions list and supportsAction() to recognize
"mark_unread" so callers that pre-flight-check capabilities won’t reject the
action.

Comment on lines +1094 to +1105
try {
await waitForAbort(ctx.abortSignal);
} finally {
startedBridges.delete(key);
if (hostRuntime && openClawPluginRuntime && hostRuntime !== openClawPluginRuntime) {
setBeeperChannelRuntimeForHost(openClawPluginRuntime, undefined);
}
await bridge.stop?.();
ctx.setStatus?.({
accountId: ctx.accountId,
running: false,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

stopAccount() does not let startAccount() finish its cleanup.

After startup, startBeeperGatewayAccount() waits only on ctx.abortSignal. stopBeeperGatewayAccount() stops the bridge and clears the map, but it never resolves that wait, so the finally block that clears the host/runtime mapping and reports shutdown may never run when the lifecycle manager calls stopAccount() directly.

Also applies to: 1186-1195

🤖 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/openclaw/src/setup.ts` around lines 1094 - 1105, The finally block
in startBeeperGatewayAccount (which awaits waitForAbort(ctx.abortSignal)) can be
skipped because stopBeeperGatewayAccount stops the bridge but never
resolves/aborts ctx.abortSignal; update the stop path to signal the start wait
so cleanup runs: in stopBeeperGatewayAccount / stopAccount invoke the
AbortController tied to ctx.abortSignal (e.g., ctx.abortController?.abort() or
otherwise trigger the same abort signal) or otherwise resolve the waitForAbort
promise before/after stopping the bridge; ensure you reference and use the same
ctx object (ctx.abortSignal / ctx.abortController) so startedBridges.delete(key)
and the host/runtime cleanup in the startBeeperGatewayAccount finally block
always execute.

Comment on lines +63 to +67
run := aistream.NewRun(req.RunID, req.ThreadID, req.Model, req.AgentID, req.AgentName, time.Now())
writer := aistream.NewWriter(run, time.Now)
writer.Start()
c.beeperAIRuns[run.RunID] = &beeperAIRunState{run: run, writer: writer}
return c.marshalBeeperAIRunSnapshot(run, outboundEventsFromAGUI(run.Events))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject duplicate Beeper AI run IDs on begin.

Line 66 overwrites existing state for the same RunID, which can drop in-flight run data on retries or duplicate calls.

Proposed fix
 func (c *Core) handleBeginBeeperAIRun(payload []byte) ([]byte, error) {
@@
-	run := aistream.NewRun(req.RunID, req.ThreadID, req.Model, req.AgentID, req.AgentName, time.Now())
+	runID := strings.TrimSpace(req.RunID)
+	if runID == "" {
+		return nil, errors.New("missing Beeper AI run ID")
+	}
+	if c.beeperAIRuns[runID] != nil {
+		return nil, errors.New("Beeper AI run is already registered")
+	}
+	run := aistream.NewRun(runID, req.ThreadID, req.Model, req.AgentID, req.AgentName, time.Now())
 	writer := aistream.NewWriter(run, time.Now)
 	writer.Start()
 	c.beeperAIRuns[run.RunID] = &beeperAIRunState{run: run, writer: writer}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
run := aistream.NewRun(req.RunID, req.ThreadID, req.Model, req.AgentID, req.AgentName, time.Now())
writer := aistream.NewWriter(run, time.Now)
writer.Start()
c.beeperAIRuns[run.RunID] = &beeperAIRunState{run: run, writer: writer}
return c.marshalBeeperAIRunSnapshot(run, outboundEventsFromAGUI(run.Events))
runID := strings.TrimSpace(req.RunID)
if runID == "" {
return nil, errors.New("missing Beeper AI run ID")
}
if c.beeperAIRuns[runID] != nil {
return nil, errors.New("Beeper AI run is already registered")
}
run := aistream.NewRun(runID, req.ThreadID, req.Model, req.AgentID, req.AgentName, time.Now())
writer := aistream.NewWriter(run, time.Now)
writer.Start()
c.beeperAIRuns[run.RunID] = &beeperAIRunState{run: run, writer: writer}
return c.marshalBeeperAIRunSnapshot(run, outboundEventsFromAGUI(run.Events))
🤖 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/pickle/native/internal/core/beeper_ai_run.go` around lines 63 - 67,
When beginning a new Beeper AI run, the code currently unconditionally
overwrites c.beeperAIRuns[run.RunID], dropping in-flight data; modify the Begin
logic around aistream.NewRun / aistream.NewWriter / writer.Start so it first
checks c.beeperAIRuns for an existing entry for run.RunID and if present rejects
the duplicate (return an error or the existing snapshot via
c.marshalBeeperAIRunSnapshot and outboundEventsFromAGUI) instead of replacing
the map entry; ensure you only call NewWriter/Start and insert into
c.beeperAIRuns when no existing runID is found and preserve beeperAIRunState for
concurrent/retry calls.

backupVersion id.KeyBackupVersion
beeperStream *beeperstream.Helper
beeperStreamMessages map[id.EventID]*beeperStreamMessage
beeperAIRuns map[string]*beeperAIRunState
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear Beeper AI run state during core shutdown.

beeperAIRuns is initialized but never reset in handleClose, so stale run state can survive a close/re-init cycle and leak across sessions.

Proposed fix
 func (c *Core) handleClose() ([]byte, error) {
@@
 	c.beeperStream = nil
+	c.beeperAIRuns = make(map[string]*beeperAIRunState)
 	c.appserviceProcessor = nil

Also applies to: 58-58

🤖 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/pickle/native/internal/core/core.go` at line 26, The beeperAIRuns
map is not cleared on shutdown; modify the core shutdown logic in handleClose
(in core.go) to reset or reinitialize the beeperAIRuns field (e.g., assign an
empty map or nil) to avoid leaking stale run state across re-inits; ensure you
do this for the same cleanup path referenced at both occurrences around
beeperAIRuns so any existing entries are dropped during close.

@batuhan
Copy link
Copy Markdown
Member Author

batuhan commented May 28, 2026

@indent review

@indent
Copy link
Copy Markdown

indent Bot commented May 28, 2026

PR Summary

This PR introduces a new ~20k LOC @beeper/openclaw bridge package that exposes OpenClaw sessions and agents over Matrix as a first-class Beeper channel plugin, plus the supporting changes in packages/bridge, packages/pickle, and the Go native core required to route the new event surface (edits, reaction removals, receipts, room state, account_data, membership, marked-unread, delete-chat) and to host AG-UI run streams. The shared bridge code now handles a wider set of Matrix-→remote and remote-→Matrix events on behalf of every connector, the appservice-websocket layer hands transactions to the native pipeline instead of constructing events itself, and the native Core adds a Beeper AI run lifecycle plus a refactored stream publisher that can fan out to a room when no subscribers are present.

  • New packages/openclaw package: channel plugin manifest, ClawHub/npm packaging, pickle-openclaw CLI (login/whoami), bridge runtime, connector, setup/dashboard entry, approval mapping, backfill planner/executor, registry, Matrix parser, and protocol-coverage manifest.
  • packages/bridge: subscribes to receipt/accountData/membership/roomState in addition to existing kinds; adds #dispatchMatrix{Edit,ReactionRemove,Receipt,MarkedUnread,RoomName,RoomTopic,RoomAvatar,Membership,DeleteChat} and #handleRemote{Edit,Reaction,ReactionRemove,MessageRemove,ReadReceipt,DeliveryReceipt,MarkUnread,Typing}; provisioning gains /contacts and /backfill/:roomId; MatrixBridgeDataStore now tolerates corrupt JSON; appservice-websocket no longer constructs events directly.
  • Go native: appservice.go accepts account_data/ephemeral/room_account_data sections and classifies events via a new classifyAppserviceEventClass; messages.go reworks Beeper stream publishing (subscribers vs. room fan-out); beeper_ai_run.go adds begin/append/finish/error/delete operations; persistent crypto store drops SenderKey from the message-index key to match the bumped mautrix interface; makePortalCreateRoomRequest defers to homeserver-assigned room IDs.
  • Pickle TS: MatrixClient.beeper.aiRuns.{begin,appendEvent,finish,error,delete} plus generated runtime types; Beeper auth accepts onlyExistingAccounts: false and tolerates empty/invalid JSON; streams/beeper-message.ts migrates the accumulator schema (textcontent, dynamic-tool/tool-${name}tool-call, new state literals); appservice.createPortalRoom plumbs creationContent.
  • Tooling: bumps mautrix-go, ai-bridge, and several indirect Go deps; new tests across bridge dispatchers, provisioning, store, Beeper auth, openclaw modules, and the native appservice/Beeper-stream/AI-run paths.

Issues

15 potential issues found:

  • Crypto store collapses message-index entries on snapshot reload when dropping SenderKey: applySnapshot now keys messageIndices only on (SessionID, Index), so an old snapshot with multiple (SenderKey, SessionID, Index) entries silently loses all but the last one — with no migration log or rejection. Beyond migration data loss, the replay guard can no longer distinguish replays from different senders that share (sessionID, index).</issue_text>
    <issue_severity>latent</issue_severity>
    <fix_agent_prompt>Edit packages/pickle/native/internal/core/persistent_crypto_load.go:107-112. While applying the snapshot, detect when two entries collide on (SessionID, Index) with different SenderKeys and at minimum log a warning. Consider gating the load behind a snapshot-version bump so old snapshots are explicitly migrated (or the load fails loudly) rather than silently dropping data. Also document in the surrounding files why the SenderKey was removed (mautrix v0.27.1-0.20260513… changed the crypto.Store.ValidateMessageIndex signature).</fix_agent_prompt>
    <inline_comment>{"file_path": "packages/pickle/native/internal/core/persistent_crypto_load.go", "line_number": 110, "text": "Silent collapse of old snapshots. Old snapshots persisted with the previous key shape can contain multiple entries that share (SessionID, Index) across different SenderKeys. Looping with map-assignment here just overwrites — last writer wins — with no warning or version check.\n\nThis is two issues bundled:\n1. Migration data loss. Anything that relied on the previous per-sender indices is dropped without trace on the first load after upgrading.\n2. Replay-guard semantics weakened going forward. ValidateMessageIndex can no longer distinguish a true replay from a legitimate decryption performed by a different sender that happens to share (sessionID, index).\n\nSuggest at minimum detecting the collision here and logging once per affected session, and ideally bumping the snapshot version so a v1 snapshot is migrated explicitly. Worth mentioning the upstream mautrix interface change in a comment so future readers know why SenderKey is gone."}</inline_comment>
    Autofix
  • pickle-openclaw login has no way to supply the email login code non-interactively: promptForLoginCode unconditionally opens a readline on process.stdin with no TTY guard and no --code/BEEPER_LOGIN_CODE flag, even though every other input (--email, --bridge-manager-token, …) supports a CLI flag and setupOptionsFromInput already accepts input.code. Piped or CI usage hangs or throws an opaque "Missing Beeper login code".</issue_text>
    <issue_severity>functional</issue_severity>
    <fix_agent_prompt>Edit packages/openclaw/src/cli.ts:25-55, 156-183. Add a --code <code> flag (and read BEEPER_LOGIN_CODE env var) alongside --email/--bridge-manager-token, wire it into setupOptionsFromInput as input.code, and only call promptForLoginCode when no code was supplied. Guard promptForLoginCode with process.stdin.isTTY and throw a clear error if stdin isn't a TTY and no code flag/env is set. Add a CLI test covering the --code path.</fix_agent_prompt>
    Autofix
  • Tool-part schema in compactParts is no longer normalized: previously every tool part round-tripped through compactAIMessage was coerced to type: "tool-call", but the new code preserves part.type as-is while the accumulator only ever emits tool-call. Downstream Beeper Matrix clients that key on the previous dynamic-tool / tool-${toolName} literals (or on state values like input-available) will no longer match accumulated parts. Out of this repo's test coverage; flag for client-side compatibility coordination.</issue_text>
    <issue_severity>latent</issue_severity>
    <fix_agent_prompt>Decide whether the schema change is intentional. If yes, document the new contract in the package README or a CHANGELOG and coordinate with downstream Matrix clients (iOS/Android/Desktop) before shipping. If unintentional, either keep the old type literals in applyFinalMessagePart (packages/pickle/src/streams/beeper-message.ts:67-90) or restore the previous normalization in compactParts (packages/pickle/src/streams/beeper-message.ts:343-368).</fix_agent_prompt>
    Autofix
  • messageFromSentEvent uses new Date() for the synthetic Message.timestamp instead of reading origin_server_ts from sent.raw. Any connector whose convertEdit/convertRemove consults existing[i].timestamp for ordering, dedup, or conflict resolution will always see "now" rather than the real send time.</issue_text>
    <issue_severity>latent</issue_severity>
    <fix_agent_prompt>Edit packages/bridge/src/bridge.ts:2103-2110. Extract origin_server_ts (or fall back to a timestamp field) from sent.raw and use new Date(originServerTs) when present, falling back to new Date() only when the raw event is missing. The signature already receives sent: SentEvent, so no caller change is needed.</fix_agent_prompt>
    <inline_comment>{"file_path": "packages/bridge/src/bridge.ts", "line_number": 2107, "text": "new Date() here loses the original send time. SentEvent.raw holds the full Matrix event (including origin_server_ts), so the synthetic Message could carry the real timestamp:\n\nts\nconst originServerTs = isRecord(sent.raw) && typeof sent.raw.origin_server_ts === \"number\"\n ? sent.raw.origin_server_ts\n : undefined;\nreturn { id: messageId, mxid: sent.eventId, partId, timestamp: originServerTs ? new Date(originServerTs) : new Date() };\n\n\nNo connector reads timestamp today, but the field is part of the Message contract; any future convertEdit/convertRemove that orders or dedups by timestamp will be misled."}</inline_comment>
    Autofix
  • Appservice transactions no longer update the appservice state store: dispatchAppserviceEvents only routes EventMessage|EventReaction|EventRedaction|EventEncrypted through processEvent; every other event (including m.room.member/m.room.encryption) is emitted but never applied to mautrix.NewMemoryStateStore(). Intents that consult the state store for encryption/membership knowledge will see stale data on pure-appservice deployments.</issue_text>
    <issue_severity>latent</issue_severity>
    <fix_agent_prompt>Edit packages/pickle/native/internal/core/appservice.go:226-257. In dispatchAppserviceEvents, also apply state events into the appservice's stateStore (e.g., as.stateStore.UpdateState(evt) for state events) before/after emitting. Add a test that submits an m.room.member invite via the appservice transaction and asserts that stateStore.GetMembership(roomID, userID) reflects the new membership.</fix_agent_prompt>
    Autofix
  • Own-user filtering in the new Matrix dispatchers does not cover ghost echoes: event.sender.isMe || event.sender.userId === this.#ownUserId is false for events sent by the bridge as an appservice ghost (the ghost is neither the device user nor the appservice bot, and toGenericEvent hard-codes isMe: false). Once the bridge starts dispatching edits/reaction-removals/membership for appservice traffic, ghost-authored echoes can be re-dispatched to connector handlers, risking edit/reaction loops or duplicated processing.</issue_text>
    <issue_severity>latent</issue_severity>
    <fix_agent_prompt>Edit packages/bridge/src/bridge.ts to additionally suppress events whose sender matches the appservice user namespace (e.g., add appserviceOwnsUserId(event.sender.userId) derived from appserviceOptions.registration.namespaces.user_ids). Apply the check at the top of #dispatchMatrixMessage, #dispatchMatrixEdit, #dispatchMatrixReaction, #dispatchMatrixReactionRemove, #dispatchMatrixMembership, #dispatchMatrixRoomName/Topic/Avatar. Add a unit test that submits a message authored by a ghost user namespace match and asserts no connector handler fires.</fix_agent_prompt>
    Autofix
  • pickle-openclaw whoami ignores the default config written by login: loadConfig only reads from disk when --config is supplied and otherwise constructs a fresh in-memory default, so the advertised login → whoami flow always reports canConnect: false / no token unless the user re-passes --config to every command.</issue_text>
    <issue_severity>functional</issue_severity>
    <fix_agent_prompt>Edit packages/openclaw/src/cli.ts:105-109 loadConfig so that when --config is omitted it tries to read defaultConfigPath() first (catching ENOENT) and only falls back to createDefaultConfig(configOverridesFromOptions(options)) if nothing is on disk. Update the related test (or add one) in packages/openclaw/src/cli.test.ts exercising whoami after login without explicitly passing --config.</fix_agent_prompt>
    <inline_comment>{"file_path": "packages/openclaw/src/cli.ts", "line_number": 108, "text": "whoami ignores the saved login config. pickle-openclaw login writes the config to defaultConfigPath(), but loadConfig here only reads from disk when --config is supplied — without the flag it always returns a fresh in-memory default. So pickle-openclaw whoami immediately after pickle-openclaw login reports canConnect: false and a null access token, breaking the documented developer flow.\n\nSuggest: when configPath is undefined, try readConfig(defaultConfigPath()) and only fall through to createDefaultConfig(...) on ENOENT."}</inline_comment>
    Autofix
  • Beeper stream advertises the wrong marker type when no subscribers are passed: handleStartBeeperStreamMessage forces the carrier com.beeper.stream marker to {"type": aistream.BeeperAIStreamDeltas} (i.e. com.beeper.ai) regardless of the requested stream type, but handlePublishBeeperStreamMessagePart later publishes deltas under com.beeper.llm.deltas (or whatever streamType was set). Passive observers can't correlate the marker with the deltas, and the missing descriptor means late subscribers can't join.</issue_text>
    <issue_severity>latent</issue_severity>
    <fix_agent_prompt>Edit packages/pickle/native/internal/core/messages.go:151-157. Build the no-subscriber marker from the actual stream type (e.g., {"type": req.StreamType} if set, falling back to BeeperAIStreamKey) so the carrier and delta keys agree. Consider also still emitting the descriptor (with subscribers omitted) so late subscribers can attach. Add a test that publishes a non-subscriber stream with streamType: "com.beeper.llm" and asserts the marker matches com.beeper.llm.</fix_agent_prompt>
    Autofix
  • OpenClawNetworkAPI.disconnect() tears down a runtime that is shared across all logins: the connector caches a single OpenClawPluginRuntimeAdapter (runtimeFactory returns the cached instance for every login), so calling disconnect() on one login closes the transport for every other live login. Only dormant today because there is exactly one login, but the connector already supports multi-login.</issue_text>
    <issue_severity>latent</issue_severity>
    <fix_agent_prompt>Edit packages/openclaw/src/connector.ts:276-278. disconnect() should not unconditionally close the cached runtime — either reference-count consumers of the cached runtime and close only when the last login disconnects, or stop sharing the runtime across logins entirely. Add a test that creates two logins, disconnects one, and asserts the other can still send/receive.</fix_agent_prompt>
    Autofix
  • BeeperChannelRuntime.createStreamPublisher overwrites the entry in #activeStreams for the same sessionKey without finalizing or aborting the previous BeeperTurnStreamCoordinator. The orphaned coordinator retains its accumulator and serial-queue work until process exit, so repeatedly restarting a turn mid-stream leaks memory unboundedly.</issue_text>
    <issue_severity>latent</issue_severity>
    <fix_agent_prompt>Edit packages/openclaw/src/beeper-channel-runtime.ts:149-165. Before this.#activeStreams.set(options.sessionKey, publisher), read the previous entry and call its abort/dispose method (or finalize with an aborted reason). Add a test that calls createStreamPublisher twice with the same sessionKey and asserts the first coordinator was finalized exactly once.</fix_agent_prompt>
    Autofix
  • matrixMembershipAction cannot detect revoke_invite: the function reads event.unsigned?.prev_content?.membership, but toGenericEvent in packages/pickle/src/events.ts never sets unsigned (the underlying MatrixSyncEvent type doesn't even expose it). Any membership === "leave" event is therefore classified as leave/kick, and MatrixMembership.action === "revoke_invite" will never be emitted to connectors.</issue_text>
    <issue_severity>latent</issue_severity>
    <fix_agent_prompt>Either (a) extend MatrixSyncEvent / toGenericEvent (in packages/pickle/src/generated-runtime-types.ts source + packages/pickle/src/events.ts:50-72) to surface unsigned (especially prev_content) on generic events, then keep the existing matrixMembershipAction logic, or (b) make matrixMembershipAction pull prev_content out of event.raw.unsigned instead of event.unsigned. Add a bridge.test.ts case asserting that an inviteleave transition (where the leaving user equals the invited user) emits revoke_invite.</fix_agent_prompt>
    <inline_comment>{"file_path": "packages/bridge/src/bridge.ts", "line_number": 1929, "text": "event.unsigned?.prev_content is always undefined here. toGenericEvent in packages/pickle/src/events.ts:50-72 does not pass unsigned through to MatrixGenericEvent (the runtime MatrixSyncEvent type doesn't expose it; the data only lives inside event.raw.unsigned). So prevMembership is always undefined and the branch returning \"revoke_invite\" is unreachable — every membership === \"leave\" is classified as leave or kick.\n\nEither propagate unsigned from the native sync layer through to MatrixGenericEvent, or rewrite this function to dig prev_content out of event.raw.unsigned (const raw = isRecord(event.raw) ? event.raw : undefined; const prevContent = isRecord(raw?.unsigned?.prev_content) ? raw.unsigned.prev_content : undefined;)."}</inline_comment>
    Autofix
  • approvalReactionsEnabled is hard-wired to return false with no configuration knob, so every reaction-based approval is silently dropped with ignored: "approval-reactions-disabled". Either intentional (then document) or a forgotten feature flag.</issue_text>
    <issue_severity>nit</issue_severity>
    <fix_agent_prompt>If reaction-based approvals are intentionally disabled, add a comment in packages/openclaw/src/connector.ts:732-734 explaining why and link to a tracking issue. If they should be supported, expose a config flag (e.g., OpenClawBridgeConfig.beeper.approvalReactions: boolean) wired through OpenClawNetworkAPI.</fix_agent_prompt>
    Autofix
  • toolDynamicByCallId in packages/pickle/src/streams/beeper-message.ts is now dead state: it is still populated by rememberTool but never read after the refactor that always emits type: "tool-call". Safe to delete to avoid per-call-id memory growth and reader confusion.</issue_text>
    <issue_severity>nit</issue_severity>
    <fix_agent_prompt>Edit packages/pickle/src/streams/beeper-message.ts. Remove the toolDynamicByCallId field from BeeperFinalMessageAccumulator, the assignment in rememberTool, and any helpers that initialize it. Run the existing tests to confirm no regressions.</fix_agent_prompt>
    Autofix
  • #matchingRemoteTarget ignores partId: both branches of if (partId) return existing[index] ?? existing[0], so the function selects the stored Matrix event purely by position. For multi-part remote messages where converted.modifiedParts ordering differs from the stored part ordering, #handleRemoteEdit will call messages.edit against the wrong eventId and then overwrite the messagePartKey(messageId, part.id) store entry with that wrong mxid.</issue_text>
    <issue_severity>functional</issue_severity>
    <fix_agent_prompt>Edit packages/bridge/src/bridge.ts:1756-1761. #matchingRemoteTarget should select the stored SentEvent whose partId matches partId. The caller #handleRemoteEdit (~line 1525-1545) has the parallel existing.db: Message[] array (built in #remoteTargetMessages ~lines 1724-1748) where each entry carries partId. Either (a) pass existing.db into #matchingRemoteTarget and look up db.findIndex((m) => m.partId === partId) then return existing.sent[i] ?? existing.sent[index] ?? existing.sent[0], or (b) drop the partId parameter entirely if positional matching is truly desired and document why. Add a unit test in bridge.test.ts covering an edit where modifiedParts are returned out of order from the stored parts.</fix_agent_prompt>
    <inline_comment>{"file_path": "packages/bridge/src/bridge.ts", "line_number": 1760, "text": "Both branches of if (partId) return the same expression — the partId parameter is read but never used to influence the result. The only caller (#handleRemoteEdit, ~line 1529) loops over converted.modifiedParts.entries() and uses the returned target.eventId for messages.edit(...), then stores messagePartKey(event.getTargetMessage(), part.id ?? String(index)). For multi-part remote messages where the modified-parts order doesn't match the stored part order, the edit will target the wrong Matrix event and write the new mxid under the wrong storage key. Looks like the intent was to look up the SentEvent whose stored partId matches partId — the parallel Message[] array built in #remoteTargetMessages (~lines 1724-1748) carries partId for exactly that purpose."}</inline_comment>
    Autofix
  • classifyAppserviceEventClass whitelist is incomplete: only 5 state event types are explicitly classified as StateEventType. Common state events (m.room.power_levels, m.room.create, m.room.canonical_alias, m.room.join_rules, m.room.tombstone, m.room.history_visibility, m.bridge, uk.half-shot.bridge, m.space.child, m.room.pinned_events, m.room.server_acl, m.room.guest_access) fall to evtType.Class, which is the zero value UnknownEventType after JSON unmarshal — downstream Go consumers that branch on evt.Type.Class == StateEventType will skip them.</issue_text>
    <issue_severity>latent</issue_severity>
    <fix_agent_prompt>Edit packages/pickle/native/internal/core/appservice.go:312-321. Replace the explicit type whitelist with a call into mautrix's own type classification (e.g., look up via event.Type.IsState() or the event.TypeMap after parsing), so any state event picks up StateEventType. Or expand the switch to cover every state-event type mautrix exposes. Add a test asserting m.room.power_levels from an appservice transaction is emitted with class == \"state\".</fix_agent_prompt>
    Autofix

CI Checks

Waiting for CI checks...


⚡ Autofix All Issues

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@package.json`:
- Line 2: The root package.json currently duplicates the workspace package
identity by having "name": "`@beeper/openclaw`" (same as
packages/openclaw/package.json); remove or change the root "name" to a distinct
value (or drop it entirely) and ensure "private": true remains, so pnpm
workspace selection isn’t ambiguous; leave the existing "scripts.test:go"
delegating to "pnpm --filter `@beeper/pickle` test:go" unchanged.
🪄 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: 6cb480b5-b73c-4f11-b391-cca54d7c8e45

📥 Commits

Reviewing files that changed from the base of the PR and between 7274fad and 1c6b06c.

📒 Files selected for processing (5)
  • package.json
  • packages/openclaw/README.md
  • packages/openclaw/package.json
  • packages/openclaw/scripts/copy-runtime-assets.mjs
  • packages/openclaw/src/openclaw-extension.test.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/openclaw/README.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/openclaw/scripts/copy-runtime-assets.mjs
  • packages/openclaw/src/openclaw-extension.test.ts
📜 Review details
🔇 Additional comments (2)
package.json (1)

18-18: LGTM!

packages/openclaw/package.json (1)

2-2: LGTM!

Also applies to: 147-148

Comment thread package.json
@@ -1,5 +1,5 @@
{
"name": "pickle-monorepo",
"name": "@beeper/openclaw",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify duplicate package names across all package.json files.
fd -a '^package\.json$' | while read -r f; do
  name=$(jq -r '.name // empty' "$f")
  if [ -n "$name" ]; then
    printf "%s\t%s\n" "$name" "$f"
  fi
done | sort | awk -F'\t' '
{
  count[$1]++
  files[$1]=files[$1] "\n  - " $2
}
END {
  for (n in count) {
    if (count[n] > 1) {
      print "DUPLICATE:", n, "(" count[n] " files)" files[n]
    }
  }
}'

Repository: beeper/pickle

Length of output: 185


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show relevant fields from root and workspace package
echo "=== ./package.json ==="
jq -r '{name, private, scripts_test_go: (.scripts["test:go"] // null)}' package.json

echo
echo "=== ./packages/openclaw/package.json ==="
jq -r '{name, private, scripts_test_go: (.scripts["test:go"] // null)}' packages/openclaw/package.json

echo
# Validate what `@beeper/pickle`'s scripts.test:go actually is (best-effort)
# Find package.json(s) with name "`@beeper/pickle`"
fd -a 'package\.json$' . -S | while read -r f; do
  n=$(jq -r '.name // empty' "$f" 2>/dev/null || true)
  if [ "$n" = "`@beeper/pickle`" ]; then
    echo "=== $f (name=`@beeper/pickle`) ==="
    jq -r '{path: "'"$f"'", scripts_test_go: (.scripts["test:go"] // null)}' "$f"
  fi
done | head -n 50

Repository: beeper/pickle

Length of output: 443


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find all package.json files and print scripts.test:go for the package(s) named `@beeper/pickle`
fd -t f 'package\.json$' . | while read -r f; do
  n="$(jq -r '.name // empty' "$f" 2>/dev/null || true)"
  if [ "$n" = "`@beeper/pickle`" ]; then
    echo "=== $f (name=`@beeper/pickle`) ==="
    jq -r '{path: "'"$f"'", scripts_test_go: (.scripts["test:go"] // null)}' "$f"
  fi
done

Repository: beeper/pickle

Length of output: 228


Avoid duplicate workspace package identity.

  • Line 2: root "name": "@beeper/openclaw" duplicates packages/openclaw/package.json’s "name": "@beeper/openclaw", which can cause workspace/package selection ambiguity in pnpm tooling—rename the root to a distinct identity (keep it private) or drop the root name.
  • Line 18: the test:go delegation to pnpm --filter @beeper/pickle test:go is correct; @beeper/pickle runs cd native && go test -tags goolm ./....
🤖 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 `@package.json` at line 2, The root package.json currently duplicates the
workspace package identity by having "name": "`@beeper/openclaw`" (same as
packages/openclaw/package.json); remove or change the root "name" to a distinct
value (or drop it entirely) and ensure "private": true remains, so pnpm
workspace selection isn’t ambiguous; leave the existing "scripts.test:go"
delegating to "pnpm --filter `@beeper/pickle` test:go" unchanged.

@indent
Copy link
Copy Markdown

indent Bot commented May 28, 2026

Review posted on the banner above. Highlights:

Functional

  • #matchingRemoteTarget (packages/bridge/src/bridge.ts:1756-1761) has identical branches — partId is read but never used, so multi-part remote edits can target the wrong Matrix event.
  • pickle-openclaw whoami ignores the config that login just wrote when --config is omitted (packages/openclaw/src/cli.ts:105-109).
  • pickle-openclaw login has no --code/env fallback or TTY guard for the email code — unusable from CI/piped stdin.

Latent

  • Crypto store silently collapses old messageIndices snapshots when dropping SenderKey, with no migration log, and weakens replay-guard sender attribution going forward.
  • matrixMembershipAction cannot detect revoke_invite because event.unsigned is never propagated through toGenericEvent.
  • messageFromSentEvent uses new Date() instead of origin_server_ts from sent.raw.
  • OpenClawNetworkAPI.disconnect() closes a runtime cached across all logins.
  • BeeperChannelRuntime.createStreamPublisher overwrites the prior coordinator for the same sessionKey without finalizing it.
  • Appservice transactions no longer update the appservice state store for non-message events.
  • classifyAppserviceEventClass whitelist misses many common state event types.
  • Non-subscriber Beeper stream advertises the wrong marker type vs. the deltas it later publishes.
  • Own-user filter in the new Matrix dispatchers doesn't cover ghost echoes arriving via appservice.
  • Tool-part schema change in streams/beeper-message.ts may break downstream Beeper clients that key on dynamic-tool / tool-${toolName}.

Nit

  • approvalReactionsEnabled hard-wired to false with no flag.
  • toolDynamicByCallId is now dead state.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant