Skip to content

Add OpenClaw bridge runtime and provisioning support#7

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

Add OpenClaw bridge runtime and provisioning support#7
batuhan wants to merge 57 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

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new OpenClaw Beeper plugin/package with connector/runtime/CLI, extensive bridge dispatch/provisioning/types updates, streaming/approval helpers, persistent registry/rooms, and native Go AI-run lifecycle/streams, with tests and configs across packages.

Changes

OpenClaw Beeper Channel + Bridge Integration

Layer / File(s) Summary
Plugin, setup, CLI surfaces
packages/openclaw/src/openclaw-extension.ts, .../setup-entry.ts, .../plugin-entry.ts, .../cli.ts
Defines Beeper channel plugin entry, setup entry, and CLI for login/whoami and config persistence.
Connector and runtime adapters
packages/openclaw/src/connector.ts, .../openclaw-runtime.ts, .../beeper-channel-runtime.ts
Implements connector, host/plugin runtime adapters, and outbound Beeper runtime including streaming.
Registry, rooms, config, IDs
packages/openclaw/src/registry.ts, .../rooms.ts, .../config.ts, .../ids.ts
Adds persistent registry, session room creation, config helpers, and bridge-id derivation.
Approvals and parsing
packages/openclaw/src/approval.ts, .../matrix-parser.ts, .../beeper-turn-events.ts
Adds approval parsing/notice helpers, Matrix text parsing, and approval event mappers.
Bridge dispatch/provisioning/types
packages/bridge/src/bridge.ts, .../provisioning.ts, .../types.ts
Expands Matrix event dispatch (edits/reaction-remove/receipts/typing/state/membership), adds contacts listing and room creation content.
Beeper streams/media helpers
packages/bridge/src/beeper-stream.ts, .../media-message.ts
Introduces AI-run stream wrapper and media upload/content helpers.
Native Go core AI-run lifecycle
packages/pickle/native/internal/core/*
Adds non-stream and stream AI-run handlers, carrier publishing, and supporting ops/constants.

Sequence Diagram(s)

(skipped)

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • beeper/pickle#5: Also modifies bridge provisioning HTTP proxy flow; overlaps with added listContacts handling and proxy path changes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch batuhan/oc-2

@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 thread packages/bridge/src/bridge.ts
Comment thread packages/openclaw/src/approval.ts Outdated
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 thread packages/openclaw/src/beeper-channel-runtime.ts
Comment thread packages/openclaw/src/bridge-agent.test.ts
Comment thread packages/openclaw/src/setup.ts
Comment thread packages/openclaw/src/setup.ts Outdated
Comment thread packages/openclaw/src/setup.ts
Comment thread packages/pickle/native/internal/core/beeper_ai_run.go Outdated
Comment thread packages/pickle/native/internal/core/core.go
@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

Adds a new @beeper/openclaw bridge package and a substantial refactor of the bridge core to wire OpenClaw plugins into Beeper via bridgev2, including new native operations for AI-run lifecycle/streaming, appservice ghost profile sync, a BridgeRoomStateAPI, multi-account onboarding keyed on Matrix user IDs, an auth-presence probe for the OpenClaw plugin contract, and (in the latest commits) release pipeline hardening so the workspace can publish cleanly.

  • New @beeper/openclaw package: plugin runtime adapter, CLI (login/whoami), connector mapping OpenClaw channels to Beeper portals.
  • packages/bridge refactor: RuntimeBridge now handles edits, reaction removals, room state, membership, marked-unread, delete-chat; ChatInfoChange reshaped; new BeeperTurnStream, BridgeRoomStateAPI, createRemoteChatInfoChange.
  • Native Go core: new beeper_ai_run ops with run-id validation and reset-on-close, appservice_set_profile, roomState.{get,set}, marker/finalize fixes, local beeperStreamFinalEdit{Extra,TopLevelExtra} helpers replacing the upstream aibridgev2 envelope dep.
  • Ghost profile sync: registerGhost is async and pushes display name / avatar / Beeper profile through appservice intents.
  • Crypto store: messageIndices key dropped SenderKey to match upstream mautrix interface change (silent snapshot migration — flagged).
  • CLI: --access-token flag, --server-env flag rename with help-text fix, productionprod.
  • Multi-account onboarding: Matrix-user-ID-keyed accounts.<mxid>, SecretInput-shaped asToken/hsToken, secret target collapses to accounts.*.{asToken,hsToken}, applyBeeperSetupConfig returns { accountId, cfg }, inbound turn handler requires matrix.accountId, auth-presence.ts wired into plugin manifest's configuredState/persistedAuthState/cliAddOptions.
  • Provisioning: new POST /_matrix/provision/v3/search_users, snake_case capability fields.
  • Beeper auth: createBeeperLogin now supports acceptTerms/username and auto-registers via /user/register when leadToken is returned and onlyExistingAccounts: false.
  • BeeperChannelRuntime sendText/sendMedia propagate threadRoot via m.thread alongside m.in_reply_to.
  • CI workflow now runs Build before Test; cold-jiti plugin-load tests bumped to a 60 s timeout.
  • Latest commit 14f1468 (Release pipeline hardening): GitHub actions bumped from v4/v5 to v6; pack:packages cleans .packs first; scripts/audit-package-surface.mjs now also audits publish manifests (required fields, repository, license/README in files, prepublishOnly guard); packages/cloudflare marked private: true; packages/pi gains its LICENSE; packages/{pi,pickle,state-file,state-simple,state-sqlite} gain prepublishOnly/repository/license; smoke-cloudflare-worker.mjs discovers tarball filenames from pnpm pack --json instead of hardcoded 0.1.0; smoke-consumer.mjs collapsed to a re-export of package-consumer-smoke.mjs; CONTRIBUTING documents the initial-release flow.

Issues

16 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
  • 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
  • assistantMessageStart no longer resets lastVisibleText in the Beeper reply stream emitter (packages/openclaw/src/openclaw-runtime.ts:1828-1832). The emitter lives for one turn but in a tool-call loop the agent fires assistant.message.start multiple times per turn (one per assistant message between tool calls). With the reset removed, the second message's first text snapshot is diffed against the previous message's final text via visibleTextDelta(lastVisibleText, text); when the new text doesn't startsWith the previous, the accumulator advances to previous + delta rather than the actual new text. Subsequent snapshots that don't startsWith the corrupted accumulator are re-sent in full, producing duplicated content emission within the second message. → 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 for ghost echoes is uneven: commit 090c8ee added event.sender?.isMe || event.sender?.userId === this.#ownUserId to #dispatchMatrixRoomName/Topic/Avatar, but #dispatchMatrixMessage, #dispatchMatrixEdit, #dispatchMatrixReaction, #dispatchMatrixReactionRemove, #dispatchMatrixMembership, #dispatchMatrixMarkedUnread, and #dispatchMatrixDeleteChat still rely only on sender.isMe/#ownUserId. For events sent by the bridge as an appservice ghost, both checks are false (toGenericEvent hard-codes isMe: false; the ghost is neither the device user nor the appservice bot), so ghost-authored echoes arriving via appservice can still be re-dispatched to connector handlers and produce edit/reaction loops.</issue_text>
    <fix_agent_prompt>In packages/bridge/src/bridge.ts, introduce a helper like #isOwnUser(event) that returns true when event.sender?.isMe, event.sender?.userId === this.#ownUserId, OR event.sender?.userId matches the appservice user namespace (derived from this.#appserviceOptions?.registration.namespaces.user_ids). Apply it at the top of #dispatchMatrixMessage, #dispatchMatrixEdit, #dispatchMatrixReaction, #dispatchMatrixReactionRemove, #dispatchMatrixMembership, #dispatchMatrixMarkedUnread, #dispatchMatrixDeleteChat, and the existing RoomName/Topic/Avatar paths so the check is uniform. Add a unit test that submits a message authored by a user matching the appservice namespace 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
  • 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
  • createBeeperLogin now auto-registers new Beeper accounts when onlyExistingAccounts: false is set, and registerBeeperUser defaults acceptTerms to true whenever the caller omits it (packages/pickle/src/beeper/auth.ts sendBeeperLoginCode -> registerBeeperUser). Any caller that wires through onlyExistingAccounts: false without explicitly threading an acceptTerms boolean from the end user silently accepts Beeper's Terms of Service on their behalf. Defaulting to true for a legal-acceptance flag is risky; safer to default false and throw a clear "acceptTerms is required to register a new Beeper account" if not provided. → Autofix
  • resolveBeeperAgentAccountId returns the requested account name without verifying it exists in listBeeperAccountIds(cfg): when requested is provided it only checks allowed (the agent's accountIds list) and returns the value if allowed is empty or includes it. If a request asks for an unconfigured account, the function silently returns that name and the failure surfaces downstream as startBeeperGatewayAccount throwing Beeper account "<id>" is not fully configured, hiding the real misconfiguration (typo, deleted account, etc.). Dormant today because there's only one account ("default") and accounts is unset, but trivially reachable once the multi-account flow is exercised. → 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 (packages/bridge/src/bridge.ts:1815-1820) still has identical if (partId) branches — partId is read but never used to drive the lookup. The new #convertedEditPartTarget typically avoids this by preferring part.part?.mxid, but it falls back to #matchingRemoteTarget whenever the connector returns a ConvertedEditPart with id/part.partId set but no mxid. In that fallback, multi-part remote edits whose modified-parts order differs from the stored part order will target the wrong Matrix event and write messagePartKey(messageId, part.id) to that wrong mxid.</issue_text>
    <fix_agent_prompt>Edit packages/bridge/src/bridge.ts. #matchingRemoteTarget (~line 1815-1820) should map partId to the matching SentEvent rather than ignoring it. The caller #convertedEditPartTarget (~line 1822-1832) already passes existing.db indirectly — extend #matchingRemoteTarget to also accept db: Message[] and do const i = db.findIndex((m) => m.partId === partId); return existing[i] ?? existing[index] ?? existing[0]. Add a unit test in bridge.test.ts where converted.modifiedParts are returned in reverse order from the stored parts and assert the correct stored mxid is edited.</fix_agent_prompt>
    <inline_comment>{"file_path": "packages/bridge/src/bridge.ts", "line_number": 1819, "text": "Both branches of if (partId) still return the same expression — the parameter is read but never used to find a stored target. The new #convertedEditPartTarget (just below) preferentially uses part.part?.mxid, so the dead branch is only reachable when a ConvertedEditPart has id/part.partId set but no mxid. In that fallback, multi-part edits whose modified-parts order differs from the stored part order will edit the wrong Matrix event and overwrite the storage key with the wrong mxid.\n\nThe parallel existing.db: Message[] carries partId on each entry — pass it in and use db.findIndex((m) => m.partId === partId) to drive the lookup before falling back to positional index."}</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
3 issues already resolved
  • 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>
    (fixed by commit 97207e4)
  • Beeper stream advertises the wrong marker type when no subscribers are passed: both handleStartBeeperStreamMessage (messages.go:151-157) and the new handleStartBeeperAIRunStream (beeper_ai_run.go non-subscriber branch) force the carrier com.beeper.stream marker to {"type": aistream.BeeperAIStreamDeltas} (i.e. com.beeper.ai) regardless of the requested stream type, but publishBeeperStreamCarrierContents 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 — and the new commit replicates the same defect in the AI-run stream path.</issue_text>
    <fix_agent_prompt>Edit packages/pickle/native/internal/core/messages.go:151-157 and packages/pickle/native/internal/core/beeper_ai_run.go (handleStartBeeperAIRunStream non-subscriber branch). 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 tests covering both handleStartBeeperStreamMessage and handleStartBeeperAIRunStream with streamType: "com.beeper.llm" asserting the marker matches.</fix_agent_prompt>
summarize_pr (fixed by commit 0349ca5) - CLI `--help` no longer advertises the `--server-env` flag: the previous `--env ` line was removed in this commit but no replacement was added, while `login`/`whoami` now only accept `--server-env `. Users have no documented way to discover the new flag (or its renamed values). (fixed by commit 2dd203d)

CI Checks

All CI checks pass on 14f1468. Typecheck, Build, Test, Go test, Pack, and both smoke suites are green; the new publish-manifest audit (scripts/audit-package-surface.mjs) accepts the workspace.


⚡ 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 Outdated
@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.

@indent
Copy link
Copy Markdown

indent Bot commented Jun 2, 2026

Re-checked after 7bee27a + 0349ca5. The big architectural changes are: BeeperTurnStream/media helpers/createRemoteChatInfoChange moved up into @beeper/pickle-bridge, ChatInfoChange/Portal/ConvertedEdit reshaped to bridgev2 form (chatInfo/memberChanges, addedParts/deletedParts/part.part.mxid), and the native AI-run-stream surface gained append_beeper_ai_run_stream_part plus a 30s per-op timeout.

Resolved

  • Stream marker mismatch: both handleStartBeeperStreamMessage and handleStartBeeperAIRunStream now set the no-subscriber marker to {"type": req.StreamType}, matching the deltas key. Issue closed.

Updated

  • #matchingRemoteTarget dead branch — still present (bridge.ts:1815-1820), but the new #convertedEditPartTarget preferentially uses part.part?.mxid, so the dead-branch fallback only fires when a ConvertedEditPart has id/part.partId set but no mxid. Updated the issue accordingly.

Still valid (unchanged in these commits)

  • whoami ignores default config; messageFromSentEvent timestamp; revoke_invite undetectable (unsigned still not propagated); crypto-store SenderKey collapse; shared-runtime disconnect; leaked stream coordinator on createStreamPublisher; appservice state-store not updated for non-message events; classifyAppserviceEventClass whitelist gaps; ghost-echo own-user filter; tool-part schema drift in streams/beeper-message.ts; hard-wired approvalReactionsEnabled; dead toolDynamicByCallId.

No new issues introduced by the refactor that I can confirm.

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

Caution

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

⚠️ Outside diff range comments (2)
packages/bridge/src/types.ts (1)

511-559: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Don't require roomState until RuntimeBridge implements it.

This new required field is the direct cause of the current typecheck failure: RuntimeBridge no longer satisfies PickleBridge. Add the roomState implementation in the same PR or keep this property optional until the runtime is wired up.

🤖 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/types.ts` around lines 511 - 559, The new required
readonly roomState on the PickleBridge interface is breaking RuntimeBridge; make
roomState optional until the runtime implements it by changing the declaration
to readonly roomState?: BridgeRoomStateAPI so existing RuntimeBridge types pass,
or alternatively implement BridgeRoomStateAPI (get and set) on the RuntimeBridge
class (methods matching BridgeRoomStateAPI) in this PR and remove the
optionality; update any consumers that assume roomState is non-null accordingly.
packages/pickle/native/internal/core/messages.go (1)

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

Persist the thread root with the stream state.

A stream started in a thread forgets its ThreadRootEventID after registration. Later non-direct carrier events are sent with only an m.reference, so live updates escape the thread and appear as top-level room messages. Store the thread root here and reapply it when publishing carrier events.

Also applies to: 166-172

♻️ Duplicate comments (2)
packages/bridge/src/bridge.ts (1)

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

Match edit targets by partId, not index fallback.

This still routes multipart edits through #matchingRemoteTarget(...), which ignores partId and picks by index. If part ordering changes, the wrong Matrix event gets edited/redacted.

🛠️ Minimal fix
-  `#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];
   }
@@
-    return this.#matchingRemoteTarget(existing.sent, partId, index);
+    return this.#matchingRemoteTarget(existing, partId, 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/bridge/src/bridge.ts` around lines 1822 - 1832, The converted edit
routing falls back to `#matchingRemoteTarget` which ignores partId and may pick by
index; update the `#convertedEditPartTarget` method to prefer finding a SentEvent
in existing.sent whose raw/metadata contains the same partId (e.g.,
sent.raw?.partId or sent.raw?.metadata?.partId) or whose eventId equals partId,
and return that SentEvent; only if no match is found, fall back to using
existing.sent[index] or existing.sent[0] as the last resort. Keep references to
the method name `#convertedEditPartTarget` and the helper `#matchingRemoteTarget` so
reviewers can locate the change.
packages/openclaw/src/registry.test.ts (1)

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

Clean up the temporary registry directory.

This test still leaves the mkdtemp(...) directory behind on every run. Please wrap the body in try/finally and remove dir so repeated local/CI runs do not accumulate temp files.

🤖 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 - 39, Wrap the test
body for the "persists agent contacts, session bindings, and dedupe keys" case
in a try/finally so the temporary directory created by mkdtemp (variable dir) is
always removed; after the existing await registry.save() and assertions (or in
finally) remove the dir (e.g., fs.rmSync(dir, { recursive: true, force: true })
or await fs.promises.rm(dir, { recursive: true, force: true })) to ensure no
temp files remain, keeping the rest of the test (path, registry, loaded,
assertions) unchanged.
🧹 Nitpick comments (2)
packages/openclaw/src/beeper-channel-config.schema.json (1)

3-14: ⚡ Quick win

Reject unknown channel config keys.

With "additionalProperties": true, typos like "beeprEnv" will still validate here and then be silently ignored by createDefaultConfig, which falls back to defaults instead of surfacing the mistake. Tightening this schema makes config errors fail fast.

Suggested change
-  "additionalProperties": true,
+  "additionalProperties": false,
🤖 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 3 - 14,
The schema currently allows unknown keys because "additionalProperties" is true;
change it to "additionalProperties": false so unknown channel config keys (e.g.,
typos like "beeprEnv") fail validation; keep the existing "properties" block for
"enabled" and "beeperEnv" intact and optionally add a "required" array if you
want to enforce presence of specific keys, but the essential fix is flipping
additionalProperties to false to reject unrecognized keys.
packages/openclaw/src/connector.ts (1)

718-724: 💤 Low value

outboundActivityPatch appears unused.

The function is defined but never called in this file. inboundActivityPatch() is used at line 331, but there's no corresponding call to outboundActivityPatch(). If outbound activity tracking is intended, the function should be wired in; otherwise, consider removing dead code.

🤖 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/connector.ts` around lines 718 - 724,
outboundActivityPatch is defined but never used; either wire it into the
outbound-event code path (mirror where inboundActivityPatch() is invoked — e.g.,
call outboundActivityPatch() from the same caller that updates outbound events
such as the send/dispatch/transport emit handler that updates activity
timestamps) so the OpenClawBridgeActivityPatch fields
(lastOutboundAt/lastTransportActivityAt) get updated, or remove the unused
outboundActivityPatch function and its OpenClawBridgeActivityPatch-only helper
if outbound tracking is not required; reference the outboundActivityPatch and
inboundActivityPatch functions and the code locations that update activity
timestamps to guide the change.
🤖 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/beeper-stream.ts`:
- Around line 217-235: The catch currently wraps both event-processing and the
finalization call in runBeeperTurnStream so a failure inside
options.stream.finalize() triggers a second terminal write with EVENT_RUN_ERROR;
restructure by separating concerns: wrap the for-await loop and the map/publish
logic (toAsyncIterable, options.mapEvent, options.stream.publishMany) in a
try/catch that on error writes a terminalPart RUN_ERROR via
options.stream.finalize(...) and rethrows, then perform the normal finalize call
(options.stream.finalize(options.finishReason ? { finishReason:
options.finishReason } : {})) in its own try/without the RUN_ERROR fallback so
any finalize failures are propagated without issuing a second terminal write;
ensure you do not call finalize twice for the same runId/turnId.

---

Outside diff comments:
In `@packages/bridge/src/types.ts`:
- Around line 511-559: The new required readonly roomState on the PickleBridge
interface is breaking RuntimeBridge; make roomState optional until the runtime
implements it by changing the declaration to readonly roomState?:
BridgeRoomStateAPI so existing RuntimeBridge types pass, or alternatively
implement BridgeRoomStateAPI (get and set) on the RuntimeBridge class (methods
matching BridgeRoomStateAPI) in this PR and remove the optionality; update any
consumers that assume roomState is non-null accordingly.

---

Duplicate comments:
In `@packages/bridge/src/bridge.ts`:
- Around line 1822-1832: The converted edit routing falls back to
`#matchingRemoteTarget` which ignores partId and may pick by index; update the
`#convertedEditPartTarget` method to prefer finding a SentEvent in existing.sent
whose raw/metadata contains the same partId (e.g., sent.raw?.partId or
sent.raw?.metadata?.partId) or whose eventId equals partId, and return that
SentEvent; only if no match is found, fall back to using existing.sent[index] or
existing.sent[0] as the last resort. Keep references to the method name
`#convertedEditPartTarget` and the helper `#matchingRemoteTarget` so reviewers can
locate the change.

In `@packages/openclaw/src/registry.test.ts`:
- Around line 8-39: Wrap the test body for the "persists agent contacts, session
bindings, and dedupe keys" case in a try/finally so the temporary directory
created by mkdtemp (variable dir) is always removed; after the existing await
registry.save() and assertions (or in finally) remove the dir (e.g.,
fs.rmSync(dir, { recursive: true, force: true }) or await fs.promises.rm(dir, {
recursive: true, force: true })) to ensure no temp files remain, keeping the
rest of the test (path, registry, loaded, assertions) unchanged.

---

Nitpick comments:
In `@packages/openclaw/src/beeper-channel-config.schema.json`:
- Around line 3-14: The schema currently allows unknown keys because
"additionalProperties" is true; change it to "additionalProperties": false so
unknown channel config keys (e.g., typos like "beeprEnv") fail validation; keep
the existing "properties" block for "enabled" and "beeperEnv" intact and
optionally add a "required" array if you want to enforce presence of specific
keys, but the essential fix is flipping additionalProperties to false to reject
unrecognized keys.

In `@packages/openclaw/src/connector.ts`:
- Around line 718-724: outboundActivityPatch is defined but never used; either
wire it into the outbound-event code path (mirror where inboundActivityPatch()
is invoked — e.g., call outboundActivityPatch() from the same caller that
updates outbound events such as the send/dispatch/transport emit handler that
updates activity timestamps) so the OpenClawBridgeActivityPatch fields
(lastOutboundAt/lastTransportActivityAt) get updated, or remove the unused
outboundActivityPatch function and its OpenClawBridgeActivityPatch-only helper
if outbound tracking is not required; reference the outboundActivityPatch and
inboundActivityPatch functions and the code locations that update activity
timestamps to guide the change.
🪄 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: 041c42a6-fdda-418a-961b-13759fddf15c

📥 Commits

Reviewing files that changed from the base of the PR and between 97207e4 and 0349ca5.

⛔ Files ignored due to path filters (1)
  • packages/pickle/native/go.sum is excluded by !**/*.sum
