* fix(imessage): stop the outbound sanitizer deleting fenced role keys
sanitizeOutboundText strips standalone `user:`/`system:`/`assistant:`
lines (leaked turn boundaries), `#+#` separators and `assistant to=`
markers, but the three regexes had no code-fence awareness. A bare YAML
mapping key like `user:` on its own line inside a ```yaml block matched
ROLE_TURN_MARKER_RE and was silently deleted before iMessage delivery,
reparenting its children under the wrong node — still-valid YAML, wrong
meaning, and nothing signalling a line was removed.
Skip matches whose offset falls inside a markdown code region for all
three patterns, reusing findCodeRegions/isInsideCode already applied by
the sibling reflection-guard on this channel. Regions are recomputed per
pass because each strip shifts later offsets. Leaked markers in prose are
still stripped.
Refs: #116942
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(imessage): cover code-aware marker stripping
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(imessage): bold approval prompt labels in poll mode
#113193 added bold headers and labels to the approval reaction prompt, but
iMessage only shows that copy when tapbacks own the controls. On any
poll-capable bridge the details message is built from `manualFallbackPayload`,
the legacy unstyled builder, so every label (`Title:`, `Tool:`, `Host:`,
`CWD:`, `Full id:`, ...) reaches Messages as flat text. Native polls are the
default on a bridge-v2 host, so in practice #85954 still reproduced after it
was closed.
Add `nativeControlsPayload` to `ApprovalReactionPendingContent`: the same rich
copy as `reactionPayload` minus the tapback hint, for channels whose native
controls already own the decision surface. iMessage poll mode now renders it,
so both control paths deliver identical styled copy.
`imsg poll send --question` has no attributed-body channel, so the poll
question keeps the marker-free rendering of that same text; otherwise the
balloon would show literal asterisks.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Byq7UrC7ZpMHxoFXcddsa
* refactor(plugin-sdk): carry native-controls prompt copy as text
The iMessage poll path reads exactly one thing from the native-controls
payload: its text. Replace the ReplyPayload field with nativeControlsText so
buildApprovalReactionPendingContent stops running the metadata/session-key
builder for fields no caller reads, and the '?? ""' sentinel goes away.
Relative to main the SDK change stays additive: neither field exists in any
release; nativeControlsPayload only ever existed on this branch.
* refactor(plugin-sdk): expose native-controls prompt text as a builder, not a type field
ClawSweeper flagged that requiring a new member on the shipped
ApprovalReactionPendingContent type is source-incompatible for external
producers that hand-construct it. The hint-free copy does not need to ride
the type at all: export buildApprovalNativeControlsPromptText and let the
iMessage handler call it at payload-build time.
ApprovalReactionPendingContent is now byte-identical to the shipped shape;
the SDK change is a single additive function export. Signal/WhatsApp test
fixtures revert to their original form.
---------
Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Track full dispatch completion separately for error handling and shutdown drain while allowing same-session follow-ups to steer active runs. Fixes#113180.
Co-authored-by: Taksh <takshkothari09@gmail.com>
* fix(imessage): detect remote hosts when HOME is blank
* fix(imessage): resolve blank HOME in SSH wrappers (#111715)
Resolve explicitly blank or whitespace HOME values from the operating-system account, preserve configured and unset home contracts, and share the canonical resolver with the iMessage monitor and recovery cursor. Add unmocked subprocess coverage for account-home resolution and working-directory tilde shadows.
Co-authored-by: LZY3538 <liu.zhenye@xydigit.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* test: consolidate OpenClaw test state fixtures
* test(plugin-sdk): expose isolated test state
Promote the isolated OpenClaw test-state lifecycle through a narrow published Plugin SDK subpath so extension tests no longer import private core helpers. This intentional SDK surface addition is maintainer-approved.
* test: use SDK test-state seam in extensions
Route bundled extension suites through the focused repo-local Plugin SDK test-state entrypoint and remove the Codex projector harness exports made stale by fixture consolidation. Keep the seam out of production builds and published package artifacts while auditing its real consumers in the full-tree deadcode scan.
* test(plugins): map test-state in package boundaries
* feat(approvals): emit bold headers and labels in approval prompts
Approval prompts carried plain-text labels, so iMessage showed no formatting
even though its send path now translates markdown into attributed-body ranges
(the markdown-core profile refactor, #113002). Emit bold on the headers and
field labels so channels that render markdown show formatted approval text:
iMessage into native ranges, other markdown channels into their native bold,
and channels that downgrade drop the markers cleanly.
Closes#85954.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7d86Ww4vJwxJwY4z1AVx6
* test(approvals): update prompt-text assertions for bold labels
* feat(approvals): bold the auto-review rationale in reaction prompts
The rationale is the reason for the interruption, so it should stand out.
Generated text, but the reaction-runtime renderers parse to an IR that
tolerates stray markers, so a rationale containing a lone marker degrades
gracefully rather than breaking the emphasis span.
* fix(approvals): preserve reaction binding and Signal rendering for bold prompts
Codex + local ClawSweeper caught that bolding the prompt headers/labels broke
downstream consumers of the visible approval text:
- Reaction/tapback binding on iMessage, Signal, and WhatsApp anchors on the
plain `Exec approval required` / `ID:` format. Strip `**` markers in each
channel's binding parser before matching, so binding still correlates the
delivered prompt. Adds an iMessage bold-format binding regression test.
- Signal sent the approval payload with textMode "plain", so the markers would
reach users literally. Switch Signal's approval sends to markdown mode;
markdownToSignalText renders the headers as native bold. WhatsApp already
renders markdown by default; iMessage renders via extractMarkdownFormatRuns.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7d86Ww4vJwxJwY4z1AVx6
* style(approvals): oxfmt the touched approval files
---------
Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(media): delete internal MsgContext.Media* parallel fields for fact-only runtime
Internal runtime now carries a fact-only RuntimeMsgContext: the legacy
MediaPath/MediaUrl/MediaType parallel fields (and plural/transcribed
variants), their alignment and default helpers, and bundled legacy payload
builders are removed from internal paths. Staging, hooks, Gateway, media
understanding, and the Telegram, QQ, Signal, Slack, iMessage, Discord, QA,
and Zalo plugins consume ordered MediaFact[] directly.
Retained boundaries per the program audit: public Plugin SDK MsgContext,
persisted transcript Media* rows, and documented template variables keep
working via projectMediaFacts at the five declared seams (fact owner,
channel payload, inbound-event, transcript persistence, SDK adapter).
Review-round hardening, each with regressions:
- hasStagedMediaProjection requires every path-bearing fact staged (was
any-satisfied; mixed contexts skipped staging unstaged facts).
- finalizeInboundContext returns Omit<T, LegacyMediaContextKey> so deleted
legacy fields leave the type when they leave the object.
- QQ image facts carry explicit kind: "image" (remote URLs have no MIME).
- resolveStagedMediaFacts adopts staged legacy paths positionally while
retaining canonical fact metadata and cardinality (staged projections
previously replaced canonical facts wholesale).
1,615 tests across 47 files; goldens untouched; delegated check:changed
green (run 30009882062).
* test(media): migrate remaining legacy Media* consumer suites to structured facts
Exact-head CI caught suites outside the curated affected set that still
asserted internal legacy fields. All were stale internal-field assertions
migrated to facts, except QA Channel, which had a real migration gap: it
still constructed the bundled legacy payload internally and now normalizes
saved attachments with toInboundMediaFacts and passes the declared media
parameter directly. Exhaustive rg sweep over every test referencing the
deleted fields (133 files, each run individually) is green.
* feat(channels): add channel-owned setup contracts
* test(channels): align legacy setup fixtures
* chore(channels): regenerate config and SDK baselines after rebase
* fix(update): run fresh doctor after current-process core changes
* fix(channels): align add pre-scan with execution precedence
* style(cli): format channels-cli test additions
* fix(channels): restore option-before-positional channel resolution via metadata arity scan
* fix(channels): keep help flags out of metadata arity escalation
* test(update): mock fresh post-update doctor in current-process suites
* style: format review fixes and correct entrypoint mock type
* fix(channels): register only modern contract options for dual-publishing plugins
* test(update): align downgrade suites with fresh-doctor child invocation
* docs(channels): record empty-contract and input-forwarding invariants
* fix(line): keep the shipped --token switch as a channel access token alias
* fix(signal): stop treating exact cross-family loopback endpoints as bind-aligned
* chore(config): regenerate docs config baselines after second rebase
* style: format rebased channels add tests
* fix(channels): enforce field-key and flag-name agreement in setup contracts
* fix(signal): detect container endpoints for bare --http-url setup
* fix(signal): ignore unconfigured accounts in transport collision checks
* fix(channels): validate negated setup flags in contract and normalizer
* fix(signal): preserve existing transport kind when setup detection is unreachable
* style(signal): use direct boolean check in collision guard
* style(signal): type test config literals
* docs(update): record two-read design of fresh-doctor validation gate
* fix(channels): satisfy post-rebase architecture gates
* docs: refresh channel setup map
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(channels): restore transcript context after restart
Merge bounded active-branch session transcript turns at the shared prepared-turn seam so message channels retain assistant replies after restart or history eviction. Migrate Telegram's one-off merge while preserving exact projection and legacy dedupe behavior.\n\nCloses #112520. Slack case reported by Joe Tam (@joetam) in #102594.
* style(channels): avoid spread in transcript mapping
* refactor(telegram): drop obsolete transcript exports
* refactor(channels)!: shrink ChannelSetupInput to a generic envelope with a deprecated compatibility tier
* fix(channels): keep ChannelSetupInput structurally assignable without an index signature
* docs: regenerate docs map