* refactor: consolidate coercion ownership
Centralize four canonical coercion helpers, migrate exact core and plugin duplicates through narrow Plugin SDK facades, and enforce declaration and plugin-normalization ownership boundaries.
The sweep adds eight focused SDK exports while deleting more production and tooling code than it adds. User-visible behavior is unchanged except for safer equivalent object and UI parsing at existing boundaries.
* fix: guard integer option ownership
Register resolveIntegerOption with the canonical function owner and extend the declaration-guard fixture so future local duplicates fail validation.
* fix: keep integer helpers on numeric facade
Remove the unshipped duplicate string-coerce exports and route every affected plugin consumer through the existing number-runtime contract.
* fix: point numeric coercion to number runtime
Make boundary and declaration diagnostics recommend the canonical numeric facade, with failing-before coverage for both guidance paths.
Native approval delivery and resolution now stay bound to the originating or explicitly targeted channel account. Unbound requests fail closed across multiple eligible accounts; trusted reviewer-less SDK callers remain compatible.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* fix(imessage): harden remote Mac transport
Route SSH-backed iMessage actions through JSON-RPC, preserve remote database paths, and stage outbound files on the Messages Mac with bounded cleanup. Keep local action semantics intact while failing closed on ambiguous wrappers and surfacing the remaining imsg v0.13.4 limits.
* fix(imessage): remove test-only exports
* fix(imessage): text echo matching honors message-id conflicts; own sends stop feeding the loop limiter
Two silent-drop paths in the iMessage monitor:
1. The persisted 12h echo cache matched inbound user text against old
outbound text even when the GUIDs conflicted — a user sending 'ok' within
12h of the agent sending 'ok' was dropped as an echo. The text branch now
applies the same hasConflictingMessageIds guard the media branch has;
genuine reconnect echoes still match by id or id-less entries.
2. Every from-me row fed the echo loop rate limiter, so a normal outbound
burst (agent replies, multi-chunk sends, operator phone traffic) could trip
the limiter and silently suppress the next legitimate inbound message.
'from me' is no longer counted as a loop signal, and a tripped limiter now
logs a default-level warning once per conversation instead of verbose-only.
* fix(imessage): preserve delayed echo guards
* fix(imessage): rewind the recovery cursor when chat.db is replaced at the same path
A restored, rebuilt, or symlink-retargeted chat.db keeps the configured
pathname, so it keeps the same recovery-cursor identity while its rowid
sequence restarts far below the persisted high-water. Startup then seeded
since_rowid with the stale value and imsg, which emits only rows above it,
suppressed every inbound message in the replacement database, including
messages arriving after the restart.
loadIMessageRecoveryCursor now takes the startup watermark and reconciles
the stored cursor against it. A cursor above the database's current
MAX(ROWID) cannot belong to the file now at that path, so the persisted
high-water rewinds to the new watermark and the monitor tails the
replacement database instead of skipping it.
* fix(imessage): rewind the recovery cursor for an empty rebuilt chat.db
A successful MAX(ROWID) on an empty message table yields null, the same value the watermark read returns for an unreadable database, so an empty rebuilt chat.db kept the stale high-water and suppressed its first rows. Return 0 for a readable empty table and keep null for an unavailable one.
* fix(imessage): preserve first row during empty database startup
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* 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>