📒 Files selected for processing (64)
  • AGENTS.md
  • package.json
  • packages/bridge/package.json
  • packages/bridge/src/beeper-stream.test.ts
  • packages/bridge/src/beeper-stream.ts
  • packages/bridge/src/bridge.test.ts
  • packages/bridge/src/bridge.ts
  • packages/bridge/src/events.ts
  • packages/bridge/src/index.ts
  • packages/bridge/src/media-message.test.ts
  • packages/bridge/src/media-message.ts
  • packages/bridge/src/types.ts
  • packages/bridge/tsdown.config.ts
  • packages/openclaw/README.md
  • packages/openclaw/openclaw.plugin.json
  • packages/openclaw/package.json
  • packages/openclaw/src/appservice.test.ts
  • packages/openclaw/src/appservice.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-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/integration.test.ts
  • packages/openclaw/src/openclaw-extension.test.ts
  • packages/openclaw/src/openclaw-runtime.test.ts
  • packages/openclaw/src/openclaw-runtime.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.test.ts
  • packages/openclaw/src/setup.ts
  • packages/openclaw/src/types.ts
  • packages/openclaw/tsdown.config.ts
  • packages/openclaw/vitest.config.ts
  • packages/pickle/native/go.mod
  • 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/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
  • scripts/openclaw-crabpot-full-test.mjs
  • tsconfig.base.json
