* fix(agents): resolve session-stable reply mode for direct resolver turns
Heartbeat wakes and other direct getReplyFromConfig callers bypass
dispatch, so no session-stable delivery mode reached their CLI binding
facts and prepare computed no messageToolPolicyHash, while dispatched
chat turns hashed the stable mode. resolveCliSessionReuse treats that
one-sided hash as a policy change, hard-invalidating the binding on
every chat<->heartbeat transition and leaving heartbeats to run in
fresh CLI sessions with no conversation history (historyPrompt=none).
Extract the synthetic-turn stable-mode resolution that
prepareAgentCommandExecution already used into
resolveSessionStableReplyMode, and apply it in prepareReplyRunContext
whenever a synthetic turn arrives without dispatch's injected mode, so
every turn kind on a session derives the same binding facts and hash.
Fixes#121485
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(agents): derive synthetic binding facts from the dispatch policy owner
Consolidate the synthetic-turn stable-mode resolution onto dispatch's own
policy pieces instead of a simplified parallel resolver:
- resolveVisibleRepliesPolicy is extracted from dispatch prepare-context
(configured visible-replies guard + harness default chain) and shared, so
the fallback and dispatch cannot drift on harness defaults, live-vs-entry
chat facts, or ExplicitDeliverRoute.
- The stable fact now applies dispatch's messageToolAvailable downgrade via
the canonical tool-policy resolvers: tool-only delivery with a policy-denied
message tool records automatic, matching dispatch.
- Synthetic turns no longer fall back to their effective turn mode: a
response-tool heartbeat's message_tool_only is per-turn enforcement, not
session policy, and previously bypassed the stable fallback entirely.
Regression coverage: response-tool heartbeat facts match dispatched turns,
and a message-tool-denied config downgrades the synthetic stable fact.
Refs #121485
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(agents): keep the session-stable reply mode sender-independent
Dispatch's stable-mode downgrade used the turn's sender-aware message-tool
availability, so a sender-scoped message denial hashed the stable policy as
automatic on chat turns while sender-less synthetic turns hashed tool-only —
the same binding reset loop on another axis.
resolveStableMessageToolAvailability becomes the one sender-independent
owner: dispatch passes it as sessionStableMessageToolAvailable for the
stable-mode resolution only (effective per-turn enforcement keeps the
sender-aware verdict), and synthetic binding facts already consume it.
Computed only when the visible-replies candidate is message_tool.
Refs #121485
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(agents): derive stable reply facts from session surfaces, not wake plumbing
Third-pass review fixes on the #121485 consolidation:
- Strip system-event wake providers ("heartbeat", "cron-event") from the
stable context and resolve origin-less entries as internal, so synthetic
facts take the same internal-channel branch dispatch's live webchat turns
do instead of falling through to harness defaults.
- Fall back to persisted session facts (delivery channel/origin, groupId,
groupChannel/subject, accountId) for the sender-independent availability
stack, so bare-ctx callers like command prepare resolve the same
group/account-scoped policies as dispatched turns; dispatch now passes its
session entry too.
- Collapse the redundant second mode resolution into the availability
downgrade, surface dispatch's injected stable mode separately from
resolvePromptSourceReplyMode so the synthetic slot has one owner, and
reuse resolveTurnModelOverride instead of an inline copy.
- Refresh docs/.generated/plugin-sdk-api-baseline.jsonl: closure hashes only
(17 exports, zero declaration changes, verified) — plugin-sdk reply/meeting
entrypoints transitively bundle the reply pipeline this branch refactors.
Regression: origin-less entry heartbeat resolves internal-automatic facts.
Refs #121485
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: adapt to main module moves and split API baseline
Mechanical rebase refresh onto d3696f8d76: subagent-capabilities moved to
src/agents/subagents/spawn/, and the plugin-sdk API baseline is now
per-entrypoint content hashes (#122082) — regenerated for the reply-pipeline
closure this branch touches.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* refactor(memory-core): split index manager along the inheritance chain
* fix(memory): restore the manager lazy boundary
* refactor(memory-core): delete dead manager plumbing
* chore(lint): ratchet max-lines baseline after memory manager split
* fix(memory): keep the manager out of the runtime facade
* test(memory): cover transitive manager facade imports
* refactor(agents): split CLI runner concepts
* chore(lint): ratchet cli-runner max-lines baseline
* refactor(agents): remove stale runner import
* refactor(agents): preserve cleanup error typing
* refactor(agents): disambiguate CLI backend predicate
* test(agents): align code-mode pending boundary
Fixes red main: both host calls are unsettled when the guest snapshot is first parked; later waits still prove that settled calls are filtered.
* test(agents): tolerate code-mode settlement race
Fixes red main: the fast host call may settle as the guest snapshot is parked, while the deliberately slow call must remain pending.
* 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.
* fix(plugins): own packed entry lifecycles by package
Persist package ownership per runtime child, route lifecycle actions through one closed resolver, reconcile removed child policy during updates, and retain rollback generations until durable config/index commit.
* fix(plugins): break uninstall policy import cycle
* test(plugins): model package ownership in lifecycle fixtures
* feat(gateway): projects.list groups known checkouts by repo identity
Implements docs/plan/runners.md milestone 4 derived projects read model.
* feat(ui): regroup the Where picker by gateway, devices, and cloud
* feat(ui): placement chip shows where a session runs with reclaim
Implements docs/plan/runners.md milestone 4 placement display and reclaim.
* refactor(gateway): extract source-agnostic desktop relay core
Split the cloud-worker desktop observer into a reusable core under
src/gateway/desktop/ so upcoming desktop sources (gateway host, nodes)
plug into one relay pipeline:
- attachment.ts: RfbAttachment union (unix socket | loopback TCP)
- session-registry.ts: generic observer lifecycle (8-observer cap,
controller eviction, 60s linger, owner-epoch fencing) with typed
stale-owner/stopped errors
- observe-bridge.ts: single-use observe tokens + WS relay, path moves
to /desktop/observe (clients consume wsPath verbatim)
- rfb-view-only-filter.ts: pure move
desktop-tunnel.ts keeps only worker SSH acquisition and app launch,
plugged into the shared registry; external API unchanged. Behavior
is identical; worker desktop tests pass unmodified apart from import
and path renames. Drops the never-used now() option.
* fix(gateway): fence only superseded desktop sessions
The desktop core extraction replaced the owner fence's "stop strictly
older owners" check with an unconditional session stop. A launcher that
claims an owner epoch first, then reaches its async fencing pass after a
same-epoch observe has already created the session, tore that session
down and failed the observer with "stopped before connecting".
Restore the original invariant in the registry that owns it:
stopSuperseded() retires an entry only when its epoch is strictly lower
than the claimant's, so peers sharing a generation keep the session.
The regression test drives launch-then-observe at one epoch and fails on
the pre-fix code inside fenceReplacedOwners.
* refactor(gateway): drop unused WorkerDesktopTunnels type export
Use successful per-attachment staging results in unsupported-document guidance so sandboxed agents receive only workspace-readable paths. Failed or partial staging keeps the existing fallback.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* refactor(agents): delete dead model-selection surface, consolidate compaction target assembly
Removes production surface with zero live callers: buildConfiguredAllowlistKeys
(orphaned since fallback allowlist filtering was split out), the
retryTransientProviderRuntimeMiss resolveModelAsync option (its gateway startup
prewarming caller was replaced by prepared runtime snapshots), the ignored
useAsyncModelResolution plumbing (kept only as a deprecated no-op field on the
plugin-SDK-shipped prepareSimpleCompletionModelForAgent), and dead facade
re-exports (inferUniqueProviderFromCatalog, ThinkLevel, ModelRefStatus).
Consolidates resolveEmbeddedCompactionTarget's five hand-built result sites into
one assembleTarget helper owning the auth-profile-drop-on-provider-change rule;
resolution precedence is unchanged and now pinned by new table-driven cases
(unique-provider inference, ambiguous literal, profile-suffix preservation).
Net -93 production LOC, -106 test LOC.
* chore(sdk): regenerate plugin SDK API baseline after facade export removals
* feat(gateway): add /startupz startup probe with auth-gated version detail
Startup/traffic-admission probe that excludes downstream channel health:
200 started once startup work completes and the gateway is not draining,
503 starting/draining otherwise. Version and uptime are only included for
local-direct or authenticated callers, reusing the /readyz detail gate.
* fix(deploy): use /startupz for traffic admission in bundled templates
fly.toml gains its missing HTTP check; render.yaml stops using pure
liveness as admission; k8s pins an immutable image tag, seeds config
only when missing, and adds a startupProbe; stale Fly healthcheck-port
doc corrected (healthcheck follows the active gateway lock port since
bc4221a07e).
* docs(k8s): make persistent-file config ownership explicit with reseed path
Remove Discord's duplicate whole-turn session serialization so corrections reach shared reply admission and steer the active run. Preserve shared steer, followup, collect, and interrupt behavior.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* fix(agents): use unique isolated completion ids
Prevent same-millisecond CLI completions from sharing run authority by minting a UUID-backed wrapper identity.
* test(agents): type isolated completion release barrier
Let eligible embedded host runs inspect root-approved unsupported documents after final sandbox, filesystem, provider, owner, and tool-policy gates. Generic ACP, sandboxed, URL-only, and restricted-tool paths retain the plain marker.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* fix(agents): scope quota failures to auth profiles
* test: repair provider suspension CI coverage
* test: keep suspension reset fixture internal
* fix(agents): spend cooldown probe only on transient candidates
Consume the one-run cooldown probe only when the candidate’s own unavailable reason is transient, so a billing-disabled pin cannot block a recoverable backup.\n\nFinding from the ClawSweeper review on openclaw/openclaw#121278.
* refactor(sessions): deprecate QuotaSuspension.laneId instead of removing
The shipped plugin-SDK surface deprecation policy requires keeping the inert field until the next surface window.
* fix(agents): extend transient probe policy to plugin-harness auth path
* fix(agents): keep provider overload from cooling auth profiles
* fix(agents): exhaust rotation candidates without cooldown records
Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
* test(agents): align auth rotation mocks with current main
* docs: regenerate plugin SDK API baseline
Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
* style(agents): format session-suspension test after rename resolution
---------
Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
`attachGatewayWsConnectionHandler` took both a `resolvedAuth` snapshot and an
optional `getResolvedAuth`, and used the snapshot for nothing except defaulting
the getter. Every production caller already passed the getter, so the snapshot
was a second signal for a fact that rotates on config reload — the exact shape
that goes stale as sibling paths evolve.
Collapse it to one required getter and drop the argument from
`server-ws-runtime.ts` and `server-startup-finish.ts`. That also puts
`server-startup-finish.ts` back under the 700-line cap, which the preflight
change's `getResolvedAuth` pass-through had pushed one line over; the file was
already sitting exactly at the limit on `main`.