* refactor(prompt): plain inbound context labels with a provenance marker
Replaces trust-worded inbound context labels ("(untrusted metadata)",
"(untrusted, for context)") with plain labels plus a fixed provenance
marker suffix appended to every OpenClaw-injected context header.
Detection keys on the marker, not label text, so strippers stay correct
across UI, TUI, replay, /trace segmentation, memory recall, and the Swift
chat preprocessor. Drops sanitizeInboundSystemTags in favor of the marker
boundary plus trusted system-prompt narration.
Renames the untrusted-named plugin SDK context identifiers to
channel-provenance names, keeping deprecated aliases registered for
removal after 2026-09-08.
Adds `openclaw doctor --fix` migrations that rewrite legacy inbound
labels in stored SQLite transcripts and purge legacy envelope-
contaminated LanceDB recall rows.
* fix(ci): resolve gate failures for plain inbound context labels
- doctor sqlite readers: open read-only connections via openNodeSqliteDatabase
so the Kysely connection-boundary guardrail holds; unexport the now-internal
transcript snapshot type (Knip unused-export gate).
- compat registry: split the record table into registry-records.ts and
plugin-sdk-subpath-records.ts. The new compat record pushed registry.ts past
the 700-line oxlint cap; suppressions are disallowed, so follow the existing
sibling record-module pattern. Public exports and PluginCompatCode literals
unchanged.
- acp-runtime test: assert current finalization behavior (newline normalization
only). The bracket de-fang and System: rewrite it expected were removed with
sanitizeInboundSystemTags; forged system lines are neutralized at the
system-event queue, the single chokepoint feeding the System:-per-line render.
- regenerate docs_map and the plugin SDK API baseline manifest.
* fix(prompt): harden inbound context label migration and drop in-band sanitizer
Review follow-ups on the plain-label + provenance-marker change:
- Remove src/security/system-tags.ts. Rewriting inbound text to neutralize
look-alike `System:`/`[System]` markers corrupted legitimate user text and is
not a real injection boundary; role separation plus external-content wrapping
is. Explicit product decision, recorded at the system-event queue.
- Narrow the LanceDB legacy-row purge so it cannot delete benign memories. It
now requires a complete known legacy sentinel line, a legacy label followed by
a fenced JSON body, or the complete legacy external-content header. The prior
predicates matched ordinary prose such as `Notes (untrusted metadata):`, and
deletion is irreversible.
- Make explicit-empty canonical ChannelStructuredContext win over the deprecated
alias via a present/absent result instead of collapsing `[]` to undefined.
- Keep `\r?` in the active-memory doctor rule. It is the only rule spanning the
header's line break, migrated assistant rows skip newline normalization, and
without it the marked-header replace wins and the body strips to empty. Added
a CRLF regression test.
- Fix stale comments that described removed behavior, and cover the Swift
prose-block strip path.
Claude-Session: https://claude.ai/code/session_01WNzsPddQmxy9Y7jKD4wAxH
* 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(discord): fall back to raw bot mentions
* fix(discord): gate raw mention fallback on hydration failure
* fix(discord): reject non-pinging raw mentions
* fix(discord): model mention hydration authority
Co-authored-by: luyifan <al3060388206@gmail.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
normalizeLabel truncated external member display names (nick / global_name /
username) with a raw .slice(0, 100). When the cut landed on a UTF-16 surrogate
pair, the label kept a lone high surrogate that corrupted the JSON-quoted
roster line fed into the agent's voice-roster prompt. Use truncateUtf16Safe
so the boundary backs off the split pair, matching the same surrogate-safe
truncation already applied to other discord truncation sites.
Co-authored-by: ben.li <li.yang6@xydigit.com>
Co-authored-by: 刘伟勤0668000710 <liu.weiqin@xydigit.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(media): ingress and preflight consumers ride ordered media facts
Media-facts program PR 2: channel context retains ordered MediaFact[]
through finalization; inbound gates, hook mapping, current-turn
normalization, and all five channel families' audio preflights consume
facts. Four review cycles hardened the canonical/legacy merge: canonical
facts are authoritative (legacy fills gaps only), one shared staging
predicate covers MediaStaged and MediaWorkspaceDir, kind inference runs
after fallback MIME fills, legacy counts use max array cardinality, and
alignment-only empty slots no longer count as media presence — all
pinned by a 54-cell canonical×legacy×type merge-matrix test.
* fix(media): hide internal fact helper
* refactor(media): consolidate parallel media-kind unions onto canonical MediaKind
One canonical MediaKind union (media-core constants) replaces ~40
duplicate/parallel kind declarations across core and channel plugins;
channel-specific narrower contracts derive via Extract/Exclude. Also
fixes a review-caught fallback bug where a stored "unknown" reply-chain
kind preempted MIME inference and relabeled images as documents.
* refactor(ui): derive attachment kinds from MediaKind
* fix(telegram): drop type-dead unknown guard in reply-context kind fallback
* style(telegram): format media kind fallback
hasInboundAudio previously accepted <media:audio> placeholder bodies,
[Audio transcript headers, and filename sniffing alongside MediaTypes.
It now consumes only the structured facts channels already carry
(MediaType/MediaTypes, kind audio or audio/*), removing the string
reverse-engineering ahead of the placeholder-elimination program.
Discord — the one channel with a coverage hole — now carries its native
audio classification (MIME, voice fields, constrained filename fallback)
as structured kind: "audio", with definitive fetched/declared MIME
precedence so generic container types cannot erase declared audio and
filename hints cannot override definitive non-audio MIME.