Skip to content

fix: reconcile session compaction count after late compaction success#45493

Merged
jalehman merged 3 commits into
openclaw:mainfrom
jackal092927:fix/compaction-late-success-counter-reconcile
Mar 25, 2026
Merged

fix: reconcile session compaction count after late compaction success#45493
jalehman merged 3 commits into
openclaw:mainfrom
jackal092927:fix/compaction-late-success-counter-reconcile

Conversation

@jackal092927

Copy link
Copy Markdown
Contributor

Fix Late Compaction Success Leaving Session Counter Stale

Summary

This PR fixes a session-state mismatch where compaction can complete successfully after the runner's 60s aggregate wait timeout, but sessions.json.compactionCount remains stale.

The timeout behavior is unchanged. The fix adds a lightweight reconciliation write on successful compaction end so the session store converges even when the enclosing run has already moved on.

Problem

Observed behavior:

  • transcript persists a new compaction entry
  • context usage drops accordingly
  • /status still shows the old Compactions count

Root cause:

  • run-finalization only increments the session-store counter from run-local metadata
  • in timeout-late-success cases, compaction completion can be observed too late for that metadata path
  • there was no follow-up reconciliation on actual compaction success

Fix

On successful auto_compaction_end:

  • increment the attempt-local counter as before
  • reconcile the persisted session-store entry immediately
  • update compactionCount using max(existing, observed) to keep the write monotonic

This preserves:

  • the existing 60s timeout
  • non-blocking conversation behavior
  • low resource usage

Tests

Added:

  • src/agents/pi-embedded-subscribe.handlers.compaction.test.ts

    • helper-level reconciliation behavior
    • no over-count when store is already ahead
    • successful compaction end updates the session store
  • src/agents/pi-embedded-subscribe.handlers.lifecycle.compaction-reconcile.test.ts

    • routed event-path coverage through createEmbeddedPiSessionEventHandler(...)
    • verifies sessions.json is updated after a successful auto_compaction_end

Verification

Ran:

  • ./node_modules/.bin/vitest run src/agents/pi-embedded-subscribe.handlers.compaction.test.ts
  • ./node_modules/.bin/vitest run src/agents/pi-embedded-subscribe.handlers.lifecycle.compaction-reconcile.test.ts

Both passed in the investigation worktree.

Issue

Closes: compaction timeout late success counter mismatch

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: M labels Mar 13, 2026
@greptile-apps

greptile-apps Bot commented Mar 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a session-state mismatch where a compactionCount in sessions.json could remain stale when an auto_compaction_end event arrives after the 60s aggregate wait timeout. It adds a lightweight, fire-and-forget reconciliation write (reconcileSessionStoreCompactionCountAfterSuccess) called on every successful compaction end, using max(existing, observed) semantics to keep the write monotonic and idempotent.

Key points:

  • The new helper correctly acquires the session-store write lock, re-reads the store inside it, and only writes when the observed count strictly exceeds the stored count — avoiding double-counting if the run-finalization path already updated the store.
  • The reconciliation is best-effort: failures are caught and logged as warnings, preserving the existing non-blocking behavior.
  • One inconsistency worth addressing: ctx.incrementCompactionCount?.() uses optional chaining, but the immediately-following ctx.getCompactionCount() does not. If the increment were ever skipped, the reconciliation would be called with the stale pre-increment count and silently become a no-op. Both methods are required on the type, so this is low-risk, but removing the ?. from incrementCompactionCount would make intent and behavior consistent.
  • Two new test files cover the helper in isolation and through the full event routing path.

Confidence Score: 4/5

  • This PR is safe to merge; the fix is narrowly scoped, idempotent, and adds meaningful test coverage.
  • The core logic — using max(existing, observed) inside a write-locked updateSessionStoreEntry — is correct and cannot over-count. The only notable issue is a pre-existing inconsistency between ?. on incrementCompactionCount and a direct call to getCompactionCount, which in the worst case results in a reconciliation no-op rather than any data corruption. Tests exercise the happy path, the idempotency case, and the routed event path. No regressions are expected.
  • Pay close attention to src/agents/pi-embedded-subscribe.handlers.compaction.ts lines 55–64 for the optional-chaining inconsistency.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/agents/pi-embedded-subscribe.handlers.compaction.ts
Line: 55-64

Comment:
**Inconsistent optional-chaining guard before `getCompactionCount()`**

`ctx.incrementCompactionCount?.()` uses optional chaining, but the immediately-following `ctx.getCompactionCount()` does not. If `incrementCompactionCount` is absent (the `?.` short-circuits), `getCompactionCount()` still executes and returns the **pre-increment** count. The reconciliation would then be called with a stale `observedCompactionCount`, making it a no-op rather than writing the new count to the store.

Both methods are declared as required on `EmbeddedPiSubscribeContext`, so in production this cannot occur. However, since the defensive `?.` on `incrementCompactionCount` implies the intent to handle absent implementations, the safest fix is to remove the `?.` from `incrementCompactionCount` (aligning it with the type) so both calls are consistent and the observed count always reflects the post-increment value.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: 62528d4

