Skip to content

feat: Delivery mirroring - let agents remember what they sent#1031

Closed
TSavo wants to merge 3 commits intoopenclaw:mainfrom
TSavo:delivery-mirror
Closed

feat: Delivery mirroring - let agents remember what they sent#1031
TSavo wants to merge 3 commits intoopenclaw:mainfrom
TSavo:delivery-mirror

Conversation

@TSavo
Copy link
Contributor

@TSavo TSavo commented Jan 16, 2026

🦞 The Problem

When Clawdbot sends a message to Discord/Slack/etc., that delivery is a side-effect that never enters the session transcript. On the next turn, the agent has no memory of what it sent — unless it fetches channel history or reads a log file.

This is particularly painful for cron jobs:

  • An isolated cron run generates a digest and sends it to Discord
  • The postToMain summary is just a truncated line, not the full output
  • If you ask "what did you post this morning?", the agent genuinely doesn't know

The agent literally cannot answer "what did I just say?" without extra tooling.

💡 The Solution: Delivery Mirroring

Add a first-class messages.deliveryMirror config option that mirrors delivered messages back into the session transcript as assistant messages.

{
  "messages": {
    "deliveryMirror": {
      "enabled": true
    }
  }
}

When enabled, every successful outbound send also appends an assistant message to the session transcript with the exact text that was delivered. Future turns naturally include "what I said" in context.

🔧 Implementation

Mirroring hooks added at three layers (covers all send paths):

  1. routeReply — normal agent replies after a turn
  2. message tool — when agents use the message tool to send
  3. Gateway send API — raw gateway sends (with optional sessionKey param)

Also adds isolation.postToMainMode: "full" for cron jobs — isolated runs can now post full output (not just summary) back to the main session.

Files Changed

  • src/config/sessions/transcript.ts — new appendAssistantMessageToSessionTranscript() helper
  • src/auto-reply/reply/route-reply.ts — mirroring for normal replies
  • src/agents/tools/message-tool.ts — mirroring for message tool
  • src/gateway/server-methods/send.ts — mirroring for gateway API
  • src/cron/*postToMainMode option for isolated cron runs
  • Config types + schemas updated

✅ Testing

  • Unit tests for appendAssistantMessageToSessionTranscript (5 tests, all passing)
  • Docker build + isolated gateway test
  • End-to-end: agent sends via message tool → message appears in transcript
  • Linter passes (0 warnings, 0 errors)

🤖 AI-Assisted

This PR was built with Claude (Opus). The implementation was tested in a Docker container with an isolated config before committing. I understand what the code does and have verified it works end-to-end.


Why this matters: Agents that can't remember what they said aren't really agents — they're stateless responders. This makes Clawdbot's cron jobs, scheduled digests, and multi-turn workflows actually coherent.

…ript

Adds messages.deliveryMirror.enabled config option that mirrors delivered
messages back into the session transcript as assistant messages, so the
agent can 'remember' what it sent without fetching channel history.

Mirroring hooks added to:
- routeReply (normal agent replies)
- message tool (agent tool sends)
- gateway send API (with sessionKey param)

Also adds cron isolation.postToMainMode='full' option for isolated cron
runs to post full output (not just summary) back to main session.

Includes unit tests for appendAssistantMessageToSessionTranscript helper.
Copy link

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

Choose a reason for hiding this comment

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

💡 Codex Review

https://github.com/clawdbot/clawdbot/blob/1713455dd7c5ccd27d3892a12b1caaf658dc0e03/src/cli/cron-cli/register.cron-add.ts#L180-L184
P2 Badge Wire post-mode/max-chars into cron add payload

The CLI adds --post-mode and --post-max-chars, but the isolation object only sets postToMainPrefix before calling cron.add. As a result, postToMainMode and postToMainMaxChars are never sent to the gateway, so users who pass --post-mode=full (or change max chars) will still get the default summary behavior with no warning. This makes the new flags silently ineffective for isolated jobs.

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

TSavo added 2 commits January 16, 2026 09:45
…yload

Addresses Codex review feedback - the CLI options were defined but not
actually passed through to the isolation object when calling cron.add.
@steipete steipete self-assigned this Jan 17, 2026
@steipete
Copy link
Contributor

Thanks! Taking over. Slopus missed some stuff :)

Findings

  • Blocker: src/config/sessions/transcript.ts:45-51 appends { message }
    without type/id/parentId/timestamp. Session format requires type:"message"
    and IDs; readers filter on type === "message" (see session tests).
    Mirrored lines likely ignored → feature doesn’t work. Use
    SessionManager.appendMessage(...) or write full SessionMessageEntry with
    parentId chain.
  • High: src/agents/tools/message-tool.ts:192-214 mirrors on bestEffort sends
    even if delivery fails (send result can be undefined). This can “remember”
    messages never delivered. Gate on result.sendResult?.result (or equivalent
    success signal) before mirroring.
  • Medium: src/config/sessions/transcript.ts:22-24 uses cached session store;
    cross‑process updates can be stale, returning “unknown sessionKey” and
    skipping mirror for newly created sessions. Use
    loadSessionStore(storePath, { skipCache: true }) or updateSessionStore on
    this write path.

@steipete
Copy link
Contributor

Squashed and landed on main: 6c79964bc6c68edefc4311f05c9e50dfce814c0b. Tests: pnpm lint, pnpm build, pnpm test.

steipete added a commit that referenced this pull request Jan 17, 2026
Co-authored-by: T Savo <TSavo@users.noreply.github.com>
@steipete
Copy link
Contributor

Rebased main + re-landed squash: fdaeada3e3c72ef9b4c5a2f38cf6d31a1844f765. Tests: pnpm lint, pnpm build, pnpm test.

@steipete steipete closed this Jan 17, 2026
PatrickBauer pushed a commit to PatrickBauer/openclaw that referenced this pull request Feb 6, 2026
Heartbeat-initiated outbound messages were not being mirrored back into the
session transcript, causing context confusion when users replied to
heartbeat messages. The agent had no memory of what it sent.

This adds the 'mirror' parameter to the deliverOutboundPayloads call in
runHeartbeatOnce, matching the pattern established by openclaw#1031 for other
delivery paths (routeReply, message tool, gateway send API).

Fixes the case where:
1. Heartbeat triggers agent to send a message (e.g., "was machst du grad?")
2. User receives and replies to that message
3. Agent has no context about the original message it sent
4. Agent is confused about what the user is replying to

Now the outbound heartbeat text/media is recorded in the session transcript,
giving the agent the context it needs.
zooqueen pushed a commit to hanzoai/bot that referenced this pull request Mar 6, 2026
Co-authored-by: T Savo <TSavo@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants