* fix(whatsapp): silently drops inbound messages when more than 450 are waiting
WhatsApp asked the shared ingress monitor to enforce a 450-entry cap and a
30-day TTL on pending durable ingress rows. Pending rows are undelivered work,
not history, and the monitor runs retention before its first claim, so an
account holding 451 accepted messages lost the oldest one to a hard DELETE
before it could ever be dispatched. Loss scaled linearly with backlog depth and
left no tombstone, no failure record, and no retained payload.
Drop pendingTtlMs and pendingMaxEntries from the WhatsApp retention config.
Completed and failed retention is unchanged so on-disk history stays bounded,
and the config now matches every other channel on the shared monitor, whose
defaults cover completed and failed only. The two values were carried over
mechanically from the pre-SQLite openKeyedStore replay-guard cache in
b0679d1f13 and inlined verbatim in #115824.
Add regression coverage driving the real monitor and the real SQLite ingress
queue over 451 accepted records.
* test(whatsapp): cover pending ingress retention
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(auto-reply): deliver ingress-retried messages after their queued run is dropped
* fix(auto-reply): key queued dedupe release by adoption lifecycle
Overflow-summary compaction clones a queued run onto a new object, so a
release keyed by the run object missed runs completed via their clone and
the ingress retry stayed suppressed on the summarize drop policy. The
adoption lifecycle reference survives cloning and already keys admission
state, so it is the identity the release must use.
* fix(auto-reply): guard queued dedupe ownership
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(agents): throw CompactionError when summarization fails
Transform compaction summarization from silent infinite failure loops
into clear, actionable errors.
Previously, when all summarization attempts failed, summarizeWithFallbackResult
returned a generic fallback string blaming "size limits". This caused:
- Misleading error messages (auth/network errors reported as "size limits")
- Callers unable to distinguish real summaries from failures
- Silent infinite retry loops where token counts climbed indefinitely
Now, CompactionError is thrown with the original error details preserved.
Callers immediately know compaction failed and can take appropriate action
(switch model, notify user, etc.).
Fixes#115413
Co-Authored-By: nebulacoder-v8.0 <noreply@zte.com.cn>
* fixup: export CompactionError from agent-core index, remove dead code
* fixup: address ClawSweeper review - keep CompactionError internal, track last error, add failure-proof test
* fixup: type corrections in failure-proof test
* fixup: type annotate failure-proof messages as UserMessage[]
* fixup: avoid possibly-undefined array access in failure-proof test
* test(agents): prove compaction provider failure cancellation
---------
Co-authored-by: nebulacoder-v8.0 <noreply@zte.com.cn>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(agents): deny all tools when scheduled authority names a removed account
resolveGroupToolPolicy returned { allow: [] } as its fail-closed sentinel
when a scheduled run named a channel account that is no longer configured.
The runtime matcher treats an empty allowlist as "allow everything not
denied", so revoking the creator account widened the scheduled tool surface
to the full set including exec and apply_patch instead of denying it.
The sentinel now carries an explicit wildcard deny, which the matcher and
the plugin-harness native-tool clamp both honor.
* test(agents): cover scheduled-run tool denial after owner account removal
Adds capability-profile level coverage that a scheduled run whose named
owner account was removed resolves to a deny-all group policy, exercising
the scheduled-authority handoff from resolveConversationCapabilityProfile
through resolveRequesterToolPolicies into resolveGroupToolPolicy.
* fix(gateway): enforce scheduled account authority for loopback tools
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(cron): bound restart catch-up to the active schedule
Editing a recurring job's schedule made the gateway fire it immediately on
the next restart. Startup catch-up compares the new schedule's previous slot
against lastRunAtMs, which still belongs to the retired schedule, so a slot
that never existed under the old schedule counted as missed.
Record when scheduling inputs take effect and replay a missed slot only when
it is newer than that. Jobs whose schedule never changed carry no stamp and
keep replaying every computed slot, so catch-up is unchanged for them.
The missed-slot predicate was duplicated in the runnable check and the
backoff-deferral pass; both now share one helper so the bound cannot drift.
Fixes#91944
* fix(cron): protect schedule activation ownership
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(memory-core): stop MEMORY.md compaction from deleting user headings
parseMemoryBlocks segmented MEMORY.md only on "## ", so a user heading of
any other level did not close the promotion section above it. The section
ran on to the next "## " heading or EOF, and dropping it to fit the budget
deleted the user text it had swallowed, with no backup and no diagnostic.
Close the current block on any ATX heading, keeping the generator's own
"### Global" and "### Project: <key>" subheadings inside the promotion
body so multi-project sections stay droppable whole.
* fix(memory-core): identify generated promotion subsections by their entry marker
The first exception treated every "### Global" and "### Project: <key>"
heading under a promotion block as dreaming-owned, so a user who wrote one
of those headings themselves still lost it to compaction, which is the same
data-loss class this change set out to fix.
A generated subsection always leads with an openclaw-memory-promotion entry
marker comment, so require that marker as the next non-blank line before
treating the heading as part of the promotion body. User headings that
collide with the generated names now terminate the block and survive.
* fix(memory-core): recognize tab-delimited and empty ATX headings
The heading detector required a literal ASCII space after the opening #
sequence. CommonMark also allows a tab or end of line, so a user note
starting with a tab-delimited heading stayed inside the promotion block
above it and was deleted when compaction dropped that block.
Accept a space, a tab, or end of line after the # sequence. The generated
subsection pattern stays space-only because that is what buildPromotionSection
emits, so a tab-delimited heading is user-authored and is preserved.
* fix(memory-core): preserve Setext headings during compaction
Setext headings were still absorbed into the generated promotion block above them, so dropping that block could silently delete durable user notes. Move the heading paragraph into a preserved block when its underline is encountered, while keeping marker-backed generated content compactable.
* fix(memory-core): preserve indented ATX headings
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(agents): apply_patch rewrites bytes on hunk context lines
A fuzzy apply_patch update replaced the entire matched span with the
model-authored patch text, so trailing whitespace, typographic punctuation,
and tab indentation on lines the hunk marked as context were overwritten
while the tool reported plain success.
The parser now records which emitted lines came in as context and which old
line each one came from, and the update applier keeps the file's own bytes
for those lines. Added and removed lines are still written from the patch.
* test(agents): cover apply_patch context preservation
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>