* fix: honor inherited model pins over channel defaults
Child sessions now keep parent model pins consistently across reply, status, harness prediction, and agentCommand.
* test: align agent command model selection fixtures
Keep concurrent override and threaded parent-selection coverage aligned with canonical model normalization and runtime snapshot access.
* feat(agents): default to eager delegation in the main session
The delegation prompt section now defaults to "prefer" in each agent's
canonical main session and "suggest" elsewhere; explicit config wins in
both directions. The section is rewritten to be token-leaner and now
covers the hidden-vs-visible spawn distinction (hidden subagents are
invisible and auto-archived; deliverable-bearing work spawns
visible=true and replies with the link) plus the run-end notification
contract. The Messaging spawn-mechanics line is suppressed when the
Delegation section renders, and the stable Tooling visible:true hint is
aligned with the new guidance.
* perf(sessions): coalesce session-state wake bursts
Watched-session change notices woke the watcher's main session with the
generic 250ms heartbeat coalesce, so a burst of changes across several
watched sessions produced one wake per change. Wakes now coalesce for
20s; notices are already queued and deduped, so none are lost.
* test(agents): regenerate prompt snapshots after rebase
* test(sessions,agents): update sibling assertions for coalesced wakes and visible guidance
session-state-events tests advance timers past the new 20s wake
coalesce window; the sessions_spawn schema test tracks the updated
visible description.
Keep ambient main-session group notices working when direct messages use an isolated dmScope, while preserving explicit session watches and routed group topology.
* fix(ui): preserve steering stream order
Record exact steering provenance and keep cumulative assistant output on its causal side of persisted user turns across live streaming, terminal events, tool boundaries, and history reloads.
* chore(ci): refresh post-rebase gates
Tighten the inherited environment-variable budget and remove a stale test import exposed by the rebased lint gate.
* fix(ui): preserve terminal steer stream segment
Assert the causal stream rollover when a steer lands and move persisted split-layout normalization off the startup path to keep the Control UI bundle within budget.
* refactor(ui): extract split layout types
Keep the persisted split-layout normalizer off the interactive module cycle while preserving the Control UI startup bundle reduction.
* fix(gateway): resolve steering provenance at injection
* fix(gateway): confirm steering provenance after persistence
* refactor(ui): move live tool filtering to identity owner
* fix(ui): preserve queued user stream ceiling
* feat(sessions): stamp agent identity on spawned sessions and return spawn receipts
Agent-spawned sessions recorded the requesting session key as createdActor.id,
so the Control UI creator chip rendered an opaque key. Spawn producers now stamp
the canonical requester agent id; parent-authority validation moves to a new
trusted requesterSessionKey field. projectSessionActor enriches agent actors
with configured identity name/avatar at read time, and visible sessions_spawn
returns a sessionUrl + owner receipt with URL-first acknowledgement guidance.
* feat(sessions): assignable session ownership with owner facet and menus
GitHub-assignee-style ownership: sessions get a mutable owner (defaulting to
the immutable createdActor) stored in additive bare-nullable SQLite columns
with first-use lazy ensure. New operator.write sessions.assignOwner validates
targets, requires an identified caller, authorizes by session visibility, and
records assignedBy/assignedAt inside the write transaction. The sessions agent
tool gains assign_owner; the Control UI adds Assign-to-me/Assign-to menus in
sidebar rows and chat headers, renders the effective owner chip, and the
creator facet/filter now keys on effective owner. Sharing authority stays
anchored on createdActor.
* feat(sessions): record session participants and stack them in the owner chip
Records every distinct external prompter (human profile/channel sender, or a
requesting agent) per session in an additive session_participants table at the
turn-admission boundary — best-effort, deferred, never blocking the turn; the
session's own agent and viewers are never recorded, capped at 32 per session.
The session row projects a bounded participants list (owner excluded) plus a
total count with the same actor enrichment as owner/createdActor. The sidebar
chip becomes a pair-stack when others have prompted (owner front, one peeking
participant or +N behind), the chat header shows the full facepile, and an
authenticated involvingMe list filter adds an Involving-me sidebar predicate.
Participant projection is excluded from logical-session CAS equality so display
history never invalidates session writes.
* fix(sessions): identify built-in agent tool callers for owner assignment
The sessions tool's assign_owner dispatched through the in-process synthetic
client, which carries neither a signed agent-runtime identity nor a human
profile, so agent-initiated reassignment always failed with FORBIDDEN. The
tool now captures its trusted requester agent identity and carries it across
in-process dispatch as internal client state (never wire params); the handler
derives assignedBy as signed runtime identity, then trusted agent-tool caller,
then authenticated human. Live-verified end-to-end on a dev gateway.
* fix(ci): split oversized session modules and refresh prompt snapshots
Split the max-lines offenders at concept boundaries for session equality, tool overrides, and protocol owner schemas. Remove the redundant Number conversion from the node:sqlite participant count. Refresh prompt snapshots after drift from the sessions and sessions_spawn tool description updates.
* fix(ci): restore solo-mode chip suppression and conform new method descriptors
Solo-mode root cause: owner-assignment submenu options reused the permanent owner-chip custom element, so hidden menu avatars were counted as attribution chrome. Menus now use viewer avatars while gateway-gated owner chips remain exclusive to collaborative sessions.
Conform sessions.assignOwner to the 2026.8 descriptor and append-only advertised-method inventories, and regenerate the Swift and Kotlin protocol surfaces.
Keep historical v15/v14 fixtures frozen by stripping the new owner columns; the existing range already excludes the participant table. Replace the new raw SQLite schema probes with synchronous Kysely queries.
Clear max-lines by splitting the organizer host contract, pure agent-navigation projections, and ownership/filtering sidebar cases at their concept boundaries.
* fix(ci): integrate ownership series with latest main surfaces
Wire the sessions-page assign-owner action, merge capability imports, narrow the navigation export scope, and apply sessions-create formatting.
The owner-presence regression came from hidden assign-owner menu avatars emitting data-viewer-id, so owner and menu chrome now opt out of presence markers while real facepiles retain them.
* fix(sessions): scope the involving-me filter to profile-backed participants
Session participant history mixed channel-native sender ids with authenticated Gateway profile ids, so involving-me missed real sessions and could accept numeric collisions.
Record the actor_source namespace at each producer, carry it through the internal SQLite projection, and match authenticated viewers only against profile-backed human participants. Legacy NULL sources fail closed for filtering, while channel ids remain available for display.
* build(ui): raise startup budget baseline for session ownership surfaces
Ownership chips, assignment menus, and the participant stack add ~0.7 KiB
gzip to the startup path; CI compression landed just over the previous
baseline+tolerance. Hard cap (350 KiB) unchanged.
* refactor(sessions): drop raw NULL projection for the lazy actor_source column
The Kysely guardrail rejects typed raw sql snippets outside allowlisted
boundaries; select the lazily-ensured column only when present and let the
row projection treat its absence as unknown/legacy.
* build(ui): refresh combined startup baseline
* feat(protocol): add named session icon glyphs
* feat(ui): add session glyph and custom emoji picker
* docs: describe named and custom session icons
* fix(ui): give the custom emoji input an accessible name
ClawSweeper P2: the input had no label relationship; assistive tech announced an unnamed edit control.
* fix(protocol): guard the v-flag icon regex for browser module loads
ClawSweeper P1: the picker's static import evaluates this module in the browser; pre-Unicode-Sets engines threw at module scope and took down the session menu. Lazy capability-guarded construction; such engines fall back to the grapheme heuristic as client pre-validation while the Gateway keeps exact RGI validation.
Message-batch transcript commits broadcast sessions.changed via the raw
transcript-update handler (server-session-events.ts), which bumps no fence
version — only emitSessionsChanged does. A completed sessions.list result
cached just before the commit stayed valid, so list rows kept stale
transcript-derived previews/titles until an unrelated session mutation.
Give the transcript-update emitter its own monotonic version, advanced
before listeners run (commit-then-broadcast), and add it to the list fence.
* fix(sessions): isolated gateways no longer inherit HOME external session catalogs
A gateway on isolated state (custom OPENCLAW_STATE_DIR/CONFIG_PATH/OPENCLAW_HOME,
relocated home, or any named profile) listed, read, continued, archived, and
reopened the operator's real Claude Code/Codex/OpenCode/Pi sessions from the
process HOME. External catalogs now require the default install identity for
process-HOME scans: every catalog verb receives the isolation policy and rejects
HOME-fallback local targets, unknown providers fail closed unless they declare
supportsProcessHomeIsolation, and one structured warning records the skip.
Paired-node hosts and explicitly rooted stores (CLAUDE_CONFIG_DIR, CODEX_HOME,
OPENCODE_DB, Pi session dirs) keep working; default-identity gateways are
unchanged.
* fix(sessions): inject catalog HOME-isolation fact at registry construction
* chore(sdk): regenerate plugin API baselines after rebase
* chore(sdk): regenerate plugin API baselines after rebase
* feat(ui): commit-scoped session diff viewer with file, sync, and view menus
The Control UI session diff panel becomes a dense Amp-style viewer: per-file
menus (copy path, open file, reveal in file tree, open in editor), a Sync
Locally popover with a copyable git fetch command, view options (collapse all,
wrapping, split/unified layout), and a sticky footer that reports how far the
branch is ahead of its merge base and switches between all changes,
uncommitted work, and individual commits.
sessions.diff gains an additive scope param (all | uncommitted | commit) plus
commits, aheadCount, and mergeBase metadata. Commit-scope diffs read only the
object database, skip untracked collection, and bypass session-start baseline
filtering; unknown commits surface a typed unavailableReason. Offscreen file
bodies use content-visibility so large diffs stay responsive.
* fix(ui): satisfy session diff CI gates
* fix(gateway): fence commit-scoped session diffs to the advertised branch history
ClawSweeper found that commit-scoped sessions.diff accepted any commit resolvable in the checkout. Fence operator.read commit reads to the advertised merge-base..HEAD history and cover sibling-branch and base-history commits.
* feat(ui): unify system notices in chat with a minimal line family
Class A (model-facing system turns: restart recovery, gateway restarted,
generic internal_system fallback) and class B (lifecycle facts: compaction)
now share one hairline+icon+label rendering; known kinds show operator
summaries instead of raw injected prompts. Core producers share one
formatSystemTurnPrompt owner for the [System] prefix; subagent resume copy
unified from "gateway reload" to "gateway restart". No protocol, channel,
or marker changes.
* fix(ui): keep system-turn prompt helper out of the plugin SDK surface
Relocate formatSystemTurnPrompt to src/sessions/system-turn-prompt.ts so the
core-internal [System] convention does not widen the SDK API baseline; drop
the unused prefix/type exports; use var(--cursor-action) per cursor policy.
Keep configured engines selected before the current turn is persisted, then durably advance accepted turns across embedded and CLI paths. Preserve runtime plugin ownership and canonical transcript anchors through prepared execution.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Co-authored-by: Peter <noreply@anthropic.com>
* fix(hooks): allow scoped hook wakes for unscheduled targets; keep announce wake aligned with queued session
- heartbeat-wake-policy: add isTargetedImmediateHookWake so a scoped hook
wake (source=hook, reason=hook:*) counts as an immediate targeted wake;
a known agent without a recurring heartbeat schedule can now be woken
once so its queued hook event gets consumed instead of sitting unread.
- scheduler + execution: accept the hook wake in the allowsUnscheduledTarget
gate alongside notifications-event wakes; unconfigured agents stay rejected.
- hooks announce path: only pass agentId when the hook explicitly named an
agent; an unnamed hook resolves its event session from fresh config at
announce time, so pairing the dispatch-time default agent with that
session could wake a stale agent after a default-agent reload.
- regression tests: targeted hook wake for an unscheduled agent runs once;
unconfigured hook targets rejected; announce wake omits default agentId
for unnamed hooks.
* test(infra): split hook-wake heartbeat tests out of scheduler test file
Keep heartbeat-runner.scheduler.test.ts inside the oxlint max-lines budget
(1045 -> 995 effective lines) by moving the two hook-source wake targeting
tests into heartbeat-runner.hook-wake.test.ts. No production code changes.
* fix(gateway): target global-scope hook wakes at the fresh default agent
Global session scope resolves hook event keys to the unscoped "global"
sentinel, which carries no agent identity. The scheduler refuses to
resolve a target from it, so announce and failure wakes for unnamed
hooks left the queued event unread.
Carry the fresh default agent alongside the global key on both paths
(announce and failure), mirroring the enqueue-wake path. Non-global
unnamed-hook wakes are unchanged: their session key embeds the agent,
and pairing a frozen default agent could wake a stale agent after a
default-agent reload.
* fix(gateway): target recovered global failure wakes at the explicit or fresh default agent
The failure-wake agent selection gated both branches on hookEventSessionKey.
When initial event-key resolution fails (early config failure), that key is
absent even though recovery still yields the unscoped "global" sentinel; the
wake then carried no agentId and the queued failure event sat unread.
Use the recovered session key for the sentinel check and attach the explicit
agent, or the fresh default agent, so the global failure event is consumed.
Non-global unnamed-hook wakes are unchanged: their session key embeds the
agent, and pairing a frozen default agent could wake a stale agent after a
default-agent reload.
* fix(gateway): preserve agent identity for global hook wakes
Signed-off-by: sallyom <somalley@redhat.com>
* test(infra): allow seeding a heartbeat session without delivery fields
The new hook-wake test seeds an empty session for the hooks agent, which
has no real delivery context. Relax seedSessionStore's seed type to
Partial so empty seeds typecheck.
* fix(gateway): keep global hook wakes on the dispatch-time agent
The announce and failure wake paths resolved the default agent from
fresh config at queue-execution time. If a default-agent reload happened
between dispatch and execution, a global-scope wake retargeted at the new
default while the isolated run used the accepted agent, so the event
produced under one agent was consumed by another. Freeze the accepted
agent at dispatch (acceptedHookAgentId) and prefer it in both wake paths;
fresh resolution remains the fallback for pre-acceptance failures. Adds
announce + failure reload regressions (default main -> work mid-queue).
* test(hooks): annotate reload fixtures as OpenClawConfig
The default-agent reload fixtures widened session.scope to string and
fixed agents.entries to the first assignment's shape, breaking
check-test-types. Annotate both fixtures so each reload conforms to the
config contract.
* fix(hooks): partition global-scope hook events by owning agent
Global-scope hook announce and failure events all land in the shared
literal `global` system-event queue while each wake targets a single
agent, so concurrent hook completions can cross-consume each other's
events: the first targeted wake drains every agent's queued result or
failure into one prompt.
Give transient system events an ownerAgentId set by the hook dispatch
paths (announce, failure, and wake-only enqueues) whenever the event
lands on the unscoped sentinel queue, and filter the heartbeat preflight
peek to the running agent's owned events. The store's literal global
session row stays shared; unowned events remain visible to every agent,
so non-hook global events are unchanged.
Add a two-agent concurrent regression proving the first targeted wake
leaves the other agent's event queued for its owner.
* fix(hooks): isolate global events by agent
Signed-off-by: sallyom <somalley@redhat.com>
* fix(infra): unexport internal targeted-wake helpers
isTargetedImmediateSystemEventWake and isTargetedImmediateHookWake are
only consumed by isTargetedImmediateUnscheduledWake within the same
module; keep them module-private so knip deadcode stops flagging the
unused exports.
---------
Signed-off-by: sallyom <somalley@redhat.com>
Co-authored-by: sallyom <somalley@redhat.com>
* fix(ai): preserve Responses server compaction state
Persist opaque Responses compaction items as fenced provider replay state so long stateless sessions can resume authoritative compressed history without exposing it in display or diagnostics. Carry state through worker transcripts and prune replay prefixes without splitting tool pairs.
Release note: Preserve long OpenAI Responses sessions across server-side compaction and worker restarts.
Related: #95788
* test(ai): align long-context fixtures with CI contracts
Make tool-result fixtures type-complete, use the canonical model selector helper, remove unused test-helper exports, and route the paid long-context live probe through the dedicated Gateway profile shard.
* test(ai): type mocked Responses terminal events
Give the mock SSE event collection an explicit open event shape so terminal response events coexist with output-item events under the root test typecheck.
* fix(ai): suppress rejected compaction replay
Persist a route-fenced suppression tombstone when encrypted-content recovery rejects a compaction item, so later turns do not retry the same opaque state. Preserve the tombstone through transcript redaction and cover successful fallback followed by the next turn.
* fix(ai): keep compaction suppression transport-private
Keep the suppression contract local to its sole Responses transport owner and make the regression fixture satisfy root type and lint checks without widening the Plugin SDK surface.
* refactor(ai): remove compaction suppression re-export
* fix(ai): scope compaction suppression to replay route
Keep foreign-route rejection tombstones from hiding the newest compatible Responses compaction while preserving same-route suppression.
* fix(ai): harden Responses replay recovery
Stage encrypted replay recovery so compaction is only suppressed after an attributable rejection. Preserve terminal ordering and keep provider replay within worker frame budgets without truncating opaque state.
* refactor(ai): centralize Responses output indexes
Keep normalized output identity tracking in the stream-slot owner, move response failure state to its diagnostic owner, and remove the obsolete replay clone export so exact-head static gates remain shrink-only.
* fix(ai): retain idless terminal tool identity
Use the canonical empty identity only when a provider supplies neither call nor item id, preventing terminal recovery from duplicating a done-only tool call while preserving stronger identities when available.
* fix(sessions): hide provider replay from public events
* fix(ai): stage encrypted replay recovery
* fix(ai): keep replay attempt kind internal
* fix(ai): route Azure through replay recovery
Use the shared encrypted-content retry owner for Azure Responses so compaction suppression and prompt-observer variants stay coherent across transports.
* fix(ai): harden replay persistence boundaries
Fence Azure replay by the resolved request endpoint, drop invalid replay during transcript sanitization, and surface worker-launch replay omissions through the existing redacted diagnostic path.
* refactor(runtime): own ambient state lifecycle cleanup
* chore(plugin-sdk): refresh additive API baseline
* fix(runtime): restore agent wait listener lifecycle
Keep agent-job data process-shared while preserving module-local event subscription ownership. Ratchet the public wildcard budget down after replacing text-runtime's wildcard singleton export with named exports.
* fix(exec): preserve approved exec continuation output
Approved async exec continuations reused the compact background notification
formatter, so the agent resumed from the last 400 characters of output with all
whitespace collapsed and the head silently dropped.
Both hosts now render the continuation through a shared whitespace-preserving
formatter bounded at 16,000 UTF-16 units. Compact `notifyOnExit` notifications,
poll/retained output, and the gateway diagnostics branch are unchanged.
The truncation marker deliberately reports no exact omission count: output can
already be capped at capture time without leaving a marker, so an exact number
would describe only this cut while reading as though nothing else was lost.
Closes#41152
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2d9762e2-f967-4cdd-9895-299512a20114
* fix(exec): secure approved continuation handoff
* test(exec): cover authenticated approval handoff
* fix(exec): retain approved followup delivery ownership
* fix(exec): retain compact continuation fallback
* fix(exec): bound accepted followup observation
* fix(exec): keep observer diagnostics internal
---------
Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2d9762e2-f967-4cdd-9895-299512a20114
* feat(gateway): manage audio and video attachments
* feat(gateway): preserve generated media metadata
* fix(gateway): align seeded managed media
* fix(gateway): scope pending media trust by URL
* fix(gateway): split managed media by trust
* fix(gateway): require terminal media trust unanimity
* fix(gateway): align managed media metadata types
* fix(gateway): refresh managed media contracts
* chore(ci): model managed media test seams
* test(sessions): expect canonical media kinds