💤 Files with no reviewable changes (1)
  • packages/openclaw/src/registration.ts
✅ Files skipped from review due to trivial changes (7)
  • packages/bridge/tsdown.config.ts
  • AGENTS.md
  • packages/bridge/package.json
  • package.json
  • packages/pickle/native/go.mod
  • packages/pickle/src/generated-runtime-types.ts
  • packages/pickle/src/generated-runtime-operations.ts
🚧 Files skipped from review as they are similar to previous changes (20)
  • packages/pickle/src/client.ts
  • packages/pickle/native/internal/core/operations.go
  • packages/openclaw/tsdown.config.ts
  • packages/openclaw/src/rooms.test.ts
  • packages/openclaw/vitest.config.ts
  • packages/pickle/src/index.ts
  • packages/pickle/src/runtime-types.ts
  • packages/openclaw/src/protocol-coverage.ts
  • packages/pickle/src/client-types.ts
  • packages/pickle/native/internal/core/core.go
  • packages/openclaw/src/openclaw-extension.test.ts
  • packages/openclaw/src/bridge-agent.test.ts
  • packages/openclaw/src/registration.test.ts
  • packages/openclaw/package.json
  • packages/bridge/src/bridge.test.ts
  • packages/pickle/src/client.test.ts
  • packages/openclaw/src/registry.ts
  • packages/openclaw/src/integration.test.ts
  • packages/openclaw/src/cli.ts
  • packages/openclaw/src/openclaw-runtime.test.ts
