You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Matrix-side companion to PR #12 — the two patterns from the research doc that generalize across both bridges. Closes the gap Jaden flagged: I shipped these for TG but not for Matrix.
What Matrix already had
retry_after_ms honoring via rateLimitedUntil ✓
Terminal-render 4× inline retry ✓
2000ms edit floor ✓
No sendMessage fallback on 429 (preserves statusEventId) ✓
What this PR adds
1. Char-delta edit gate
On non-terminal renders with no phase transition, skip the edit if the rendered text length changed by fewer than CHAR_DELTA_CUTOFF = 50 chars. Stops thinking-preview ticks (5 chars of italic narration delta) from burning matrix.org quota.
Phase transitions (STARTING → RUNNING, PLANNING → EDITING, etc.) always edit through so the user sees the header emoji + label swap immediately.
2. Self-tuning per-turn 429 backoff bonus
Each non-terminal 429 from matrix.org bumps backoffBonusMs by BACKOFF_PER_429_MS = 1500ms, capped at MAX_BACKOFF_BONUS_MS = 10_000ms. The bonus adds to the EDIT_MIN_INTERVAL_MS floor so subsequent edits in the same turn space out and the chain settles instead of bouncing off the limit repeatedly.
Previously each 429 only deferred to now + retry_after_ms without ratcheting — the next allowed tick could immediately hit another 429.
Fresh-spawn wipe
Both knobs (backoffBonusMs, rateLimitedUntil, lastRenderedLen, lastEditedPhase) added to the existing storage.delete batch in the /watch fresh-spawn path. Calm turns start fast, noisy turns self-throttle within their own scope.
Why no group/DM cadence split (the third TG borrow)
Doesn't port — matrix.org's per-room rate cap is uniform regardless of room size, so the TG group/DM signal isn't useful here. The 2000ms floor already targets matrix.org's quota directly.
Test plan
bun run typecheck clean
bun test src/render.test.ts — 43/43 pass
Deployed to matrix bridge worker 4be67d8a
Live test: send a multi-step prompt in a busy room; expect the bridge to ratchet down its edit rate as 429s come in instead of bouncing off the limit
Live test: send a trivial prompt right after; expect it to start fast (backoffBonusMs wiped on fresh /watch)
Stacks on
This PR is independent of PR #11 (one-emoji invariant) — both touch different parts of poller.ts. Merge in either order.
Matrix port of the two patterns from PR #12 that generalize cleanly
across both bridges. Matrix already has retry_after_ms honoring via
rateLimitedUntil and terminal-render 4× retry; what was missing:
(1) Char-delta edit gate
On non-terminal renders with no phase transition, skip the edit
if the rendered text length changed by fewer than CHAR_DELTA_CUTOFF
(50) chars. Stops thinking-preview ticks (5 chars of italic
narration delta) from burning matrix.org quota. Phase transitions
(STARTING → RUNNING, etc.) always edit through so the header
emoji + label swap stays snappy.
(2) Self-tuning per-turn 429 backoff bonus
Each non-terminal 429 from matrix.org bumps backoffBonusMs by
BACKOFF_PER_429_MS (1500) ms, capped at MAX_BACKOFF_BONUS_MS
(10s). The bonus adds to the EDIT_MIN_INTERVAL_MS floor so
subsequent edits in the same turn space out and the chain settles
instead of bouncing off the limit repeatedly. Previously each 429
only deferred to `now + retry_after_ms` without ratcheting, so
the next allowed tick could immediately hit another 429.
Both knobs wipe on /watch fresh-spawn (added backoffBonusMs +
rateLimitedUntil + lastRenderedLen + lastEditedPhase to the existing
storage.delete batch) — calm turns start fast, noisy turns
self-throttle within their own scope.
Group-vs-DM cadence (the third borrow in PR #12) does NOT port to
Matrix — matrix.org's per-room rate cap is uniform regardless of room
size, so the TG group/DM split isn't the right signal here.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: itsablabla <itsablabla@users.noreply.github.com>
Capy auto-review is paused for this organization because the monthly auto-review limit has been reached. Increase the limit or turn it off in billing settings to resume automatic reviews.
Second follow-up after Jaden asked to go deeper. Read all relevant TG
plugin files in Open-ACP/OpenACP end-to-end:
- activity.ts (491 lines): ThinkingIndicator + ToolCard + ActivityTracker
- streaming.ts (290 lines): MessageDraft text streaming
- formatting.ts (451 lines): markdownToTelegramHtml + renderToolCard
- topic-manager.ts (159 lines): per-session forum topics
- permissions.ts (126 lines): inline keyboard permission flow
Documents the actual mechanics the prior summary missed:
- ThinkingIndicator state machine: idle → showing (15s refresh) →
dismissed (msg stays in chat); auto-stop at 3 min enforced by the
refresh body itself; dismissed re-checked AFTER sendQueue wait
because the agent may have moved on during the queue wait.
- ToolCard internals: flushPromise as serializing chain; lastSentText
dedup; overflow strip (strip outputContent from specs when render
exceeds 4096); overflow chunks recycled via editMessageText; stale
chunks deleted when count decreases; aborted flag for destroy().
- ActivityTracker seal-and-keep vs clear-on-new-prompt: out-of-order
tool updates after a seal still route to previousToolStateMap; new
prompt clears previous refs to prevent cross-turn leakage.
- MessageDraft critical patterns: snapshot buffer BEFORE await (with
the explicit "append() can mutate" comment); do NOT reset messageId
on transient errors (matches our hard-won d79c798 lesson);
displayTruncated flag for finalize correctness; "enqueue all chunks
in tight synchronous loop" to prevent concurrent handlers slipping
between.
- renderToolCard visual order: completed-first, then plan, then
running; ACP status icons ⬜🔄✅; tool-kind icon per-spec.
- Permission flow: nanoid(8) callback key for 64-byte callback_data
limit; in-memory pending Map (not persistent across restarts);
two-channel surface (session topic + Notifications topic with deep
link); editMessageReplyMarkup({reply_markup: undefined}) strips
buttons on response.
Closes with a prioritized table of borrowable patterns + effort
estimates. Top two cheap wins: 3-min thinking auto-stop (~10 lines
render-side) and displayTruncated flag (~5 lines poller-side).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: itsablabla <itsablabla@users.noreply.github.com>
The reason will be displayed to describe this comment to others. Learn more.
🔴 Steering path doesn't reset lastRenderedLen/lastEditedPhase, causing char-delta gate to suppress the "💬 You:" injection
The steering path at replicas-matrix-bridge/src/poller.ts:197 resets lastRendered: "" to force a re-render, but does not reset the newly-added lastRenderedLen and lastEditedPhase storage keys. This creates an inconsistency: the text-equality guard (text === lastRendered) passes because lastRendered is "", but the char-delta gate at line 630 compares text.length against the stalelastRenderedLen from the previous edit cycle.
Concrete scenario: Turn is active with lastRenderedLen ≈ 350, phase is RUNNING. User sends a follow-up → steering injects 💬 <i>You: hello</i> (~30 extra chars) and resets lastRendered: "" but leaves lastRenderedLen = 350. The immediate renderAndSend() is throttled by gate (1) (edit interval). When the next attempt passes gate (1) 2-4s later, gate (3) evaluates Math.abs(~383 - 350) = 33 < 50 → suppressed. The user's follow-up line remains invisible in the status frame until the agent produces enough new content to push the delta past 50 characters.
(Refers to line 197)
Was this helpful? React with 👍 or 👎 to provide feedback.
The reason will be displayed to describe this comment to others. Learn more.
🔴 Steering path doesn't reset lastRenderedLen/lastEditedPhase, causing char-delta gate to suppress the "💬 You:" injection
The steering path at replicas-matrix-bridge/src/poller.ts:197 resets lastRendered: "" to force a re-render, but does not reset the newly-added lastRenderedLen and lastEditedPhase storage keys. This creates an inconsistency: the text-equality guard (text === lastRendered) passes because lastRendered is "", but the char-delta gate at line 630 compares text.length against the stalelastRenderedLen from the previous edit cycle.
Concrete scenario: Turn is active with lastRenderedLen ≈ 350, phase is RUNNING. User sends a follow-up → steering injects 💬 <i>You: hello</i> (~30 extra chars) and resets lastRendered: "" but leaves lastRenderedLen = 350. The immediate renderAndSend() is throttled by gate (1) (edit interval). When the next attempt passes gate (1) 2-4s later, gate (3) evaluates Math.abs(~383 - 350) = 33 < 50 → suppressed. The user's follow-up line remains invisible in the status frame until the agent produces enough new content to push the delta past 50 characters.
(Refers to line 197)
Was this helpful? React with 👍 or 👎 to provide feedback.
@devin-ai-integration[bot] This repository is not connected to a Replicas organization.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Matrix-side companion to PR #12 — the two patterns from the research doc that generalize across both bridges. Closes the gap Jaden flagged: I shipped these for TG but not for Matrix.
What Matrix already had
retry_after_mshonoring viarateLimitedUntil✓sendMessagefallback on 429 (preservesstatusEventId) ✓What this PR adds
1. Char-delta edit gate
On non-terminal renders with no phase transition, skip the edit if the rendered text length changed by fewer than
CHAR_DELTA_CUTOFF = 50chars. Stops thinking-preview ticks (5 chars of italic narration delta) from burning matrix.org quota.Phase transitions (STARTING → RUNNING, PLANNING → EDITING, etc.) always edit through so the user sees the header emoji + label swap immediately.
2. Self-tuning per-turn 429 backoff bonus
Each non-terminal
429from matrix.org bumpsbackoffBonusMsbyBACKOFF_PER_429_MS = 1500ms, capped atMAX_BACKOFF_BONUS_MS = 10_000ms. The bonus adds to theEDIT_MIN_INTERVAL_MSfloor so subsequent edits in the same turn space out and the chain settles instead of bouncing off the limit repeatedly.Previously each 429 only deferred to
now + retry_after_mswithout ratcheting — the next allowed tick could immediately hit another 429.Fresh-spawn wipe
Both knobs (
backoffBonusMs,rateLimitedUntil,lastRenderedLen,lastEditedPhase) added to the existingstorage.deletebatch in the/watchfresh-spawn path. Calm turns start fast, noisy turns self-throttle within their own scope.Why no group/DM cadence split (the third TG borrow)
Doesn't port — matrix.org's per-room rate cap is uniform regardless of room size, so the TG group/DM signal isn't useful here. The 2000ms floor already targets matrix.org's quota directly.
Test plan
bun run typecheckcleanbun test src/render.test.ts— 43/43 pass4be67d8abackoffBonusMswiped on fresh/watch)Stacks on
This PR is independent of PR #11 (one-emoji invariant) — both touch different parts of
poller.ts. Merge in either order.Workspace · Slack Thread