Skip to content

fix: persist pending followup queues across gateway restart#51623

Closed
tomsalphaclawbot wants to merge 2 commits into
openclaw:mainfrom
tomsalphaclawbot:fix/persist-pending-messages-across-restart
Closed

fix: persist pending followup queues across gateway restart#51623
tomsalphaclawbot wants to merge 2 commits into
openclaw:mainfrom
tomsalphaclawbot:fix/persist-pending-messages-across-restart

Conversation

@tomsalphaclawbot

Copy link
Copy Markdown

Summary

Fixes #51620 — gateway restart drops queued and in-flight inbound messages.

Changes

New: src/auto-reply/reply/queue/persist.ts

Serializes all non-empty FOLLOWUP_QUEUES entries to state/pending-messages.json before shutdown. Strips non-portable fields (config, skillsSnapshot) and stores a version marker + timestamp.

New: src/gateway/server-pending-messages.ts

On startup, reads the persisted file, injects missed messages as system events into their respective sessions (so the agent processes them on next turn), then deletes the file. Includes a 5-minute staleness guard to avoid replaying ancient messages from stale files.

Modified: src/gateway/server-close.ts

Calls persistFollowupQueues(FOLLOWUP_QUEUES) before chatRunState.clear() so queue state is captured while still available. Best-effort — failure doesn't block shutdown.

Modified: src/gateway/server-startup.ts

Calls replayPersistedPendingMessages() 1.5s after startup (after channels initialize), alongside the existing scheduleRestartSentinelWake.

Tests

16 new tests covering:

  • persistFollowupQueues: empty queues, non-empty queues, config stripping, multiple queues
  • consumePersistedQueues: no file, read+delete, staleness guard (>5min), corrupt file, wrong version
  • Round-trip: persist → consume
  • replayPersistedPendingMessages: no entries, empty entries, system event injection, multiple queues, prompt truncation, empty items skip

Limitations / Follow-up

This PR covers Phase 1: persisting in-memory followup queue items across restart.

Still not covered (follow-up work):

  • Phase 2: Messages arriving during the drain window (hitting GatewayDrainingError) — these are rejected at the channel level before reaching the queue
  • Phase 3: Channel-level message acknowledgment deferral — for channels that support it (Slack Socket Mode), defer ACK until message is processed or persisted so the platform can redeliver

Testing

npx vitest run src/auto-reply/reply/queue/persist.test.ts src/gateway/server-pending-messages.test.ts

All 16 tests pass.


Upstream issue: #51620

Before this change, in-memory followup queue items (FollowupQueueState)
were silently lost when the gateway restarted. Messages arriving during
the drain window hit GatewayDrainingError and were dropped with no
persistence or replay mechanism.

This adds:

1. Queue persistence (persist.ts): Before shutdown, serialize non-empty
   followup queues to state/pending-messages.json. Strips non-portable
   fields (config, skillsSnapshot) and includes a version marker.

2. Post-restart replay (server-pending-messages.ts): On startup, read
   the persisted file, inject the missed messages as system events into
   their respective sessions, and delete the file. Includes a 5-minute
   staleness guard to avoid replaying ancient messages.

3. Close handler integration: persistFollowupQueues() is called before
   chatRunState.clear() in server-close.ts so queue state is captured
   while still available.

4. Startup integration: replayPersistedPendingMessages() runs 1.5s after
   startup (after channels initialize) alongside the existing restart
   sentinel wake.

Tests: 16 new tests covering persist, consume, round-trip, staleness,
corruption handling, and system event injection.

Closes openclaw#51620
@openclaw-barnacle openclaw-barnacle Bot added gateway Gateway runtime size: L labels Mar 21, 2026
@greptile-apps

greptile-apps Bot commented Mar 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR implements Phase 1 of crash-safe followup-queue persistence: on graceful shutdown the in-memory FOLLOWUP_QUEUES are serialized to state/pending-messages.json (stripped of non-portable fields), and on the next startup any surviving entries are re-injected as system events so the agent is notified of messages that may not have been processed.

The architecture is clean and fits well with the existing system-events infrastructure. The 5-minute staleness guard, file-delete-before-validate strategy, and best-effort error handling are all sound choices.

Issues found:

  • config: undefined as never type-unsafe cast (src/auto-reply/reply/queue/persist.ts, lines 43–47): JSON.stringify silently drops undefined values, so config will be absent from persisted JSON. The as never cast hides this from the type system. Any Phase 2 code that re-enqueues a replayed FollowupRun and reads item.run.config will get a runtime undefined instead of a type error. A dedicated PersistedFollowupRun type that explicitly omits config and skillsSnapshot would make this contract visible and safe.
  • persistedAt not validated as a number (src/auto-reply/reply/queue/persist.ts, lines 105–117): If the field is absent or non-numeric, Date.now() - parsed.persistedAt yields NaN, and NaN > MAX_AGE_MS is false, silently bypassing the staleness guard. A typeof parsed.persistedAt !== "number" check alongside the existing structural validation would close this gap.

Confidence Score: 4/5

  • Safe to merge with one recommended follow-up: replace config: undefined as never with a typed PersistedFollowupRun before Phase 2 work begins.
  • The core logic is correct and well-tested (16 tests covering persist, consume, staleness, corruption, round-trip, and replay). The shutdown and startup integration points are minimally invasive and correctly ordered. The two flagged issues are non-blocking for Phase 1 — the as never cast works today because Phase 1 replay never reads config, and the persistedAt validation gap requires a deliberately malformed file to exploit. Neither causes incorrect behaviour in normal usage.
  • src/auto-reply/reply/queue/persist.ts — the config: undefined as never cast and missing persistedAt number guard should be addressed before Phase 2 work that re-enqueues replayed items.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/auto-reply/reply/queue/persist.ts
Line: 43-47

Comment:
**`config: undefined as never` silently drops a required field**

Setting `config: undefined as never` suppresses the TypeScript error at compile time, but the JSON round-trip will silently omit the `config` key entirely (since `JSON.stringify` drops `undefined` values). Any Phase 2 code that tries to re-enqueue a replayed `FollowupRun` and access `item.run.config` will get a runtime crash rather than a type error.

A safer approach is to give the persisted form an explicit type that omits the stripped fields, rather than using `as never` to paper over the type mismatch:

```ts
export type PersistedFollowupRun = Omit<FollowupRun, 'run'> & {
  run: Omit<FollowupRun['run'], 'config' | 'skillsSnapshot'>;
};
```

Then use `PersistedFollowupRun[]` inside `PersistedQueueEntry.items` so callers cannot accidentally treat replayed entries as full `FollowupRun` values without re-attaching a live `config`.

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

---

This is a comment left during a code review.
Path: src/auto-reply/reply/queue/persist.ts
Line: 105-117

Comment:
**`persistedAt` type not validated — staleness guard can be bypassed**

`parsed.persistedAt` is cast with `as PersistedQueueFile` but never validated to be an actual number. If the field is absent or is a non-numeric value (e.g., a string), `Date.now() - parsed.persistedAt` produces `NaN`, and `NaN > MAX_AGE_MS` evaluates to `false`, silently skipping the staleness check and potentially replaying arbitrarily old messages.

Consider adding a type guard alongside the existing structural check:

```suggestion
  if (!parsed || parsed.version !== 1 || !Array.isArray(parsed.entries) || typeof parsed.persistedAt !== "number") {
    log.warn("pending-messages.json has unexpected format; discarding");
    return null;
  }
```

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

Last reviewed commit: "fix: persist pending..."

Comment on lines +43 to +47
...item.run,
// Config object is too large and non-portable across restarts;
// it will be re-resolved from the live config on replay.
config: undefined as never,
skillsSnapshot: undefined,

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.

P2 config: undefined as never silently drops a required field

Setting config: undefined as never suppresses the TypeScript error at compile time, but the JSON round-trip will silently omit the config key entirely (since JSON.stringify drops undefined values). Any Phase 2 code that tries to re-enqueue a replayed FollowupRun and access item.run.config will get a runtime crash rather than a type error.

A safer approach is to give the persisted form an explicit type that omits the stripped fields, rather than using as never to paper over the type mismatch:

export type PersistedFollowupRun = Omit<FollowupRun, 'run'> & {
  run: Omit<FollowupRun['run'], 'config' | 'skillsSnapshot'>;
};

Then use PersistedFollowupRun[] inside PersistedQueueEntry.items so callers cannot accidentally treat replayed entries as full FollowupRun values without re-attaching a live config.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/auto-reply/reply/queue/persist.ts
Line: 43-47

Comment:
**`config: undefined as never` silently drops a required field**

Setting `config: undefined as never` suppresses the TypeScript error at compile time, but the JSON round-trip will silently omit the `config` key entirely (since `JSON.stringify` drops `undefined` values). Any Phase 2 code that tries to re-enqueue a replayed `FollowupRun` and access `item.run.config` will get a runtime crash rather than a type error.

A safer approach is to give the persisted form an explicit type that omits the stripped fields, rather than using `as never` to paper over the type mismatch:

```ts
export type PersistedFollowupRun = Omit<FollowupRun, 'run'> & {
  run: Omit<FollowupRun['run'], 'config' | 'skillsSnapshot'>;
};
```

Then use `PersistedFollowupRun[]` inside `PersistedQueueEntry.items` so callers cannot accidentally treat replayed entries as full `FollowupRun` values without re-attaching a live `config`.

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

Comment on lines +105 to +117
if (!parsed || parsed.version !== 1 || !Array.isArray(parsed.entries)) {
log.warn("pending-messages.json has unexpected format; discarding");
return null;
}

// Discard entries older than 5 minutes (messages may be stale)
const MAX_AGE_MS = 5 * 60 * 1000;
if (Date.now() - parsed.persistedAt > MAX_AGE_MS) {
log.info(
`pending-messages.json is ${Math.round((Date.now() - parsed.persistedAt) / 1000)}s old; discarding as stale`,
);
return null;
}

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.

P2 persistedAt type not validated — staleness guard can be bypassed

parsed.persistedAt is cast with as PersistedQueueFile but never validated to be an actual number. If the field is absent or is a non-numeric value (e.g., a string), Date.now() - parsed.persistedAt produces NaN, and NaN > MAX_AGE_MS evaluates to false, silently skipping the staleness check and potentially replaying arbitrarily old messages.

Consider adding a type guard alongside the existing structural check:

Suggested change
if (!parsed || parsed.version !== 1 || !Array.isArray(parsed.entries)) {
log.warn("pending-messages.json has unexpected format; discarding");
return null;
}
// Discard entries older than 5 minutes (messages may be stale)
const MAX_AGE_MS = 5 * 60 * 1000;
if (Date.now() - parsed.persistedAt > MAX_AGE_MS) {
log.info(
`pending-messages.json is ${Math.round((Date.now() - parsed.persistedAt) / 1000)}s old; discarding as stale`,
);
return null;
}
if (!parsed || parsed.version !== 1 || !Array.isArray(parsed.entries) || typeof parsed.persistedAt !== "number") {
log.warn("pending-messages.json has unexpected format; discarding");
return null;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/auto-reply/reply/queue/persist.ts
Line: 105-117

Comment:
**`persistedAt` type not validated — staleness guard can be bypassed**

`parsed.persistedAt` is cast with `as PersistedQueueFile` but never validated to be an actual number. If the field is absent or is a non-numeric value (e.g., a string), `Date.now() - parsed.persistedAt` produces `NaN`, and `NaN > MAX_AGE_MS` evaluates to `false`, silently skipping the staleness check and potentially replaying arbitrarily old messages.

Consider adding a type guard alongside the existing structural check:

```suggestion
  if (!parsed || parsed.version !== 1 || !Array.isArray(parsed.entries) || typeof parsed.persistedAt !== "number") {
    log.warn("pending-messages.json has unexpected format; discarding");
    return null;
  }
```

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
Contributor

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: c979792f40

ℹ️ 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".

// so queued messages survive restart and can be replayed on next startup.
// This is best-effort — if persistence fails, messages are still lost
// (same as today) but now we at least attempt to save them.
await persistFollowupQueues(FOLLOWUP_QUEUES).catch(() => {});

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.

P1 Badge Clear queued followups after writing restart snapshot

After persisting FOLLOWUP_QUEUES, shutdown leaves the in-memory map intact. In the in-process restart path (src/cli/gateway-cli/run-loop.ts falls back to restart without process exit), those same queue items remain live and can still drain, while startup also replays the just-written snapshot from disk, so the same pending messages can be processed/reported twice. Clear the followup queue state after a successful persist to avoid duplicate recovery behavior on in-process restarts.

Useful? React with 👍 / 👎.

Comment on lines +111 to +113
const MAX_AGE_MS = 5 * 60 * 1000;
if (Date.now() - parsed.persistedAt > MAX_AGE_MS) {
log.info(

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.

P2 Badge Remove hard 5-minute expiry for persisted pending messages

The replay loader unconditionally discards persisted queues older than 5 minutes, which reintroduces message loss whenever restart downtime exceeds that window (for example, manual downtime, slow reboot, or repeated failed restarts). Since this feature is intended to preserve queued messages across restarts, this fixed TTL makes recovery unreliable and should be removed or made configurable with a much safer default.

Useful? React with 👍 / 👎.

- test/restart-persistence.test.ts: 3 integration tests that exercise
  the full persist → consume → replay pipeline with realistic multi-session
  multi-channel scenarios, staleness guard, and corruption handling

- test/prove-restart-persistence.sh: standalone bash/node script that
  proves the mechanism end-to-end against the compiled build, with
  human-readable step-by-step output

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e2324f0d2f

ℹ️ 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 +32 to +33
const text = item.prompt.length > 500 ? `${item.prompt.slice(0, 500)}…` : item.prompt;
return `${i + 1}. [${channel}] from ${sender}: ${text}`;

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.

P1 Badge Keep replayed message text out of trusted system events

This code places item.prompt (which originates from user messages in queued followups) directly into a system event, and those events are later emitted as trusted System: lines on the next turn. After a restart, a crafted queued message can therefore be elevated from untrusted user input into system-priority instructions, bypassing the normal System (untrusted) protection path and potentially changing model behavior; replay text should be escaped/quoted as untrusted content instead of injected as trusted system context.

Useful? React with 👍 / 👎.

@clawsweeper

clawsweeper Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge. Reviewed July 9, 2026, 8:24 AM ET / 12:24 UTC.

Summary
This PR adds JSON sidecar persistence for non-empty followup queues during gateway shutdown and replays saved messages as startup system events.

PR surface: Source +200, Tests +819. Total +1019 across 8 files.

Reproducibility: yes. source-level: current main keeps accepted followup queues in a process-global map and has no durable followup replay path, so a full process restart can drop queued items; no destructive live channel proof was run in this read-only review.

Review metrics: 1 noteworthy metric.

  • Runtime queue state sidecar: 1 JSON sidecar added, 0 migrations. The PR persists OpenClaw-owned followup queue state outside the SQLite state model, which is a maintainer-visible storage decision.

Stored data model
Persistent data-model change detected: serialized state: src/gateway/server-pending-messages.test.ts, serialized state: test/prove-restart-persistence.sh. Confirm migration or upgrade compatibility proof before merge.

Merge readiness
Overall: 🧂 unranked krab
Proof: 🧂 unranked krab
Patch quality: 🧂 unranked krab
Result: blocked until real behavior proof is added.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • Rebase onto the current startup surface.
  • Replace or explicitly approve the JSON sidecar storage model, preferably with SQLite-backed recovery.
  • Attach redacted real restart and channel delivery proof showing exactly-once recovery.

Proof guidance:

  • [P1] Needs real behavior proof before merge: The PR body lists unit tests and a synthetic proof script, but it does not include redacted after-fix output from a real gateway restart plus channel delivery path; contributor should provide logs, live output, or video with private details redacted. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Mantis proof suggestion
A native Telegram Desktop proof would materially show the changed restart recovery behavior and exactly-once delivery path. A maintainer can ask Mantis to capture proof by posting this exact PR comment:

@openclaw-mantis telegram desktop proof: queue a second Telegram message while the first turn is active, restart the gateway, and show exactly-once delivery after recovery.

Risk before merge

  • [P1] The branch is dirty/conflicting and patches a startup file that no longer exists on current main.
  • [P1] The PR adds OpenClaw-owned runtime queue state as a JSON sidecar instead of using the SQLite state model and doctor/migration boundary.
  • [P1] Replayed user-originated prompt text is injected into trusted System: prompt lines rather than normal untrusted message handling.
  • [P2] In-process restart fallback can preserve the live FOLLOWUP_QUEUES map while startup also replays the disk snapshot, creating duplicate recovery risk.
  • [P1] The five-minute expiry can still silently drop messages after slow updates, downtime, or repeated restart failures.
  • [P1] The PR lacks redacted real gateway restart plus channel-delivery proof, and the newer SQLite candidate is not merged or clean.

Maintainer options:

  1. Rebuild as SQLite-backed recovery (recommended)
    Move durable followup queue state into shared SQLite with migration or doctor handling, current startup integration, and exactly-once replay proof before merge.
  2. Accept a documented sidecar exception
    Maintainers may intentionally land a JSON sidecar only after approving the state-storage exception and fixing replay trust and duplicate-recovery behavior.
  3. Pause this stale branch
    Keep this PR as source context until a merged successor exists, then close it as superseded if the remaining useful work is covered.

Next step before merge

  • [P1] Manual review is needed because the remaining action is an architectural storage/replay decision with overlapping open PRs, security-boundary concerns, a dirty branch, and real proof requirements rather than a narrow automated repair.

Maintainer decision needed

  • Question: Should OpenClaw advance restart followup recovery through a SQLite-backed successor and treat this JSON-sidecar branch as source context, or intentionally sponsor this PR's sidecar and system-event replay approach?
  • Rationale: The remaining blocker is a storage ownership and prompt-boundary decision that automation cannot safely settle, especially with a newer unmerged SQLite candidate and an obsolete startup surface.
  • Likely owner: steipete — Peter has the strongest combined history on queue lifecycle, system-event authority, and gateway startup surfaces involved in this storage/replay decision.
  • Options:
    • Advance SQLite recovery (recommended): Use the newer SQLite-backed direction or a rebased successor after conflicts, context persistence, and exact-head transport proof are resolved.
    • Sponsor this sidecar branch: Maintainers can explicitly accept a JSON sidecar exception plus the replay semantics after requiring fixes for duplicate replay, staleness, and trust-boundary handling.
    • Pause behind broader queue API work: Wait for the broader followup/runtime API boundary if durable restart recovery should be solved through a new owner contract first.

Security
Needs attention: The diff raises a concrete trust-boundary issue by replaying queued user text through trusted system-event prompt lines.

Review findings

  • [P1] Rebase the replay hook onto the current startup surface — src/gateway/server-startup.ts:193-197
  • [P1] Store durable queue recovery in SQLite — src/auto-reply/reply/queue/persist.ts:67
  • [P1] Keep replayed user text out of trusted system events — src/gateway/server-pending-messages.ts:32-33
Review details

Best possible solution:

Land or sponsor a rebased SQLite/doctor-backed followup recovery path that restores via normal or explicitly untrusted replay semantics, proves exactly-once delivery in a real channel restart, and leaves the canonical issue open until merged.

Do we have a high-confidence way to reproduce the issue?

Yes, source-level: current main keeps accepted followup queues in a process-global map and has no durable followup replay path, so a full process restart can drop queued items; no destructive live channel proof was run in this read-only review.

Is this the best way to solve the issue?

No: persisting queued followups is the right bug boundary, but this branch uses a JSON sidecar, trusted system-event replay, an obsolete startup hook, duplicate-prone in-process restart handling, and a fixed TTL.

Full review comments:

  • [P1] Rebase the replay hook onto the current startup surface — src/gateway/server-startup.ts:193-197
    This PR schedules replay in src/gateway/server-startup.ts, but current main has moved post-ready sidecar startup to src/gateway/server-startup-post-attach.ts. Rebase the integration onto the current startup path before this can be reviewed as a real merge result.
    Confidence: 0.94
  • [P1] Store durable queue recovery in SQLite — src/auto-reply/reply/queue/persist.ts:67
    This writes OpenClaw-owned runtime queue state to state/pending-messages.json. Repository policy keeps runtime queues and caches in SQLite-backed stores, with sidecars handled by doctor or migration code, so this creates a parallel steady-state source of truth.
    Confidence: 0.92
  • [P1] Keep replayed user text out of trusted system events — src/gateway/server-pending-messages.ts:32-33
    item.prompt comes from channel/user messages, but this path embeds it in a recovery summary and queues that summary as a system event. Current main renders system events as trusted System: prompt lines, so crafted queued text can be elevated after restart instead of replayed through normal untrusted message handling.
    Confidence: 0.9
  • [P1] Prevent duplicate replay on in-process restarts — src/gateway/server-close.ts:132
    After persisting FOLLOWUP_QUEUES, the shutdown path leaves the live global queue intact. Current in-process restarts preserve process globals while startup can also replay the disk snapshot, so the same followup can be reported twice.
    Confidence: 0.85
  • [P2] Do not drop persisted recovery after five minutes — src/auto-reply/reply/queue/persist.ts:112-116
    The loader drops all persisted queue entries older than five minutes, which means a slow update, manual downtime, or repeated restart failure can still silently lose the messages this PR is meant to preserve. Durable recovery should restore until consumed or use an explicit visible product policy.
    Confidence: 0.8

Overall correctness: patch is incorrect
Overall confidence: 0.91

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against f786efddcd88.

Label changes

Label justifications:

  • P1: The PR targets active gateway/channel workflows where accepted user messages can be lost, duplicated, or replayed with the wrong trust boundary across restart.
  • merge-risk: 🚨 compatibility: The diff adds a new persistent JSON state file and patches an outdated startup surface, affecting upgrades and the canonical runtime-state model.
  • merge-risk: 🚨 message-delivery: The diff changes whether queued channel messages are dropped, replayed, duplicated, or routed after gateway restart.
  • merge-risk: 🚨 security-boundary: The replay path can place user-controlled prompt text into trusted system-event prompt lines after restart.
  • rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🧂 unranked krab and patch quality is 🧂 unranked krab.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs real behavior proof before merge: The PR body lists unit tests and a synthetic proof script, but it does not include redacted after-fix output from a real gateway restart plus channel delivery path; contributor should provide logs, live output, or video with private details redacted. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
  • mantis: telegram-visible-proof: Mantis should capture Telegram visible proof. The PR changes user-visible Telegram followup delivery after gateway restart, which can be demonstrated with a short Telegram Desktop restart-recovery recording.
Evidence reviewed

PR surface:

Source +200, Tests +819. Total +1019 across 8 files.

View PR surface stats
Area Files Added Removed Net
Source 4 200 0 +200
Tests 4 819 0 +819
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 8 1019 0 +1019

Security concerns:

  • [high] User prompt text enters trusted system events — src/gateway/server-pending-messages.ts:32
    The replay code builds a system-event summary from persisted item.prompt values and current main formats system events as trusted System: prompt lines, so user-controlled queued text can gain higher authority after restart.
    Confidence: 0.9

What I checked:

  • Repository policy applied: Root policy requires full review beyond the diff and says runtime state, queues, caches, and sidecars should use SQLite-backed stores rather than steady-state JSON sidecars. (AGENTS.md:29, f786efddcd88)
  • SQLite-first storage rule: Runtime state migrations are database-first, runtime reads/writes canonical stores only, and JSON/TXT sidecars should not be added for OpenClaw-owned runtime queues. (AGENTS.md:76, f786efddcd88)
  • PR head and mergeability: this PR is open at head e2324f0, has +1019/-0 across 8 files, and GitHub reports mergeable=CONFLICTING, mergeStateStatus=DIRTY. (e2324f0d2f87)
  • Obsolete startup surface: The PR patches src/gateway/server-startup.ts, but current origin/main no longer has that file; post-ready startup sidecars now live in src/gateway/server-startup-post-attach.ts. (src/gateway/server-startup-post-attach.ts:666, f786efddcd88)
  • Current startup sidecar owner: Current main starts post-ready sidecars through startGatewaySidecars and schedules restart sentinel work from that path, so the PR's replay hook is not on the current startup surface. (src/gateway/server-startup-post-attach.ts:856, f786efddcd88)
  • Current followup queue is process-local: Current main keeps FOLLOWUP_QUEUES in a process-global map, which makes the restart-loss issue source-reproducible but also means in-process restart fallback can preserve live queue state. (src/auto-reply/reply/queue/state.ts:49, f786efddcd88)

Likely related people:

  • steipete: Peter Steinberger has repeated history in the followup queue, gateway startup, restart flow, and system-event surfaces involved in the storage and replay design decision. (role: likely follow-up owner; confidence: high; commits: d116bcfb144a, c397a02c9ad5, 71c31266a1db; files: src/auto-reply/reply/queue/drain.ts, src/gateway/server-startup-post-attach.ts, src/infra/system-events.ts)
  • vincentkoc: Vincent Koc appears in current-main blame and recent history for process-global followup queue state, singleton queue behavior, and startup/restart-adjacent runtime code. (role: recent area contributor; confidence: high; commits: 0ac8933721f7, 4ca84acf240d, f630e8d44037; files: src/auto-reply/reply/queue/state.ts, src/auto-reply/reply/queue/enqueue.ts, src/gateway/server-startup-post-attach.ts)
  • jacobtomlinson: Jacob Tomlinson's gateway trust-boundary hardening is directly adjacent to the PR's system-event replay concern. (role: adjacent owner; confidence: medium; commits: a77928b1087e; files: src/infra/system-events.ts, src/auto-reply/reply/session-system-events.ts)
  • VACInc: VACInc authored restart sentinel wake/thread-routing work that overlaps the gateway restart recovery and routing boundary. (role: adjacent restart recovery contributor; confidence: medium; commits: 1c9f62fad3d0; files: src/gateway/server-restart-sentinel.ts, src/infra/restart-sentinel.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.
Review history (5 earlier review cycles)
  • reviewed 2026-07-03T04:17:05.137Z sha e2324f0 :: needs real behavior proof before merge. :: [P1] Rebase the startup hook onto the current surface | [P1] Move durable queue state into SQLite | [P1] Keep replay text out of trusted system events | [P1] Avoid duplicate replay on in-process restart | [P2] Remove the fixed five-minute recovery drop
  • reviewed 2026-07-05T05:03:26.334Z sha e2324f0 :: needs real behavior proof before merge. :: [P1] Rebase the startup hook onto the current surface | [P1] Move durable queue state into SQLite | [P1] Keep replay text out of trusted system events | [P1] Avoid duplicate replay on in-process restart | [P2] Remove the fixed five-minute recovery drop
  • reviewed 2026-07-05T16:22:08.053Z sha e2324f0 :: needs real behavior proof before merge. :: [P1] Rebase the replay hook onto the current startup path | [P1] Store durable queue recovery in SQLite | [P1] Keep replayed user text out of trusted system events | [P1] Prevent duplicate replay on in-process restarts | [P2] Do not drop persisted recovery after five minutes
  • reviewed 2026-07-05T17:53:20.447Z sha e2324f0 :: needs real behavior proof before merge. :: [P1] Rebase the replay hook onto the current startup surface | [P1] Store durable queue recovery in SQLite | [P1] Keep replayed user text out of trusted system events | [P1] Prevent duplicate replay on in-process restarts | [P2] Do not drop persisted recovery after five minutes
  • reviewed 2026-07-09T06:11:46.675Z sha e2324f0 :: needs real behavior proof before merge. :: [P1] Rebase the replay hook onto the current startup surface | [P1] Store durable queue recovery in SQLite | [P1] Keep replayed user text out of trusted system events | [P1] Prevent duplicate replay on in-process restarts | [P2] Do not drop persisted recovery after five minutes

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. mantis: telegram-visible-proof Mantis should capture Telegram visible proof. P1 High-priority user-facing bug, regression, or broken workflow. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. labels May 19, 2026
@openclaw-barnacle openclaw-barnacle Bot added the triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. label May 19, 2026
@clawsweeper

clawsweeper Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper PR egg

🎁 Pass real behavior proof to wake the egg and unlock a hatchable treat.

Where did the egg go?
  • The egg game starts only after the PR passes the real-behavior proof check.
  • Before that, no creature or rarity is rolled. The treat waits for real proof.
  • This is still just collectible flavor: proof affects review readiness, not creature quality.

@openclaw-barnacle

Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle Bot added the stale Marked as stale due to inactivity label Jun 6, 2026
@clawsweeper clawsweeper Bot removed the rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. label Jun 6, 2026
@clawsweeper clawsweeper Bot added status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. mantis: telegram-visible-proof Mantis should capture Telegram visible proof. labels Jun 16, 2026
@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. mantis: telegram-visible-proof Mantis should capture Telegram visible proof. and removed rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. mantis: telegram-visible-proof Mantis should capture Telegram visible proof. labels Jun 16, 2026
@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. mantis: telegram-visible-proof Mantis should capture Telegram visible proof. and removed rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. mantis: telegram-visible-proof Mantis should capture Telegram visible proof. labels Jun 16, 2026
@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. mantis: telegram-visible-proof Mantis should capture Telegram visible proof. and removed rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. mantis: telegram-visible-proof Mantis should capture Telegram visible proof. labels Jun 16, 2026
@openclaw-barnacle

Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle Bot added stale Marked as stale due to inactivity triage: needs-pr-context Candidate: external PR body lacks required problem context or evidence. and removed stale Marked as stale due to inactivity labels Jul 1, 2026
@steipete

steipete commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Thanks @tomsalphaclawbot. Closing this stale implementation branch, not the restart-recovery problem; #51620 and the newer #82572 remain open. This head depends on obsolete startup behavior and a JSON sidecar design that can replay untrusted or duplicate events, retarget sessions, and lose messages. A replacement needs the current queue/session model plus restart and duplicate-delivery proof.

@steipete steipete closed this Jul 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gateway Gateway runtime mantis: telegram-visible-proof Mantis should capture Telegram visible proof. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. P1 High-priority user-facing bug, regression, or broken workflow. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: XL status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. triage: needs-pr-context Candidate: external PR body lacks required problem context or evidence. triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Gateway restart drops queued and in-flight inbound messages

2 participants