fix(agents): heartbeats lose all conversation context on claude-cli chat↔heartbeat transitions (#121509)

* 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>
This commit is contained in:
James Tatum
2026-08-12 03:55:38 -07:00
committed by GitHub
parent 6780d87e4f
commit 5235269b3c
25 changed files with 502 additions and 83 deletions
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"10bdd5bfc10ab30dad145d36d7a78a052f52960c4f8e534a0ba258f5f777dba5","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"}
{"contentHash":"993018e34f2e7951c2a0412a4df0cd4be50256da04f3d6a539942c3a8168da24","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"}