📜 Review details
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: beeper/pickle

Timestamp: 2026-06-02T03:11:10.919Z
Learning: Use Crabpot for OpenClaw plugin and integration compatibility testing, including plugin API seams, channel registration, lifecycle hooks, provider capabilities, cold imports, workspace planning, and static execution policy
Learnt from: CR
Repo: beeper/pickle

Timestamp: 2026-06-02T03:11:10.919Z
Learning: Maintain expected sibling directory structure with checkouts at /Projects/pickle, /Projects/crabpot, and /Projects/openclaw
Learnt from: CR
Repo: beeper/pickle

Timestamp: 2026-06-02T03:11:10.919Z
Learning: Run full Pickle and OpenClaw plugin compatibility suite using `npm run full-test` before deployment
Learnt from: CR
Repo: beeper/pickle

Timestamp: 2026-06-02T03:11:10.919Z
Learning: Do not run opt-in isolated execution commands (with CRABPOT_EXECUTE_ISOLATED=1) unless the task explicitly requires side effects
🪛 GitHub Actions: CI / 0_Typecheck, test, build, and package.txt
packages/bridge/src/index.ts

[error] 1-1: pnpm -r typecheck failed at packages/bridge with command 'tsc --noEmit'.

packages/bridge/src/media-message.ts

[error] 1-1: pnpm -r typecheck failed at packages/bridge with command 'tsc --noEmit'.

packages/bridge/src/events.ts

[error] 1-1: pnpm -r typecheck failed at packages/bridge with command 'tsc --noEmit'.

packages/bridge/src/beeper-stream.ts

[error] 1-1: pnpm -r typecheck failed at packages/bridge with command 'tsc --noEmit'.

packages/bridge/src/media-message.test.ts

[error] 1-1: pnpm -r typecheck failed at packages/bridge with command 'tsc --noEmit'.

packages/bridge/src/beeper-stream.test.ts

[error] 1-1: pnpm -r typecheck failed at packages/bridge with command 'tsc --noEmit'.

packages/bridge/src/bridge.ts

[error] 113-113: TypeScript (tsc) error TS2741: Property 'roomState' is missing in type 'RuntimeBridge' but required in type 'PickleBridge'.


[error] 1-1: pnpm -r typecheck failed at packages/bridge with command 'tsc --noEmit'.

packages/bridge/src/types.ts

[error] 1-1: pnpm -r typecheck failed at packages/bridge with command 'tsc --noEmit'.

🪛 GitHub Actions: CI / Typecheck, test, build, and package
packages/bridge/src/bridge.ts

[error] 113-113: TypeScript (tsc --noEmit) failed: TS2741: Property 'roomState' is missing in type 'RuntimeBridge' but required in type 'PickleBridge'.

🪛 LanguageTool
packages/openclaw/README.md

[grammar] ~18-~18: Ensure spelling is correct
Context: ...d login available when needed. - Beeper appservice registration for the OpenClaw bridge. -...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~20-~20: Ensure spelling is correct
Context: ... and ClawHub install metadata. - Pickle bridgev2-style transport for Matrix portals, med...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🔇 Additional comments (22)
packages/bridge/src/index.ts (1)

25-25: LGTM!

Also applies to: 47-47, 59-61

packages/bridge/src/media-message.ts (1)

23-66: LGTM!

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

56-79: LGTM!

packages/bridge/src/media-message.test.ts (1)

4-44: LGTM!

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

76-89: threadRoot is still ignored for outbound sends.

sendText() and sendMedia() both accept threadRoot, but neither serializes an m.thread relation, so threaded sends still land as top-level messages.

Also applies to: 91-101

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

211-215: Use a unique temp directory in the empty-state whoami test.

The hardcoded /tmp/pickle-openclaw-empty path can still pick up residue from another run and make this case flaky.

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

212-232: ⚡ Quick win

Reuse the loaded runtime instance for sendTurn.

loadUserLogin() creates a runtime via #runtimeFactory(this.config), but #sendTurn calls the factory again on every turn. If a custom runtimeFactory returns a fresh adapter per call, dispatch gets split away from the runtime the agent is tracking, which can desynchronize event streams and lifecycle handling.

The default factory at lines 125-128 returns the same cached runtime instance, but a custom factory wouldn't have this guarantee.


264-273: LGTM!

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

184-211: ⚡ Quick win

Media sends still drop replyToId.

sendMedia doesn't accept or forward replyToId from the context. The send.media wrapper at lines 145-153 includes replyToId in its signature, but it's lost when calling sendMedia. Replied media messages will be sent as top-level posts instead of replies.


446-459: ⚡ Quick win

mark_unread not exposed in advertised action set.

handleAction() at lines 489-494 implements mark_unread, but beeperMessageToolActions (line 446) and supportsAction() (lines 458-459) omit it. Callers checking capabilities first will reject the action before it reaches the handler.


1163-1172: ⚖️ Poor tradeoff

stopBeeperGatewayAccount cannot signal a running bridge to stop.

When a bridge is running and waiting at waitForAbort(ctx.abortSignal) (line 1072), stopBeeperGatewayAccount stops the bridge object but doesn't abort the signal. The finally block in startBeeperGatewayAccountOnce won't run until the signal is aborted by the lifecycle manager. If the caller expects stopAccount() to fully shut down the bridge synchronously, this may leave the startup task hanging.


1004-1026: LGTM!


939-998: LGTM!

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

1-174: LGTM!

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