Comment on lines +55 to +64
ctx.incrementCompactionCount?.();
const observedCompactionCount = ctx.getCompactionCount();
void reconcileSessionStoreCompactionCountAfterSuccess({
sessionKey: ctx.params.sessionKey,
agentId: ctx.params.agentId,
configStore: ctx.params.config?.session?.store,
observedCompactionCount,
}).catch((err) => {
ctx.log.warn(`late compaction count reconcile failed: ${String(err)}`);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Inconsistent optional-chaining guard before getCompactionCount()

ctx.incrementCompactionCount?.() uses optional chaining, but the immediately-following ctx.getCompactionCount() does not. If incrementCompactionCount is absent (the ?. short-circuits), getCompactionCount() still executes and returns the pre-increment count. The reconciliation would then be called with a stale observedCompactionCount, making it a no-op rather than writing the new count to the store.

Both methods are declared as required on EmbeddedPiSubscribeContext, so in production this cannot occur. However, since the defensive ?. on incrementCompactionCount implies the intent to handle absent implementations, the safest fix is to remove the ?. from incrementCompactionCount (aligning it with the type) so both calls are consistent and the observed count always reflects the post-increment value.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/pi-embedded-subscribe.handlers.compaction.ts
Line: 55-64

Comment:
**Inconsistent optional-chaining guard before `getCompactionCount()`**

`ctx.incrementCompactionCount?.()` uses optional chaining, but the immediately-following `ctx.getCompactionCount()` does not. If `incrementCompactionCount` is absent (the `?.` short-circuits), `getCompactionCount()` still executes and returns the **pre-increment** count. The reconciliation would then be called with a stale `observedCompactionCount`, making it a no-op rather than writing the new count to the store.

Both methods are declared as required on `EmbeddedPiSubscribeContext`, so in production this cannot occur. However, since the defensive `?.` on `incrementCompactionCount` implies the intent to handle absent implementations, the safest fix is to remove the `?.` from `incrementCompactionCount` (aligning it with the type) so both calls are consistent and the observed count always reflects the post-increment value.

How can I resolve this? If you propose a fix, please make it concise.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 62528d476f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

sessionKey,
update: async (entry) => {
const currentCount = Math.max(0, entry.compactionCount ?? 0);
const nextCount = Math.max(currentCount, observedCompactionCount);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reconcile compaction count using per-run delta

observedCompactionCount here is the current run's local counter (it starts at 0 in subscribeEmbeddedPiSession), but this update treats it like an absolute session total by doing max(currentCount, observedCompactionCount). That means late compaction success is still dropped for sessions with existing history: if sessions.json already has compactionCount=7 and one late compaction finishes (observedCompactionCount=1), this write becomes a no-op and the count stays stale at 7 instead of 8. The reconcile step needs to apply the newly observed increment (or use a stored run baseline), not compare the local count directly against the persisted lifetime count.

Useful? React with 👍 / 👎.

@jackal092927

Copy link
Copy Markdown
Contributor Author

Related issues for context, but this PR is not intended as a fix for those broader timeout/abort/session-freeze bugs:

So the intended scope here is only:

  • compaction eventually succeeded
  • transcript/session state already reflects that success
  • sessions.json.compactionCount stayed stale

This PR makes that state converge by reconciling the session-store counter on successful compaction end, without increasing the timeout or blocking the active conversation.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b5f7b1b844

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +119 to +120
const currentCount = Math.max(0, entry.compactionCount ?? 0);
const nextCount = Math.max(currentCount, observedCompactionCount);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reconcile using per-run delta instead of absolute count

observedCompactionCount is the current subscription’s in-memory counter, not the lifetime session total (subscribeEmbeddedPiSession resets it to 0 each run in src/agents/pi-embedded-subscribe.ts:90), so Math.max(currentCount, observedCompactionCount) drops legitimate late-success increments whenever the store already has prior history. For example, if sessions.json already has compactionCount=7 and this run records one late successful compaction (observedCompactionCount=1), this code keeps 7 and the intended fix does not converge the persisted count; the reconcile write needs to apply the new run’s increment relative to the persisted baseline.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 86a07f13a3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

ctx.incrementCompactionCount?.();
ctx.incrementCompactionCount();
const observedCompactionCount = ctx.getCompactionCount();
void reconcileSessionStoreCompactionCountAfterSuccess({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid double-counting compactions during normal runs

This unconditional reconcile call runs on every successful auto_compaction_end, but normal run finalization still adds the run delta to the store (src/commands/agent/session-store.ts:103 and src/auto-reply/reply/session-updates.ts:279). In non-timeout flows, that means the same compaction can be counted twice: for example, a session at compactionCount=0 with one successful compaction is first written to 1 here and then incremented again to 2 at finalization. The reconcile write should be limited to the late-success gap path or computed as a true delta against a per-run baseline.

Useful? React with 👍 / 👎.

@jalehman jalehman self-assigned this Mar 21, 2026
@jalehman jalehman force-pushed the fix/compaction-late-success-counter-reconcile branch from 86a07f1 to 1a67456 Compare March 21, 2026 19:50

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1a674568a5

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +116 to +119
const nextEntry = await updateSessionStoreEntry({
storePath,
sessionKey,
update: async (entry) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Sync in-memory session state after late compaction reconcile

This helper only patches sessions.json, but the live sessionEntry/sessionStore objects captured by createFollowupRunner remain stale (src/auto-reply/reply/followup-runner.ts:37-55 and src/auto-reply/reply/followup-runner.ts:365-374). In the common sequence “follow-up A times out during compaction, late success reconciles to compactionCount=1, then follow-up B on the same queue compacts once more”, B still computes from the old in-memory 0 and writes 1 back, so the second compaction is dropped. The late-success path needs to update the live session state as well, not only the persisted file.

Useful? React with 👍 / 👎.

@jalehman jalehman force-pushed the fix/compaction-late-success-counter-reconcile branch from 1a67456 to a91fe15 Compare March 21, 2026 21:24

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a91fe15b1f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +57 to +61
void reconcileSessionStoreCompactionCountAfterSuccess({
sessionKey: ctx.params.sessionKey,
agentId: ctx.params.agentId,
configStore: ctx.params.config?.session?.store,
observedCompactionCount,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Guard late compaction reconcile by sessionId

This reconcile is keyed only by sessionKey, even though ctx.params.sessionId is available and OpenClaw regenerates the session ID on /new and /reset (src/auto-reply/reply/session.ts:364). In the timeout-late-success case, an old run can finish compaction after the user has already reset the conversation, and this code will then increment compactionCount on the new session that now occupies the same key. Please pass/check the expected sessionId before patching sessions.json so late events from a previous session cannot corrupt the replacement session's state.

Useful? React with 👍 / 👎.

Comment on lines +125 to +127
return {
compactionCount: nextCount,
updatedAt: Math.max(entry.updatedAt ?? 0, now),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid treating late compaction as new session activity

This background reconcile also advances updatedAt, but updatedAt is used as the session's last-activity timestamp in /status and in stale-session maintenance (src/auto-reply/status.ts:524-527, src/config/sessions/store.ts:860). When compaction completes after the run has already timed out, no new user turn has happened, so rewriting updatedAt here makes an idle session look recently active and can delay stale-reset/pruning decisions. The reconcile should fix the counter without moving the activity timestamp forward.

Useful? React with 👍 / 👎.

@jalehman jalehman force-pushed the fix/compaction-late-success-counter-reconcile branch 3 times, most recently from c539d18 to 94f200c Compare March 21, 2026 23:27
@AI-OWEN

AI-OWEN commented Mar 22, 2026

Copy link
Copy Markdown

Still reproducing on 2026.3.13 (stable channel)

Confirming this is still happening on 2026.3.13 (released 2026-03-14, includes PR #40324 from 2026-03-09).

Log sequence from 2026-03-22:

19:14:40 gateway drain started — draining 2 active embedded runs (hot reload via config.patch)
19:15:12 Telegram message delivered successfully
19:16:10 DRAIN TIMEOUT — activeRuns=1 timeoutMs=90000 (90s drain expired, run still stuck)
19:16:10 drain timeout reached; proceeding with restart
19:20:25 Lane wait exceeded: waitedMs=529340 (8.8 MINUTES), queueAhead=1
19:27:12 Manual compact cleared the stuck lane

Recovery: Manual compact was the only way to clear. Compaction deadlock preventing session recovery as described in #40295.

Confirmed:

What we observe: After drain timeout + forced restart, one embedded run remains orphaned. activeRuns counter never decrements. New messages queue behind stuck lane. Only manual compact recovers.

@jalehman jalehman force-pushed the fix/compaction-late-success-counter-reconcile branch from 94f200c to cec4630 Compare March 23, 2026 14:24

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cec463094b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +125 to +127
return {
compactionCount: nextCount,
updatedAt: Math.max(entry.updatedAt ?? 0, now),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Prevent compaction-timeout sessions from rotating auth profiles

When auto session auth-profile rotation is enabled, the next turn rotates whenever sessionEntry.compactionCount > authProfileOverrideCompactionCount (src/agents/auth-profiles/session-override.ts:113-149), and src/agents/pi-embedded-runner.run-embedded-pi-agent.auth-profile-rotation.e2e.test.ts:902 explicitly asserts that compaction timeouts must not rotate. This late-success reconcile only bumps compactionCount, so a compaction that finishes after the 60s timeout will be treated like a normal successful compaction on the next turn and unexpectedly switch away from the current auto-pinned profile.

Useful? React with 👍 / 👎.

@jalehman jalehman force-pushed the fix/compaction-late-success-counter-reconcile branch from cec4630 to 9c62625 Compare March 24, 2026 16:15

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9c62625721

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +120 to +121
const currentCount = Math.max(0, entry.compactionCount ?? 0);
const nextCount = Math.max(currentCount, observedCompactionCount);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reconcile compaction count using run delta, not local absolute

observedCompactionCount here is a per-subscription counter, not the lifetime session total (subscribeEmbeddedPiSession resets compactionCount to 0 each run in src/agents/pi-embedded-subscribe.ts:94), so comparing it directly with the persisted count drops legitimate late-success increments for sessions with prior history. In the timeout-late-success path this can still leave sessions.json stale (e.g., persisted 7, local observed 1 => stays 7), which means the new reconcile logic does not converge for existing sessions; this update needs to apply a per-run delta or track a baseline before using Math.max.

Useful? React with 👍 / 👎.

Comment on lines +126 to +127
compactionCount: nextCount,
updatedAt: Math.max(entry.updatedAt ?? 0, now),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep late compaction reconcile from advancing session activity

This reconciliation write updates updatedAt, but that field is used as last-activity time in status and stale-session maintenance (src/auto-reply/status.ts:639-643, src/config/sessions/store-maintenance.ts:151-168). When compaction finishes after the run has already timed out, no new user activity happened, so bumping updatedAt makes an idle session appear recently active and can delay reset/pruning behavior; the reconcile should correct compactionCount without moving activity time.

Useful? React with 👍 / 👎.

Comment on lines +116 to +119
const nextEntry = await updateSessionStoreEntry({
storePath,
sessionKey,
update: async (entry) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Sync in-memory session state when reconciling late compactions

This helper only updates the on-disk session store entry, so live in-memory session state can remain behind and later overwrite the reconciled value. createFollowupRunner holds a long-lived sessionStore reference (src/auto-reply/reply/followup-runner.ts:41-62), and later increments compute from that in-memory entry (src/auto-reply/reply/session-updates.ts:285-325); if late reconcile writes disk compactionCount=1 but memory is still 0, the next compaction increment can write 1 again instead of 2.

Useful? React with 👍 / 👎.

@jalehman jalehman force-pushed the fix/compaction-late-success-counter-reconcile branch from 9c62625 to 7e0aeee Compare March 24, 2026 16:57

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7e0aeee09b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

sessionKey,
update: async (entry) => {
const currentCount = Math.max(0, entry.compactionCount ?? 0);
const nextCount = Math.max(currentCount, observedCompactionCount);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reconcile compaction count using run delta

observedCompactionCount is run-local (it starts at 0 in subscribeEmbeddedPiSession at src/agents/pi-embedded-subscribe.ts:94), but this update treats it like a lifetime total by computing Math.max(currentCount, observedCompactionCount). That leaves late-success reconciles stale for sessions with prior history (for example stored 7 with one late success still stays 7), and when the store is behind it can pre-write values that are incremented again by normal run finalization (src/agents/command/session-store.ts:121 and src/auto-reply/reply/session-updates.ts:290). The reconcile logic needs to apply a per-run delta (or baseline-adjusted absolute), not compare a run-local absolute to persisted lifetime count.

Useful? React with 👍 / 👎.

Comment on lines +58 to +61
sessionKey: ctx.params.sessionKey,
agentId: ctx.params.agentId,
configStore: ctx.params.config?.session?.store,
observedCompactionCount,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Verify sessionId before patching compaction count

This reconcile call uses only sessionKey, even though ctx.params.sessionId is available. Since /new/reset flows rotate session IDs for the same key (src/auto-reply/reply/session.ts:372), a late auto_compaction_end from a timed-out prior run can update the replacement session’s compactionCount incorrectly. Pass the expected sessionId into reconciliation and skip the write when the stored entry no longer matches.

Useful? React with 👍 / 👎.

@jalehman jalehman force-pushed the fix/compaction-late-success-counter-reconcile branch 4 times, most recently from ee33ab6 to 953afb2 Compare March 25, 2026 05:49

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 953afb2825

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

sessionKey,
update: async (entry) => {
const currentCount = Math.max(0, entry.compactionCount ?? 0);
const nextCount = Math.max(currentCount, observedCompactionCount);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reconcile compaction count using per-run delta

observedCompactionCount is run-local (subscribeEmbeddedPiSession resets it to 0 at src/agents/pi-embedded-subscribe.ts:94), but this reconciliation treats it as a lifetime total with Math.max(currentCount, observedCompactionCount). For sessions that already have history (for example persisted compactionCount=7 and one late success in the current run), observedCompactionCount is 1, so this update becomes a no-op and the stored count stays stale. Fresh evidence in this revision is that the new tests initialize the in-memory count to match the seeded store (initialCount: 1), so they never exercise the real run-local baseline and therefore miss this undercount path.

Useful? React with 👍 / 👎.

@jalehman jalehman force-pushed the fix/compaction-late-success-counter-reconcile branch from 953afb2 to d0715a5 Compare March 25, 2026 16:43
@jalehman jalehman merged commit 2de32fb into openclaw:main Mar 25, 2026
40 checks passed
npmisantosh pushed a commit to npmisantosh/openclaw that referenced this pull request Mar 25, 2026
…openclaw#45493)

Merged via squash.

Prepared head SHA: d0715a5
Co-authored-by: jackal092927 <3854860+jackal092927@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
godlin-gh pushed a commit to YouMindInc/openclaw that referenced this pull request Mar 27, 2026
…openclaw#45493)

Merged via squash.

Prepared head SHA: d0715a5
Co-authored-by: jackal092927 <3854860+jackal092927@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
BotstersOrg pushed a commit to TheBotsters/botster-ego that referenced this pull request Mar 27, 2026
* test: collapse msteams helper suites

* test: collapse msteams graph suites

* test: collapse msteams state and monitor suites

* fix: preserve Telegram forum topic last-route delivery (#53052) (thanks @VACInc)

* fix(telegram): preserve forum topic thread in last-route delivery

* style(telegram): format last-route update

* test(telegram): cover General topic last-route thread

* test(telegram): align topic route helper

* fix(telegram): skip bound-topic last-route writes

---------

Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>

* fix(feishu): default requireMention to false for groupPolicy open

Groups configured with groupPolicy: open are expected to respond to all
messages. Previously, requireMention defaulted to true regardless of
groupPolicy, causing image (and other non-text) messages to be silently
dropped because they cannot carry @-mentions.

Fix: when groupPolicy is 'open' and requireMention is not explicitly
configured, resolve it to false instead of true. Users who want
mention-required behaviour in open groups can still set requireMention: true
explicitly.

Adds three regression tests covering the new default, explicit override, and
the unchanged allowlist-policy behaviour.

Closes #52553

* test(feishu): update config-schema test for removed requireMention default

* fix: finish feishu open-group docs and baselines (#54058) (thanks @byungsker)

* test: refresh pairing reply assertions for fenced codes (#54058) (thanks @byungsker)

* test: accept fenced discord pairing codes (#54058) (thanks @byungsker)

* test: allow daemon start hints to grow on linux (#54058) (thanks @byungsker)

* fix: preflight invalid telegram photos (#52545) (thanks @hnshah)

* fix(telegram): validate photo dimensions before sendPhoto

Prevents PHOTO_INVALID_DIMENSIONS errors by checking image dimensions
against Telegram Bot API requirements before calling sendPhoto.

If dimensions exceed limits (width + height > 10,000px), automatically
falls back to sending as document instead of crashing with 400 error.

Tested in production (openclaw 2026.3.13) where this error occurred:
  [telegram] tool reply failed: GrammyError: Call to 'sendPhoto' failed!
  (400: Bad Request: PHOTO_INVALID_DIMENSIONS)

Uses existing sharp dependency to read image metadata. Gracefully
degrades if sharp fails (lets Telegram handle validation, backward
compatible behavior).

Closes: #XXXXX (will reference OpenClaw issue if one exists)

* fix(telegram): validate photo aspect ratio

* refactor: use shared telegram image metadata

* fix: fail closed on telegram image metadata

* fix: preflight invalid telegram photos (#52545) (thanks @hnshah)

---------

Co-authored-by: Bob Shah <bobshah@Macs-Mac-Studio.local>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>

* fix: clarify cron best-effort partial delivery status (#42535) (thanks @MoerAI)

* fix(cron): track and log bestEffort delivery failures, mark not delivered on partial failure

* fix(cron): cache successful results on partial failure to preserve replay idempotency

When a best-effort send partially fails, we now still cache the successful delivery results via rememberCompletedDirectCronDelivery. This prevents duplicate sends on same-process replay while still correctly marking the job as not fully delivered.

* fix(cron): preserve partial-failure state on replay (#27069)

* fix(cron): restore test infrastructure and fix formatting

* fix: clarify cron best-effort partial delivery status (#42535) (thanks @MoerAI)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>

* fix(cli): route telegram thread create to topic-create

* fix: make telegram thread create use topic payload (#54336) (thanks @andyliu)

* cron: queue isolated delivery awareness

* fix(browser): add Edge LaunchServices bundle IDs for macOS default browser detection

macOS registers Edge as 'com.microsoft.edgemac' in LaunchServices, which
differs from the CFBundleIdentifier 'com.microsoft.Edge' in the app's own
Info.plist. Without recognising the LaunchServices IDs, Edge users who set
Edge as their default browser are not detected as having a Chromium browser.

Add the four com.microsoft.edgemac* variants to CHROMIUM_BUNDLE_IDS and a
corresponding test that mocks the LaunchServices → osascript resolution
path for Edge.

* fix: add changelog for macOS Edge default browser detection (#48561) (thanks @zoherghadyali)

* fix: cover macOS Edge osascript fallback path (#48561) (thanks @zoherghadyali)

* fix: fail loud when PTY cursor mode is unknown (#51490) (thanks @liuy)

* fix(process): auto-detect PTY cursor key mode for send-keys

When a PTY session sends smkx (\x1b[?1h) or rmkx (\x1b[?1l) to switch
cursor key mode, send-keys now detects this and encodes cursor keys
accordingly.

- smkx/rmkx detection in handleStdout before sanitizeBinaryOutput
- cursorKeyMode stored in ProcessSession
- encodeKeySequence accepts cursorKeyMode parameter
- DECCKM_SS3_KEYS for application mode (arrows + home/end)
- CSI sequences for normal mode
- Modified keys (including alt) always use xterm modifier scheme
- Extract detectCursorKeyMode for unit testing
- Use lastIndexOf to find last toggle in chunk (later one wins)

Fixes #51488

* fix: fail loud when PTY cursor mode is unknown (#51490) (thanks @liuy)

* style: format process send-keys guard (#51490) (thanks @liuy)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>

* fix: isolate session:patch hook payload (#53880) (thanks @graciegould)

* gateway: make session:patch hook typed and non-blocking

* gateway(test): add session:patch hook coverage

* docs(gateway): clarify session:patch security note

* fix: address review feedback on session:patch hook

Remove unused createInternalHookEvent import and fix doc example
to use inline event.type check matching existing hook examples.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: isolate hook payload to prevent mutation leaking into response

Shallow-copy sessionEntry and patch in the session:patch hook event
so fire-and-forget handlers cannot mutate objects used by the
response path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: isolate session:patch hook payload (#53880) (thanks @graciegould)

---------

Co-authored-by: “graciegould” <“graciegould5@gmail.com”>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>

* fix: unify log timestamp offsets (#38904) (thanks @sahilsatralkar)

* fix: skip session:patch hook clone without listeners

* fix: preserve before_dispatch delivery semantics (#50444) (thanks @gfzhx)

* Plugins: add before_dispatch hook

* Tests: fix before_dispatch hook mock typing

* Rebase: adapt before_dispatch hook to routeReplyRuntime refactor

* fix: preserve before_dispatch delivery semantics (#50444) (thanks @gfzhx)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>

* fix(whatsapp): avoid eager login tool runtime access

* fix: normalize before_dispatch conversation id

* test: harden parallels npm update runner

* docs: sort changelog by user impact

* test: collapse telegram helper suites

* test: collapse telegram context and transport suites

* docs: add missing changelog items

* refactor: align pairing replies, daemon hints, and feishu mention policy

* test: collapse telegram button and access suites

* test: collapse telegram transport and status suites

* fix(whatsapp): unwrap FutureProofMessage (botInvokeMessage) to restore reply-to-bot detection

* fix(whatsapp): compare selfLid for reply-to-bot implicit mention in groups

* fix(whatsapp): read selfLid from creds.json for reply-to-bot detection

* fix(whatsapp): use async fs.promises.readFile for selfLid creds read

* refactor(auth): separate profile ids from email metadata

* refactor(openai): extract codex auth identity helper

* test: isolate voice-call temp stores

* build: prepare 2026.3.24-beta.1

* fix: copy openclaw bin before docker install

* refactor: unify whatsapp identity handling

* test: fix clobbered config snapshot expectation

* fix(config): ignore same-base correction publish warnings

* test: add Open WebUI docker smoke

* fix(media): align outbound media access with fs policy

* fix(update): preflight npm target node engine

* fix(ci): restore e2e docker cache boundary

* test(release): sync llama peer fixture

* Discord: log rejected native command deploy failures (#54118)

Merged via squash.

Prepared head SHA: be250f96204ed6dc755c10d2b9640f7dd49bc70c
Co-authored-by: huntharo <5617868+huntharo@users.noreply.github.com>
Co-authored-by: huntharo <5617868+huntharo@users.noreply.github.com>
Reviewed-by: @huntharo

* fix(runtime): support Node 22.14 installs

* test(media): make local roots fixture windows-safe

* Tests: isolate security audit home skill resolution (#54473)

Merged via squash.

Prepared head SHA: 82181e15fbfa47244cf166f57f40646bc35629c6
Co-authored-by: huntharo <5617868+huntharo@users.noreply.github.com>
Co-authored-by: huntharo <5617868+huntharo@users.noreply.github.com>
Reviewed-by: @huntharo

* docs: prepare 2026.3.24-beta.2 release

* fix(sandbox): honor sandbox alsoAllow and explicit re-allows (#54492)

* fix(sandbox): honor effective sandbox alsoAllow policy

* fix(sandbox): prefer resolved sandbox context policy

* fix: honor sandbox alsoAllow policy (#54492) (thanks @ngutman)

* fix(feishu): close WebSocket connections on monitor stop (#52844)

* fix(feishu): close WebSocket connections on monitor stop/abort

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test(feishu): add WebSocket cleanup tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(feishu): close WebSocket connections on monitor stop (#52844) (thanks @schumilin)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: George Zhang <georgezhangtj97@gmail.com>

* fix(feishu): use message create_time for inbound timestamps (#52809)

* fix(feishu): use message create_time instead of Date.now() for Timestamp field

When a message is sent offline and later retried by the Feishu client
upon reconnection, Date.now() captures the *delivery* time rather than
the *authoring* time.  This causes downstream consumers to see a
timestamp that can be minutes or hours after the user actually composed
the message, leading to incorrect temporal semantics — for example, a
"delete this" command may target the wrong resource because the agent
believes the instruction was issued much later than it actually was.

Replace every Date.now() used for message timestamps with the original
create_time from the Feishu event payload (millisecond-epoch string),
falling back to Date.now() only when the field is absent.  The
definition is also hoisted to the top of handleFeishuMessage so that
both the pending-history path and the main inbound-payload path share
the same authoritative value.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test(feishu): verify Timestamp uses message create_time

Add two test cases:
1. When create_time is present, Timestamp must equal the parsed value
2. When create_time is absent, Timestamp falls back to Date.now()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: revert unrelated formatting change to lifecycle.test.ts

This file was inadvertently formatted in a prior commit. Reverting to
match main and keep the PR scoped to the Feishu timestamp fix only.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(feishu): use message create_time for inbound timestamps (#52809) (thanks @schumilin)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: George Zhang <georgezhangtj97@gmail.com>

* Fix local copied package installs honoring staged project .npmrc (#54543)

* fix(release): add plugin-sdk:check-exports to release:check (#54283)

* fix(plugins): resolve sdk alias from import.meta.url for external plugins

When a plugin is installed outside the openclaw package (e.g.
~/.openclaw/extensions/), resolveLoaderPluginSdkPackageRoot() fails to
locate the openclaw root via cwd or argv1 hints, resulting in an empty
alias map. Jiti then cannot resolve openclaw/plugin-sdk/* imports and
the plugin fails to load with "Cannot find module".

Since sdk-alias.ts is always compiled into the openclaw package itself,
import.meta.url reliably points inside the installation directory. Add it
as an unconditional fallback in resolveLoaderPluginSdkPackageRoot() so
external plugins can always resolve the plugin SDK.

Fixes: Error: Cannot find module 'openclaw/plugin-sdk/plugin-entry'

* fix(plugins): pass loader moduleUrl to resolve sdk alias for external plugins

The previous approach of adding import.meta.url as an unconditional
fallback inside resolveLoaderPluginSdkPackageRoot() broke test isolation:
tests that expected null from untrusted fixtures started finding the real
openclaw root. Revert that and instead thread an optional moduleUrl through
buildPluginLoaderAliasMap → resolvePluginSdkScopedAliasMap →
listPluginSdkExportedSubpaths → resolveLoaderPluginSdkPackageRoot.

loader.ts passes its own import.meta.url as the hint, which is always
inside the openclaw installation. This guarantees the sdk alias map is
built correctly even when argv1 does not resolve to the openclaw root
(e.g. single-binary distributions, custom launchers, or Docker images
where the binary wrapper is not a standard npm symlink).

Tests that call sdk-alias helpers directly without moduleUrl are
unaffected and continue to enforce the existing isolation semantics.
A new test covers the moduleUrl resolution path explicitly.

* fix(plugins): use existing fixture file for moduleUrl hint in test

The previous test pointed loaderModuleUrl to dist/plugins/loader.js
which is not created by createPluginSdkAliasFixture, causing resolution
to fall back to the real openclaw root instead of the fixture root.
Use fixture.root/openclaw.mjs (created by the bin+marker fixture) so
the moduleUrl hint reliably resolves to the fixture package root.

* fix(test): use fixture.root as cwd in external plugin alias test

When process.cwd() is mocked to the external plugin dir, the
findNearestPluginSdkPackageRoot(process.cwd()) fallback resolves to
the real openclaw repo root in the CI test runner, making the test
resolve the wrong aliases. Using fixture.root as cwd ensures all
resolution paths consistently point to the fixture.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(release): add plugin-sdk:check-exports to release:check

plugin-sdk subpath exports (e.g. openclaw/plugin-sdk/plugin-entry,
openclaw/plugin-sdk/provider-auth) were missing from the published
package.json, causing external plugins to fail at load time with
'Cannot find module openclaw/plugin-sdk/plugin-entry'.

Root cause: sync-plugin-sdk-exports.mjs syncs plugin-sdk-entrypoints.json
into package.json exports, but this sync was never validated in the
release:check pipeline. As a result, any drift between
plugin-sdk-entrypoints.json and the published package.json goes
undetected until users hit the runtime error.

Fix: add plugin-sdk:check-exports to release:check so the CI gate
fails loudly if the exports are out of sync before publishing.

* fix(test): isolate moduleUrl hint test from process.cwd() fallback

Use externalPluginRoot as cwd instead of fixture.root, so only the
moduleUrl hint can resolve the openclaw package root. Previously,
withCwd(fixture.root) allowed the process.cwd() fallback to also
resolve the fixture root, making the moduleUrl path untested.

Spotted by greptile-apps review on #54283.

* fix(test): use empty string to disable argv1 in moduleUrl hint test

Passing undefined for argv1 in buildPluginLoaderAliasMap triggers the
STARTUP_ARGV1 default (process.argv[1], the vitest runner binary inside
the openclaw repo). resolveTrustedOpenClawRootFromArgvHint then resolves
to the real openclaw root before the moduleUrl hint is checked, making
the test resolve wrong aliases.

Pass "" instead: falsy so the hint is skipped, but does not trigger the
default parameter value. Only the moduleUrl can bridge the gap.

Made-with: Cursor

* fix(plugins): thread moduleUrl through SDK alias resolution for external plugins (#54283) Thanks @xieyongliang

---------

Co-authored-by: bojsun <bojie.sun@bytedance.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Jerry <jerry@JerrydeMacBook-Air-2.local>
Co-authored-by: yongliang.xie <yongliang.xie@bytedance.com>
Co-authored-by: George Zhang <georgezhangtj97@gmail.com>

* feat(minimax): add image generation provider and trim model catalog to M2.7 (#54487)

* feat(minimax): add image generation and TTS providers, trim TUI model list

Register MiniMax image-01 and speech-2.8 models as plugin providers for
the image_generate and TTS tools. Both resolve CN/global base URLs from
the configured model endpoint origin.

- Image generation: base64 response, aspect-ratio support, image-to-image
  via subject_reference, registered for minimax and minimax-portal
- TTS: speech-2.8-turbo (default) and speech-2.8-hd, hex-encoded audio,
  voice listing via get_voice API, telephony PCM support
- Add MiniMax to TTS auto-detection cascade (after ElevenLabs, before
  Microsoft) and TTS config section
- Remove MiniMax-VL-01, M2, M2.1, M2.5 and variants from TUI picker;
  keep M2.7 and M2.7-highspeed only (backend routing unchanged)

* feat(minimax): trim legacy model catalog to M2.7 only

Cherry-picked from temp/feat/minimax-trim-legacy-models (949ed28).
Removes MiniMax-VL-01, M2, M2.1, M2.5 and variants from the model
catalog, model order, modern model matchers, OAuth config, docs, and
tests. Keeps only M2.7 and M2.7-highspeed.

Conflicts resolved:
- provider-catalog.ts: removed MINIMAX_TUI_MODELS filter (no longer
  needed since source array is now M2.7-only)
- index.ts: kept image generation + speech provider registrations
  (added by this branch), moved media understanding registrations
  earlier (as intended by the cherry-picked commit)

* fix(minimax): update discovery contract test to reflect M2.7-only catalog

Cherry-picked from temp/feat/minimax-trim-legacy-models (2c750cb).

* feat(minimax): add web search provider and register in plugin entry

* fix(minimax): resolve OAuth credentials for TTS speech provider

* MiniMax: remove web search and TTS providers

* fix(minimax): throw on empty images array after generation failure

* feat(minimax): add image generation provider and trim catalog to M2.7 (#54487) (thanks @liyuan97)

---------

Co-authored-by: tars90percent <tars@minimaxi.com>
Co-authored-by: George Zhang <georgezhangtj97@gmail.com>

* build: prepare 2026.3.24 release

* docs: format changelog for release

* fix: reconcile session compaction count after late compaction success (#45493)

Merged via squash.

Prepared head SHA: d0715a5555791dd44a406d4732843454a3e9619e
Co-authored-by: jackal092927 <3854860+jackal092927@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman

* build: update appcast for 2026.3.24

* Telegram: ignore self-authored DM message updates (#54530)

Merged via squash.

Prepared head SHA: c1c8a851682944c49259afe1741d6d976016b08b
Co-authored-by: huntharo <5617868+huntharo@users.noreply.github.com>
Co-authored-by: huntharo <5617868+huntharo@users.noreply.github.com>
Reviewed-by: @huntharo

* test: keep vitest on forks only

* fix: stop leaking reply tags in iMessage outbound text (#39512) (thanks @mvanhorn)

* fix: stop leaking reply tags in iMessage outbound text (#39512) (thanks @mvanhorn)

* fix: preserve iMessage outbound whitespace without directive tags (#39512) (thanks @mvanhorn)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>

* fix: mid-turn 429 rate limit silent no-reply and context engine registration failure (#50930)

Merged via squash.

Prepared head SHA: eea7800df31f30caabfec2aefe8f20008365a8e8
Co-authored-by: infichen <13826604+infichen@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman

* plugin-runtime: expose runHeartbeatOnce in system API (#40299)

* plugin-runtime: expose runHeartbeatOnce in system API

Plugins that enqueue system events and need the agent to deliver
responses to the originating channel currently have no way to
override the default `heartbeat.target: "none"` behaviour.

Expose `runHeartbeatOnce` in the plugin runtime `system` namespace
so plugins can trigger a single heartbeat cycle with an explicit
`heartbeat: { target: "last" }` override — the same pattern the
cron service already uses (see #28508).

Changes:
- Add `RunHeartbeatOnceOptions` type and `runHeartbeatOnce` to
  `PluginRuntimeCore.system` (types-core.ts)
- Wire the function through a thin wrapper in runtime-system.ts
- Update the test-utils plugin-runtime mock

Made-with: Cursor

* feat(plugins): expose runHeartbeatOnce in system API (#40299) (thanks @loveyana)

---------

Co-authored-by: George Zhang <georgezhangtj97@gmail.com>

* fix(compaction): surface safeguard cancel reasons and clarify /compact skips (#51072)

Merged via squash.

Prepared head SHA: f1dbef044384fbd79ca5ef3616ad37fb03b05fae
Co-authored-by: afurm <6375192+afurm@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman

* Fix/telegram writeback admin scope gate (#54561)

* fix(telegram): require operator.admin for legacy target writeback persistence

* Address claude feedback

* Update extensions/telegram/src/target-writeback.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Remove stray brace

* Add updated docs

* Add missing test file, address codex concerns

* Fix test formatting error

* Address comments, fix tests

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* test: fix windows tmp root assertions

* fix: per-model cooldown scope, stepped backoff, and user-facing rate-limit message (#49834)

Merged via squash.

Prepared head SHA: 7c488c070c0cafb5a4b53c598d8ccd38c418b67c
Co-authored-by: kiranvk-2011 <91108465+kiranvk-2011@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf

* fix: reject path traversal and home-dir patterns in media parse layer (#54642)

* fix: reject path traversal and home-dir patterns in media parse layer

* Update parse tests

* Block reset-profile on lower-privilege browser request surfaces (#54618)

* Block reset-profile on lower-privilege browser request surfaces

* add missing tests

* Fix tests

* Test fix

* fix: trigger compaction on LLM timeout with high context usage (#46417)

Merged via squash.

Prepared head SHA: 619bc4c1fa1db3829ff3aa78ffcab8e4201379b5
Co-authored-by: joeykrug <5925937+joeykrug@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman

* fix(talk-voice): enforce operator.admin scope on /voice set config writes (#54461)

* fix(talk-voice): enforce operator.admin scope on /voice set config writes

* fix(talk-voice): align scope guard with phone-control pattern

Use optional chaining (?.) instead of Array.isArray so webchat callers
with undefined scopes are rejected, matching the established pattern in
phone-control. Add test for webchat-with-no-scopes case.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: apply host-env blocklist to auth-profile env refs in daemon install (#54627)

* fix: apply host-env blocklist to auth-profile env refs in daemon install

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ci: retrigger checks

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* OpenShell: exclude hooks/ from mirror sync (#54657)

* OpenShell: exclude hooks/ from mirror sync

* OpenShell: make excludeDirs case-insensitive for cross-platform safety

* Trigger preflight compaction from transcript estimates when usage is stale (#49479)

Merged via squash.

Prepared head SHA: 8d214b708b433702aba21b86ad19b6e2721c3ee0
Co-authored-by: jared596 <37019497+jared596@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman

* refactor(sandbox): remove tool policy facade (#54684)

* refactor(sandbox): remove tool policy facade

* fix(sandbox): harden blocked-tool guidance

* fix(sandbox): avoid control-char guidance leaks

* fix: harden sandbox blocked-tool guidance (#54684) (thanks @ngutman)

* fix(ci): restore main green

* Filter untrusted CWD .env entries before OpenClaw startup (#54631)

* Filter untrusted CWD .env entries before OpenClaw startup

* Add missing test file

* Fix missing and updated files

* Address feedback

* Feedback updates

* Feedback update

* Add test coverage

* Unit test fix

* fix: enforce localRoots sandbox on Feishu docx upload file reads (#54693)

* fix: enforce localRoots sandbox on Feishu docx upload file reads

* Formatting fixes

* Update tests

* Feedback updates

* test: introduce planner-backed test runner, stabilize local builds (#54650) 

* test: stabilize ci and local vitest workers

* test: introduce planner-backed test runner

* test: address planner review follow-ups

* test: derive planner budgets from host capabilities

* test: restore planner filter helper import

* test: align planner explain output with execution

* test: keep low profile as serial alias

* test: restrict explicit planner file targets

* test: clean planner exits and pnpm launch

* test: tighten wrapper flag validation

* ci: gate heavy fanout on check

* test: key shard assignments by unit identity

* ci(bun): shard vitest lanes further

* test: restore ci overlap and stabilize planner tests

* test: relax planner output worker assertions

* test: reset plugin runtime state in optional tools suite

* ci: split macos node and swift jobs

* test: honor no-isolate top-level concurrency budgets

* ci: fix macos swift format lint

* test: cap max-profile top-level concurrency

* ci: shard macos node checks

* ci: use four macos node shards

* test: normalize explain targets before classification

* feat(cli): add json schema to cli tool (#54523)

Merged via squash.

Prepared head SHA: 39c15ee70d04a1721ebafde327b3b95355653fac
Co-authored-by: kvokka <15954013+kvokka@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf

* fix(schema): tools.web.fetch.maxResponseBytes #53397 (#53401)

Merged via squash.

Prepared head SHA: 5d10a98bdb1f7fc3065adf438c4d572c3d61f8ce
Co-authored-by: erhhung <5808864+erhhung@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf

* fix: make buttons schema optional in message tool (#54418)

Merged via squash.

Prepared head SHA: 0805c095e930d669ba1b3aedc09baec80882ce45
Co-authored-by: adzendo <246828680+adzendo@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf

* fix(gateway): block silent reconnect scope-upgrade escalation (#54694)

* fix(gateway): block silent reconnect scope-upgrade escalation

* formatting updateas

* Resolve feedback

* formatting fixes

* Update src/gateway/server.silent-scope-upgrade-reconnect.poc.test.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Feedback updates

* fix unit test

* Feedback update

* Review feedback update

* More Greptile nit fixes

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* fix: allow msteams feedback and welcome config keys (#54679)

Merged via squash.

Prepared head SHA: f56a15ddeaeb5369fd8feac09bb89d3d600d6ff4
Co-authored-by: gumclaw <265388744+gumclaw@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf

* fix(mattermost): thread resolved cfg through reply delivery send calls (#48347)

Merged via squash.

Prepared head SHA: 7ca468e365087c802be32675696080a8dd11fae2
Co-authored-by: mathiasnagler <9951231+mathiasnagler@users.noreply.github.com>
Co-authored-by: mukhtharcm <56378562+mukhtharcm@users.noreply.github.com>
Reviewed-by: @mukhtharcm

* feat: add video generation core infrastructure and extend image generation parameters (#53681)

* feat: add video generation core infrastructure and extend image generation parameters

Add full video generation capability to OpenClaw core:

- New `video_generate` agent tool with support for prompt, duration, aspect ratio,
  resolution, seed, watermark, I2V (first/last frame), camerafixed, and draft mode
- New `VideoGenerationProvider` plugin SDK type and `registerVideoGenerationProvider` API
- New `src/video-generation/` module (types, runtime with fallback, provider registry)
- New `openclaw/plugin-sdk/video-generation` export for external plugins
- 200MB max file size for generated videos (vs default 5MB for images)

Extend image generation with additional parameters:
- `seed`, `watermark`, `guidanceScale`, `optimizePrompt`, `providerOptions`
- New `readBooleanParam()` helper in tool common utilities

Update plugin registry, contracts, and all test mocks to include
`videoGenerationProviders` and `videoGenerationProviderIds`.

Made-with: Cursor

* fix: validate aspect ratio against target provider when model override is set

* cleanup: remove redundant ?? undefined from video/image generate tools

* chore: regenerate plugin SDK API baseline after video generation additions

---------

Co-authored-by: yongliang.xie <yongliang.xie@bytedance.com>

* fix: deliver verbose tool summaries in Telegram forum topics (#43236) (thanks @frankbuild)

* fix(auto-reply): deliver verbose tool summaries in Telegram forum topics

Forum topics have ChatType 'group' but are threaded conversations where
verbose tool output should be delivered (same as DMs). The
shouldSendToolSummaries gate now checks IsForum to allow tool summaries
in forum topic sessions.

Fixes #43206

* test: add sendToolResult count assertion per review feedback

* fix: add changelog for forum topic verbose tool summaries (#43236) (thanks @frankbuild)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>

* docs: add WeChat channel via official Tencent iLink Bot plugin (#52131) (thanks @MonkeyLeeT)

* docs: add WeChat channel via official Tencent iLink Bot plugin

Add WeChat to the README channel lists and setup section.

Uses the official Tencent-published plugin @tencent-weixin/openclaw-weixin
which connects via the iLink Bot API (QR code login, long-poll).
Requires WeChat 8.0.70+ with the ClawBot plugin enabled; the plugin
is being rolled out gradually by Tencent.

Covers: setup steps, capabilities (DM-only, media up to 100 MB,
multi-account, pairing authorization, typing indicators, config path),
and the context token restart caveat.

* docs: update WeChat plugin install for v2.0 compatibility

- Add version compatibility note (v2.x requires OpenClaw >= 2026.3.22,
  @legacy tag for older hosts)
- Add plugins.allow step (required since plugins.allow was introduced)

* docs: drop manual plugins.allow/enable steps (handled by plugins install)

* docs: fix multi-account instruction to require explicit --account id

* docs: trim WeChat section to match neighboring channels, fix pairing link

* docs: sync WeChat channel docs

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>

* ci: collapse preflight manifest routing (#54773)

* ci: collapse preflight manifest routing

* ci: fix preflight workflow outputs

* ci: restore compat workflow tasks

* ci: match macos shards to windows

* ci: collapse macos swift jobs

* ci: skip empty submodule setup

* ci: drop submodule setup from node env

* fix(whatsapp): clarify allowFrom policy error (#54850)

* fix: resolve telegram token fallback for binding-created accounts (#54362) (thanks @openperf)

* fix(telegram): resolve channel-level token fallthrough for binding-created accountIds

Fixes #53876

* fix(telegram): align isConfigured with resolveTelegramToken multi-bot guard

* fix(telegram): use normalized account lookup and require available token

* fix: tighten systemd duplicate gateway detection (#45328) (thanks @gregretkowski)

* daemon: tighten systemd duplicate gateway detection (#15849)

* fix three issues from PR review

* fix windows unit tests due to posix/windows path differences
* ensure line continuations are handled in systemd units
* fix misleading test name

* attempt fix windows test due to fs path separator

* fix system_dir separator, fix platform side-effect

* change approach for mocking systemd filesystem test

* normalize systemd paths to linux style

* revert to vers that didnt impact win32 tests

* back out all systemd inspect tests

* change test approach to avoid other tests issues

* fix: tighten systemd duplicate gateway detection (#45328) (thanks @gregretkowski)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>

* fix: use provider-aware context window lookup (#54796) (thanks @neeravmakwana)

* fix(status): use provider-aware context window lookup

* test(status): cover provider-aware context lookup

* fix: use provider-aware context window lookup (#54796) (thanks @neeravmakwana)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>

* fix: restore CLI message transcript mirroring (#54187) (thanks @KevInTheCloud5617)

* fix: pass agentId in CLI message command to enable session transcript writes

The CLI `openclaw message send` command was not passing `agentId` to
`runMessageAction()`, causing the outbound session route resolution to
be skipped (it's gated on `agentId && !dryRun`). Without a route, the
`mirror` object is never constructed, and `appendAssistantMessageToSessionTranscript()`
is never called.

This fix resolves the agent ID from the config (defaulting to "main")
and passes it through, enabling transcript mirroring for all channels
when using the CLI.

Closes #54186

* fix: format message.ts with oxfmt

* fix: use resolveDefaultAgentId instead of cfg.agent

* fix: restore CLI message transcript mirroring (#54187) (thanks @KevInTheCloud5617)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>

* fix: restore Kimi Code under Moonshot setup (#54619) (thanks @sparkyrider)

* Onboarding: restore Kimi Code under Moonshot setup

* Update extensions/kimi-coding/index.ts

Fix naming convention in metadata

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* fix: auto-enable configured channel plugins in routed CLI commands (#54809) (thanks @neeravmakwana)

* CLI: auto-enable configured channel plugins in routed commands

* fix: auto-enable configured channel plugins in routed CLI commands (#54809) (thanks @neeravmakwana)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>

* fix: restore image-tool generic provider fallback (#54858) (thanks @MonkeyLeeT)

* Image tool: restore generic provider fallback

* Image tool: cover multi-image generic fallback

* test: tighten minimax-portal image fallback coverage

* fix: restore image-tool generic provider fallback (#54858) (thanks @MonkeyLeeT)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>

* fix: support OpenAI Codex media understanding (#54829) (thanks @neeravmakwana)

* OpenAI: register Codex media understanding provider

* fix: route codex image prompts through system instructions

* fix: add changelog for codex image tool fix (#54829) (thanks @neeravmakwana)

* fix: remove any from provider registration tests (#54829) (thanks @neeravmakwana)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>

* fix(whatsapp): drop fromMe echoes in self-chat DMs using outbound ID tracking (#54570)

Merged via squash.

Prepared head SHA: dad53caf3974e2e90a358d0561a0b0ee9bd924f2
Co-authored-by: joelnishanth <140015627+joelnishanth@users.noreply.github.com>
Co-authored-by: mcaxtr <7562095+mcaxtr@users.noreply.github.com>
Reviewed-by: @mcaxtr

* Revert "feat: add video generation core infrastructure and extend image generation parameters (#53681)" (#54943)

This reverts commit 4cb8dde894e3a85ce81b770869d7bd86e6b6e7a1.

* msteams: fetch thread history via Graph API for channel replies (#51643)

* msteams: fetch thread history via Graph API for channel replies

* msteams: address PR #51643 review feedback

- Wrap resolveTeamGroupId Graph call in try/catch, fall back to raw
  conversationTeamId when Team.ReadBasic.All permission is missing
- Remove dead fetchChatMessages function (exported but never called)
- Add JSDoc documenting oldest-50-replies Graph API limitation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* msteams: address thread history PR review comments

* msteams: only cache team group IDs on successful Graph lookup

Avoid caching raw conversationTeamId as a Graph team GUID when the
/teams/{id} lookup fails — the raw ID may be a Bot Framework conversation
key, not a valid GUID, causing silent thread-history failures for the
entire cache TTL.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* msteams: add search message action (#54832)

* msteams: add pin/unpin, list-pins, and read message actions

Wire up Graph API endpoints for message read, pin, unpin, and list-pins
in the MS Teams extension, following the same patterns as edit/delete.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* msteams: address PR review comments for pin/unpin/read actions

- Handle 204 No Content in postGraphJson (Graph mutations may return empty body)
- Strip conversation:/user: prefixes in resolveConversationPath to avoid Graph 404s
- Remove dead variable in channel pin branch
- Rename unpin param from messageId to pinnedMessageId for semantic clarity
- Accept both pinnedMessageId and messageId in unpin action handler for compat

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* msteams: resolve user targets + add User-Agent to Graph helpers

- Resolve user:<aadId> targets to actual conversation IDs via conversation
  store before Graph API calls (fixes 404 for DM-context actions)
- Add User-Agent header to postGraphJson/deleteGraphRequest for consistency
  with fetchGraphJson after rebase onto main

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* msteams: resolve DM targets to Graph chat IDs + expose pin IDs

- Prefer cached graphChatId over Bot Framework conversation IDs for user
  targets; throw descriptive error when no Graph-compatible ID is available
- Add `id` field to list-pins rows so default formatters surface the pinned
  resource ID needed for the unpin flow

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* msteams: add react and reactions (list) message actions

* msteams: add search message action via Graph API

* msteams: fix search query injection, add ConsistencyLevel header, use manual query string

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(plugins): skip allowlist warning for config paths

* feat: Add Microsoft Foundry provider with Entra ID authentication (#51973)

* Microsoft Foundry: add native provider

* Microsoft Foundry: tighten review fixes

* Microsoft Foundry: enable by default

* Microsoft Foundry: stabilize API routing

* fix: restore inbound image embedding for CLI routed BlueBubbles turns (#51373)

* fix(cli): hydrate prompt image refs for inbound media

* Agents: harden CLI prompt image hydration (#51373)

* test: fix CLI prompt image hydration helper mocks

* fix: route codex responses over websocket and preserve tool warnings (#53702) (thanks @Nanako0129)

* fix: route codex responses over websocket and suppress gated core tool warnings

* fix: rebase codex websocket patch onto main

* fix: preserve explicit alsoAllow warnings (#53702) (thanks @Nanako0129)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>

* Docs: rename modelstudio.md to qwen_modelstudio.md, add Standard API endpoints (#54407)

* Docs: rename modelstudio.md to qwen_modelstudio.md, add Standard API endpoints

* refine docs

* Docs: fix broken link in providers/index.md after modelstudio rename

* Docs: add redirect from /providers/modelstudio to /providers/qwen_modelstudio

* Docs: adjust the order in index.md

* docs: rename modelstudio to qwen_modelstudio, add Standard API endpoints (#54407) (thanks @wenmengzhou)

---------

Co-authored-by: George Zhang <georgezhangtj97@gmail.com>

* fix: wire microsoft foundry into contract registry

* docs: refresh config baseline for microsoft foundry

* fix: add slack upload-file action (#54987) (thanks @kevinlin-openai)

* feat(slack): add upload-file action

Co-authored-by: Codex <noreply@openai.com>

* fix(slack): guard upload-file routing

Co-authored-by: Codex <noreply@openai.com>

* fix(slack): tighten upload-file validation

---------

Co-authored-by: kevinlin-openai <kevin@dendron.so>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>

* build: update plugin sdk api baseline

* Remove Qwen OAuth integration (qwen-portal-auth) (#52709)

* Remove Qwen OAuth integration (qwen-portal-auth)

Qwen OAuth via portal.qwen.ai is being deprecated by the Qwen team due
to traffic impact on their primary Qwen Code user base. Users should
migrate to the officially supported Model Studio (Alibaba Cloud Coding
Plan) provider instead.

Ref: https://github.com/openclaw/openclaw/issues/49557

- Delete extensions/qwen-portal-auth/ plugin entirely
- Remove qwen-portal from onboarding auth choices, provider aliases,
  auto-enable list, bundled plugin defaults, and pricing cache
- Remove Qwen CLI credential sync (external-cli-sync, cli-credentials)
- Remove QWEN_OAUTH_MARKER from model auth markers
- Update docs/providers/qwen.md to redirect to Model Studio
- Update model-providers docs (EN + zh-CN) to remove Qwen OAuth section
- Regenerate config and plugin-sdk baselines
- Update all affected tests

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* Clean up residual qwen-portal references after OAuth removal

* Add migration hint for deprecated qwen-portal OAuth provider

* fix: finish qwen oauth removal follow-up

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Frank Yang <frank.ekn@gmail.com>

* fix(extensions): route fetch calls through fetchWithSsrFGuard (#53929)

* fix(extensions): route fetch calls through fetchWithSsrFGuard

Replace raw fetch() with fetchWithSsrFGuard in BlueBubbles, Mattermost,
Nextcloud Talk, and Thread Ownership extensions so outbound requests go
through the shared DNS-pinning and network-policy layer.

BlueBubbles: thread allowPrivateNetwork from account config through all
fetch call sites (send, chat, reactions, history, probe, attachments,
multipart). Add _setFetchGuardForTesting hook for test overrides.

Mattermost: add guardedFetchImpl wrapper in createMattermostClient that
buffers the response body before releasing the dispatcher. Handle
null-body status codes (204/304).

Nextcloud Talk: wrap both sendMessage and sendReaction with
fetchWithSsrFGuard and try/finally release.

Thread Ownership: add fetchWithSsrFGuard and ssrfPolicyFromAllowPrivateNetwork
to the plugin SDK surface; use allowPrivateNetwork:true for the
Docker-internal forwarder.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(extensions): improve null-body handling and test harness cleanup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(bluebubbles): default to strict SSRF policy when allowPrivateNetwork is unset

Callers that omit allowPrivateNetwork previously got undefined policy,
which caused blueBubblesFetchWithTimeout to fall through to raw fetch
and bypass the SSRF guard entirely.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(bluebubbles): thread allowPrivateNetwork through action and monitor call sites

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(mattermost,nextcloud-talk): add allowPrivateNetwork config for self-hosted/LAN deployments

* fix: regenerate config docs baseline for new allowPrivateNetwork fields

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* BlueBubbles: enrich group participants with local Contacts names (#54984)

* BlueBubbles: enrich group participants with Contacts names

* BlueBubbles: gate contact enrichment behind opt in config

* chore: add lockfile entry for `extensions/microsoft-foundry`

* fix(discord): force fresh gateway reconnects (#54697)

* fix(discord): force fresh gateway reconnects

* fix(discord): harden forced reconnect teardown

* fix(discord): retry after socket drain timeouts

* fix(discord): guard forced socket teardown

* fix(discord): stop cleanly during reconnect drain

* Feishu: validate webhook signatures before parsing (#55083)

* Feishu: validate webhook signatures before parsing

* Scripts: allow Feishu raw body guard callsite

* fix(agents): enforce session_status guard after sessionId resolution (#55105)

* fix(agents): enforce visibility guard after sessionId resolution in session_status

When a sessionId (rather than an explicit agent key) is passed to the
session_status tool, the sessionId resolution block rewrites
requestedKeyRaw to an explicit "agent:..." key.  The subsequent
visibility guard check at line 375 tested
`!requestedKeyRaw.startsWith("agent:")`, which was now always false
after resolution — skipping the visibility check entirely.

This meant a sandboxed agent could bypass visibility restrictions by
providing a sessionId instead of an explicit session key.

Fix: use the original `isExplicitAgentKey` flag (captured before
resolution) instead of re-checking the dynamic requestedKeyRaw.
This ensures the visibility guard runs for sessionId inputs while
still skipping the redundant check for inputs that were already
validated at the earlier explicit-key check (lines 281-286).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: cover session status sessionId guard

* test: align parent sessionId guard coverage

---------

Co-authored-by: Kevin Sheng <shenghuikevin@github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Telegram: enforce DM auth for callbacks (#55112)

* fix(msteams): align feedback invoke authorization (#55108)

* msteams: align feedback invoke authorization

* msteams: fix feedback allowlist regressions

* msteams: tighten feedback group authorization

* fix(bluebubbles): auto-allow private network for local serverUrl and add allowPrivateNetwork to channel schema

* Matrix: gate verification notices on DM access (#55122)

* fix: surface provider-specific rate limit error message (#54433) (#54512)

Merged via squash.

Prepared head SHA: 755cff833c1f6aca06ab2c3e1f802f35e7c4d553
Co-authored-by: bugkill3r <2924124+bugkill3r@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf

* test: fix bluebubbles attachment ssrf expectations

* docs: add beta release testing guidance

* fix(bluebubbles): enable group participant enrichment by default, add fallback fetch and handle field aliases

* fix: prefer freshest duplicate row promotion

* fix: replace stale canonical duplicate rows

* docs: sync config baseline

* build: prepare 2026.3.25 unreleased

* ACP: sanitize terminal tool titles (#55137)

* ACP: sanitize terminal tool titles

Co-authored-by: nexrin <268879349+nexrin@users.noreply.github.com>

* Config: refresh config baseline and stabilize restart pid test

---------

Co-authored-by: nexrin <268879349+nexrin@users.noreply.github.com>

* Google Chat: require stable group ids (#55131)

* Google Chat: require stable group ids

* Google Chat: fail closed on deprecated room keys

* Feishu: reject legacy raw card command payloads (#55130)

* Feishu: reject legacy raw card callbacks

* Feishu: cover legacy text card payloads

* Docs: refresh config baseline

* CI: refresh PR checks

* Feishu: limit legacy card guard scope

* fix(bluebubbles): refactor sendMessageBlueBubbles to use resolveBlueBubblesServerAccount and enhance private network handling in tests

* test(memory): initialize providers in lazy manager tests

* test: fix portable stderr capture and env leakage (#55184)

* docs: add beta blocker contributor guidance (#55199)

* docs: add beta blocker contributor guidance

* fix: tighten beta blocker labeling and flaky config test

* docs: update PR template review guidance

* refactor: dedupe msteams graph actions

* fix(bluebubbles): throttle webhook auth guesses (#55133)

* fix(bluebubbles): throttle webhook auth guesses

* test(bluebubbles): isolate attachment ssrf config

* test(bluebubbles): hoist attachment mocks

* docs: refresh bluebubbles config baseline

* fix(bluebubbles): trust proxied webhook client IPs

* fix(bluebubbles): honor trusted proxy webhook IPs

* fix(bluebubbles): honor real-ip fallback for webhooks

* refactor: share web media loader

* fix: avoid duplicate ACP Telegram finals (#55173)

* fix: avoid duplicate final ACP text on telegram

* fix: keep ACP final fallback for non-telegram blocks

* fix: count telegram ACP block replies as success

* fix: recover ACP final fallback after block failures

* fix: settle telegram ACP block delivery before fallback

* test: isolate ACP dispatch mocks under shared workers

* fix: prefer telegram provider for ACP visibility

* refactor: share matrix and telegram dedupe helpers

* fix: prefer freshest duplicate store matches

* feat: pluginize cli inference backends

* test: dedupe foundry auth fixtures

* fix(whatsapp): unwrap quoted wrapper messages

* test: share pi compaction fixtures

* test: share redact and approval fixtures

* fix: preserve metadata on voice session touches

* test: dedupe web search provider fixtures

* synology-chat: throttle webhook token guesses (#55141)

* synology-chat: throttle webhook token guesses

* synology-chat: keep valid webhook traffic within configured limits

* docs: refresh generated config baseline

* synology-chat: enforce lockout after repeated token failures

* fix: preserve reset ownership metadata

* test: improve test runner help text (#55227)

* test: improve test runner help text

* test: print extension help to stdout

* test: leave extension help passthrough alone

* test: parse timing update flags in one pass

* perf: speed up channel test runs

* test: share msteams monitor and pi runner fixtures

* fix: preserve reset spawn depth

* test: dedupe config compatibility fixtures

* ci: make docker release tag-driven

* fix: preserve reset elevated level

* fix: preserve reset spawn context

* test: share subagent and policy test fixtures

* perf: enable local channel planner parallelism on node 25

* test: dedupe remaining agent test seams

* telegram: throttle repeated webhook auth guesses (#55142)

* telegram: throttle repeated webhook auth guesses

* telegram: use per-listener webhook rate limits

* config: stabilize doc baseline ordering

* test: share matrix migration fixtures

* test: share gateway authz and watchdog fixtures

* test: share ui reconnect and storage helpers

* perf: overlap isolated channel runs with shared lane

* fix: preserve reset session behavior config

* fix: preserve reset channel identity

* fix: preserve reset acp session metadata

* fix: preserve reset cli session linkage

* refactor: share auto-reply reply helpers

* test: add docker cli-backend smoke

* docs: note guest openclaw shim in parallels skill

* test: share plugin auth and ui storage fixtures

* test: dedupe matrix setup seams

* test: dedupe ui chat seams

* style: normalize ui slash executor formatting

* fix(ci): refresh plugin sdk api baseline

* fix: keep plugin HTTP runtime scopes least-privileged (#55284)

* Gateway: require caller scope for subagent session deletion (#55281)

* gateway: require pairing for backend scope upgrades (#55286)

* test: dedupe secrets and guardrail fixtures

* bluebubbles: honor reaction mention gating (#55283)

* Gateway: align HTTP session history scopes (#55285)

* Gateway: require scopes for HTTP session history

* Gateway: cover missing HTTP history scope header

* refactor: dedupe gateway and binding helpers

* test: dedupe extension channel fixtures

* Gateway: require requester ownership for HTTP session kills (#55308)

* test: share cli and channel setup fixtures

* test: share auto-reply typing helpers

* refactor: share plugin setup helpers

* ci: optimize windows test shard fanout (#55261)

* ci: reduce windows test shard fanout

* ci: tighten windows shard target

* ci: back off windows shard target

* ci: restore windows shard cap

* fix: include dashboard children in owner filters

* fix: expose parent session keys in sessions list

* fix: expose spawned session owners in sessions list

* fix: keep spawned session owners in live events

* refactor: share browser and sandbox helpers

* refactor: share discord outbound session routing

* refactor: dedupe gateway session resolve visibility

* refactor: share slack and telegram action helpers

* fix: auto-load bundled plugin capabilities from config refs

* test: share cli and doctor test helpers

* fix(ci): repair discord regression tests

* test(gateway): strip MiniMax live scaffolding

* test: share cli command and discord test helpers

* test: dedupe extension channel fixtures

* fix(ci): repair discord message handler tests

* test: reduce remaining clone seams

* perf: speed up test parallelism

* telegram: rebuild transport after stalled polling cycles

* test: share discord monitor fixtures

* fix: preserve cli sessions across model changes

* test: split cli agent command coverage

* test: eliminate remaining clone seams

* refactor: dedupe cli runner session reuse

* fix(ci): repair discord harness regressions

* fix(ci): clean up discord harness types

* fix(agents): classify "Failed to extract accountId from token" as auth error for failover (#27055) (#55206)

Co-authored-by: Lyle Hopkins <55105+cosmicnet@users.noreply.github.com>

* test: dedupe discord provider proxy overrides

* fix(ci): restore discord provider test seams

* fix(ci): format discord provider follow-up

* refactor: split telegram polling and sdk surfaces

* fix: export shared channel action enum helpers

* fix: backfill claude cli chat history

* fix: skip cli backends in models auth warnings

* test: dedupe telegram polling session harness

* fix(ci): repair discord and telegram follow-ups

* refactor: clean plugin capability boundaries

* refactor: share speech normalization helpers

* perf: speed up shared extension test batches

* refactor: simplify bundled plugin contracts

* refactor: move memory flush ownership into memory plugin

* refactor: move memory tooling into memory-core extension

* refactor: add memory-core extension sources

* fix: unify claude cli imported tool messages

* test: share planner and sandbox test helpers

* refactor: split memory-core plugin helpers

* fix: regenerate pnpm-lock.yaml after upstream merge (workspace dep added)

* fix: resolve post-absorb CI failures

After absorbing 140 upstream commits, several botster-specific files
needed updating to match upstream SDK changes:

1. extensions/zulip/index.ts — import order reordered by oxfmt
   (types import after value import, as required by formatter)

2. scripts/lib/plugin-sdk-entrypoints.json + package.json —
   './plugin-sdk/zulip' subpath export missing from upstream's
   entrypoints list (our addition). Added 'zulip' to the canonical
   entrypoints JSON so sync-exports keeps it.

3. src/seks/spine-exec-intercept.test.ts —
   - TextContent|ImageContent union: .text access now requires type
     narrowing (cast to { type: 'text'; text: string })
   - AgentTool now requires execute: test case for missing-execute
     path casts to any (intentionally testing fallback behavior)

4. docs/.generated/* — regenerated config-baseline and
   plugin-sdk-api-baseline after upstream config schema + plugin SDK
   surface changes.

* fix: lint and docs issues from post-absorb CI

- src/seks/spine-exec-intercept.test.ts: replace 'as any' with
  'as Parameters<typeof createSpineExecTool>[0]' to satisfy
  oxlint no-explicit-any rule while still testing the no-execute
  fallback path
- docs/providers/anthropic.md: rename duplicate '### Config snippet'
  heading (added by upstream in ab4de18) to '### Config snippet (claude-cli)'
  to satisfy markdownlint MD024

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: VACInc <hixvac@gmail.com>
Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Co-authored-by: lbo728 <extreme0728@gmail.com>
Co-authored-by: hnshah <hnshah@gmail.com>
Co-authored-by: Bob Shah <bobshah@Macs-Mac-Studio.local>
Co-authored-by: ToToKr <friendnt@g.skku.edu>
Co-authored-by: Andy <lucktv@gmail.com>
Co-authored-by: Sparkyrider <will@willthings.com>
Co-authored-by: Zoher Ghadyali <zoherghadyali@users.noreply.github.com>
Co-authored-by: Liu Yuan <namei.unix@gmail.com>
Co-authored-by: Gracie Gould <66045258+graciegould@users.noreply.github.com>
Co-authored-by: “graciegould” <“graciegould5@gmail.com”>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: ZhangXuan <gfzhx@139.com>
Co-authored-by: khhjoe <joe264326832008@hotmail.com>
Co-authored-by: Harold Hunt <harold@pwrdrvr.com>
Co-authored-by: huntharo <5617868+huntharo@users.noreply.github.com>
Co-authored-by: Nimrod Gutman <nimrod.gutman@gmail.com>
Co-authored-by: Lin Z <schumilin@163.com>
Co-authored-by: George Zhang <georgezhangtj97@gmail.com>
Co-authored-by: Devin Robison <drobison00@users.noreply.github.com>
Co-authored-by: xieyongliang <yongliangxie@hotmail.com>
Co-authored-by: bojsun <bojie.sun@bytedance.com>
Co-authored-by: Jerry <jerry@JerrydeMacBook-Air-2.local>
Co-authored-by: yongliang.xie <yongliang.xie@bytedance.com>
Co-authored-by: liyuan97 <33855278+liyuan97@users.noreply.github.com>
Co-authored-by: tars90percent <tars@minimaxi.com>
Co-authored-by: Jackal Xin <jackalxin2020@gmail.com>
Co-authored-by: jackal092927 <3854860+jackal092927@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
Co-authored-by: Matt Van Horn <mvanhorn@users.noreply.github.com>
Co-authored-by: chenxingzhen <chenxingzhen0204@126.com>
Co-authored-by: infichen <13826604+infichen@users.noreply.github.com>
Co-authored-by: M1a0 <liyuhan.loveyana@bytedance.com>
Co-authored-by: Andrii Furmanets <furmanets.andriy@gmail.com>
Co-authored-by: afurm <6375192+afurm@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: kiranvk2011 <91108465+kiranvk-2011@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Co-authored-by: Joseph Krug <joeykrug@gmail.com>
Co-authored-by: joeykrug <5925937+joeykrug@users.noreply.github.com>
Co-authored-by: Jacob Tomlinson <jacobtomlinson@users.noreply.github.com>
Co-authored-by: Jared <37019497+jared596@users.noreply.github.com>
Co-authored-by: Mikhail Beliakov <kvokka@yahoo.com>
Co-authored-by: kvokka <15954013+kvokka@users.noreply.github.com>
Co-authored-by: Erhhung Yuan <erhhung@gmail.com>
Co-authored-by: erhhung <5808864+erhhung@users.noreply.github.com>
Co-authored-by: adzendo <nickfelixson@adzendo.com>
Co-authored-by: adzendo <246828680+adzendo@users.noreply.github.com>
Co-authored-by: gumclaw <gumclaw@gumroad.com>
Co-authored-by: gumclaw <265388744+gumclaw@users.noreply.github.com>
Co-authored-by: Mathias Nagler <mathiasnagler@users.noreply.github.com>
Co-authored-by: mathiasnagler <9951231+mathiasnagler@users.noreply.github.com>
Co-authored-by: mukhtharcm <56378562+mukhtharcm@users.noreply.github.com>
Co-authored-by: Frank the Builder <frank@codelumberjack.ai>
Co-authored-by: Ted Li <ted@portola.ai>
Co-authored-by: Marcus Castro <7562095+mcaxtr@users.noreply.github.com>
Co-authored-by: wangchunyue <80630709+openperf@users.noreply.github.com>
Co-authored-by: Greg Retkowski <greg@rage.net>
Co-authored-by: Neerav Makwana <neeravmakwana@gmail.com>
Co-authored-by: Kevin Boyle <kevincba17@gmail.com>
Co-authored-by: OfflynAI <140015627+joelnishanth@users.noreply.github.com>
Co-authored-by: sudie-codes <suvenkat95@gmail.com>
Co-authored-by: MetaX e|acc <xuhaoruins@hotmail.com>
Co-authored-by: Tyler Yust <64381258+tyler6204@users.noreply.github.com>
Co-authored-by: Nyanako <44753291+Nanako0129@users.noreply.github.com>
Co-authored-by: wenmeng zhou <wenmeng.zwm@alibaba-inc.com>
Co-authored-by: kevinlin-openai <kevinlin@openai.com>
Co-authored-by: kevinlin-openai <kevin@dendron.so>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: pomelo <czynwu@outlook.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Frank Yang <frank.ekn@gmail.com>
Co-authored-by: Altay <altay@uinaf.dev>
Co-authored-by: Kevin Sheng <shenghuikevin@github.com>
Co-authored-by: Tyler Yust <TYTYYUST@YAHOO.COM>
Co-authored-by: Saurabh Mishra <skmishra1991@gmail.com>
Co-authored-by: bugkill3r <2924124+bugkill3r@users.noreply.github.com>
Co-authored-by: Shakker <shakkerdroid@gmail.com>
Co-authored-by: nexrin <268879349+nexrin@users.noreply.github.com>
Co-authored-by: pkuGeo <geo.zhouhy@gmail.com>
Co-authored-by: Lyle Hopkins <cosmicnet@cpan.org>
Co-authored-by: Lyle Hopkins <55105+cosmicnet@users.noreply.github.com>
lovewanwan pushed a commit to lovewanwan/openclaw that referenced this pull request Apr 28, 2026
…openclaw#45493)

Merged via squash.

Prepared head SHA: d0715a5
Co-authored-by: jackal092927 <3854860+jackal092927@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
…openclaw#45493)

Merged via squash.

Prepared head SHA: d0715a5
Co-authored-by: jackal092927 <3854860+jackal092927@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
…openclaw#45493)

Merged via squash.

Prepared head SHA: d0715a5
Co-authored-by: jackal092927 <3854860+jackal092927@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
goweii pushed a commit to goweii/openclaw that referenced this pull request May 24, 2026
…openclaw#45493)

Merged via squash.

Prepared head SHA: d0715a5
Co-authored-by: jackal092927 <3854860+jackal092927@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
…openclaw#45493)

Merged via squash.

Prepared head SHA: d0715a5
Co-authored-by: jackal092927 <3854860+jackal092927@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants