* fix(mattermost): key private channels as group on outbound routing
A Mattermost private channel (server type `P`) is authoritatively chat_type
`group`, but it is addressed as the delivery target `channel:<id>` — the same
prefix as a public channel. Inbound classified it correctly as `group`, while
outbound/session reconstruction re-derived `channel` from the target string, so
one conversation was keyed under two session namespaces
(`...:mattermost:group:<id>:thread` inbound vs a phantom `...:channel:<id>:thread`
on delivery). Threaded/scheduled deliveries bound to one then failed to match the
other (fail-closed delivery, or a conversation split across two session keys).
The Mattermost outbound path could not represent `group` at all:
resolveMattermostOutboundSessionRoute only produced direct/channel, and
resolveMattermostOpaqueTarget only classified user/channel.
- session-route: key a conversation as `group` from an authoritative signal — the
resolved target kind, an explicit `group:` prefix, or the inbound
currentSessionKey peer kind — so outbound shares the inbound `group:<id>`
namespace instead of forking `channel:<id>`.
- target-resolution: classify a bare channel id by its real channel type
(P/G -> group, O -> channel), cached per id.
The wire target stays `channel:<id>` (Mattermost posts to the channel id either
way; parseMattermostTarget only accepts channel:/user:) — the group distinction
lives in the session key. Adds unit coverage for both paths.
Resolves#95646.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cron): route bound-session cron deliveries under the bound namespace
An isolated cron run executes under an ephemeral agentSessionKey that does not
carry the source conversation's namespace. resolveDirectCronDeliverySessionKey
resolved the outbound delivery route from that isolated key, so for a job bound
to a real conversation thread (e.g. the gitlab-pipeline-watch recheck bound to
agent:...:mattermost:group:<id>🧵<root>) the lossy channel:<id> target was
re-derived as `channel`, forking a phantom channel:<id> session and splitting the
private-channel thread across two namespaces (#95646).
Prefer the job's bound conversation identity as the currentSessionKey used to
resolve the route (new selectCronRouteCurrentSessionKey helper), so the existing
currentSessionKey-based namespace resolution keeps group:<id>. No channel-type
cache is introduced — which is what made the cache-based attempts brittle on cold
restart (a sibling PR documented exactly that failure mode). Falls back to the
isolated key for unbound jobs and cron-namespace bindings. Adds unit coverage.
Refs #95646.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(mattermost): key public channels as channel in directory listing
listMattermostDirectoryGroups labeled every joined channel — public `O`
and private `P` — as kind `group`. A name-resolved public channel could
then be keyed as `mattermost:group:<id>` on outbound routing, forking a
phantom group session and splitting the transcript from the inbound
`channel:<id>` one. Derive the kind from the authoritative Mattermost
channel type (`O` -> channel, `P`/`G` -> group) and add a regression
test. This closes the public-channel regression path flagged in review
for #95646 while keeping private channels keyed as `group`.
* fix(mattermost): harden private channel routing
* test: expose cron route selection through production module
* fix. scope cron session reuse to Mattermost delivery
* fix(cron): validate bound delivery peer and channel authority
* fix(cron): capture validated delivery destination peer
---------
Co-authored-by: leon <leon@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Classify pinned Grammy blocked, kicked, and deactivated recipient 403 responses as permanently unreachable. Preserve retry behavior for recoverable permissions and flood control, inspect wrapped causes, and prove real SQLite ingress drain dead-lettering without duplicate dispatch.
Fixes#112893
* fix(mac): move device auth tokens to shared SQLite state
* fix(mac): fail identity migration when source auth vanishes mid-read
Missing-file tolerance now applies only to the first observation; a disappearance after the auth file was observed fails the migration so the claimed identity survives for retry instead of committing without its credentials.
* fix(ios): validate auth scope UTF-8 encoding
* fix(ios): isolate SQLite auth profiles
* chore(ios): refresh native source inventory
* fix(mac): keep device auth in the shared token table
* test(mac): pin shared-key token cache semantics
Retain the existing guarded-fetch timeout callback while consuming native Ollama NDJSON. Prove active slow HTTP streams remain alive and genuine stalls still expire.
Reconstructed from the independently reviewed contributor fix for #94251.
Co-authored-by: Henry <henrybrewer93@icloud.com>
Beta versions (e.g. 2026.7.2-beta.5) are treated by SemVer as older than
the cutover release (2026.7.2), so migrateLegacyConfigMachineState would
re-infer plugins.bundledDiscovery = "compat" on every new CLI process.
The inferred entry always went into importConfigMachineState's 'kept'
bucket, which was then reported as a Doctor change — producing misleading
"Auto-migrated legacy state" / "Kept existing shared SQLite ..." output
on every restart.
Fix: skip the inferred compat candidate when the canonical SQLite row
already exists. A try/catch guards against temporary SQLite
unavailability.
Related to #115151
Co-authored-by: Claude <noreply@anthropic.com>
* fix(active-memory): share one recall per run across retries
Register in-flight recalls per ctx.runId so overlapping or changed-prompt
attempts join a single execution, and gate replacement recalls on settled
timeout cleanup. Entries clear on agent_end. Fixes#106957.
* fix(active-memory): evict rejected recall entries
* test(active-memory): clarify run result retention
* test(active-memory): complete recall result fixture
signalRpcRequest returns the JSON-RPC result via a bare cast, so getAttachment data reaches Buffer.from unvalidated. Node drops out-of-alphabet characters instead of throwing, so a damaged payload was silently written to disk as a corrupted attachment. Canonicalize after the existing size guard and fail with the attachment id instead.
* fix: redact imported CLI chat history
Apply transcript redaction to Claude CLI messages before history merge so locally redacted and imported copies dedupe consistently.
This also prevents secrets present only in the CLI transcript from reaching Control UI chat history.
Fixes#112930
* fix(gateway): type imported history redaction bridge
Reuse the existing runtime shutdown grace so a hanging plugin stop cannot prevent channel and MCP/LSP child-process cleanup after SIGINT.
Fixes#106714
Co-authored-by: Peter Steinberger <steipete@gmail.com>