1-67: LGTM!

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

1-187: LGTM!

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

1-100: LGTM!

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

1-141: LGTM!

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

64-78: Use matrixDomainFromConfig() in createSessionRoom() too.

This path still derives the Matrix domain from config.homeserver, so the service-bot MXID is wrong whenever homeserverDomain intentionally differs from the URL hostname.

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

367-382: Still only one event source is wired here.

The early returns still drop other host event streams, and #localEvents is still skipped entirely when runtime.events is a function or subscribe() path is used.


2252-2255: Reset the visible-text buffer on replacements.

This still appends replacement text onto the previous buffer, so a rewrite like "hello" -> "hi" leaves corrupted state for later deltas and finalization.

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

398-417: Reject duplicate or empty run IDs before replacing run state.

beginBeeperAIRun() still unconditionally overwrites c.beeperAIRuns[run.RunID], so a retry or empty runId can clobber an in-flight run and drop its accumulated stream state.

Comment thread packages/bridge/src/beeper-stream.ts
@indent
Copy link
Copy Markdown

indent Bot commented Jun 2, 2026

Re-checked after 28cac3c + 090c8ee. Highlights:

Resolved

  • PickleBridge.roomState typecheck regression — RuntimeBridge now implements both roomState.{get,set} (via MatrixClient.rooms.getStateEvent/sendStateEvent) and createBeeperTurnStream.

Updated

  • Ghost-echo own-user filter — 090c8ee added a sender?.isMe || sender?.userId === #ownUserId check to the new #dispatchMatrixRoomName/Topic/Avatar dispatchers, but #dispatchMatrixMessage/Edit/Reaction/ReactionRemove/Membership/MarkedUnread/DeleteChat still rely only on sender.isMe/#ownUserId and won't catch ghost echoes from the appservice namespace. Updated the issue + fix prompt accordingly.

Still valid (unchanged in these commits)

  • #matchingRemoteTarget dead branch (still in fallback path), whoami ignores default config, messageFromSentEvent synthetic timestamp, revoke_invite undetectable (unsigned still not propagated by toGenericEvent), crypto-store SenderKey collapse, shared-runtime disconnect, leaked stream coordinator on createStreamPublisher, appservice state-store not updated for non-message events, incomplete classifyAppserviceEventClass whitelist, tool-part schema drift, hard-wired approvalReactionsEnabled, dead toolDynamicByCallId.

CodeRabbit's two outstanding finds (runBeeperTurnStream finalize-double-call in bridge/src/beeper-stream.ts:217-236 and stream-state losing ThreadRootEventID so non-direct carrier sends escape the thread) are also still present.

Comment thread packages/openclaw/src/cli.ts
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