fix: emit message:sent hook on Telegram streaming preview finalization#50917
Conversation
Greptile SummaryThis PR correctly closes a gap where Key changes:
Confidence Score: 4/5
Prompt To Fix All With AIThis is a comment left during a code review.
Path: extensions/telegram/src/lane-delivery.test.ts
Line: 561-568
Comment:
**Missing test for `stopCreatesFirstPreview` + `onPreviewDelivered` path**
The existing test covers the case where the stream already has a message ID before delivery (`answerMessageId: 999`), so `previewMessageId` is captured as a valid number and the `?? lane.stream?.messageId()` fallback on lines 536 and 544 of `lane-delivery-text-deliverer.ts` is never actually exercised.
The path where stop creates the _first_ preview message (i.e., `answerMessageIdAfterStop: 777`, no initial `answerMessageId`) is exercised by the existing "primes stop-created previews with final text before editing" test, but that test makes no assertion on `onPreviewDelivered`. This leaves the `previewMessageId ?? lane.stream?.messageId()` fallback logic — which was specifically introduced in this PR to surface the message ID for that case — untested.
Consider adding a test similar to the existing "primes stop-created previews with final text before editing" case that also asserts the hook fires with the correct message ID:
```ts
it("calls onPreviewDelivered when stop-created first preview is finalized", async () => {
const harness = createHarness({ answerMessageIdAfterStop: 777 });
const result = await harness.deliverLaneText({
laneName: "answer",
text: "Hello final",
payload: { text: "Hello final" },
infoKind: "final",
});
expect(result).toBe("preview-finalized");
expect(harness.onPreviewDelivered).toHaveBeenCalledWith("Hello final", 777);
});
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: extensions/telegram/src/lane-delivery-text-deliverer.ts
Line: 435-439
Comment:
**Unreachable fallback in `archivedPreview.textSnapshot ?? text`**
`ArchivedPreview.textSnapshot` is a required `string` field, so it can never be `null` or `undefined` at runtime. The `?? text` fallback is therefore dead code — it can never activate.
The same pattern appears at line 543 in the active-stream path (`lane.lastPartialText ?? text`): `DraftLaneState.lastPartialText` is also typed as a required `string`, so the fallback is similarly unreachable there.
Both expressions could be simplified:
```suggestion
const visibleText = archivedPreview.textSnapshot;
params.onPreviewDelivered?.(visibleText, archivedPreview.messageId);
```
How can I resolve this? If you propose a fix, please make it concise.Last reviewed commit: "fix: pass visible pr..." |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c465b2b3b8
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (finalized === "edited") { | ||
| markActivePreviewComplete(laneName); | ||
| params.onPreviewDelivered?.(text, previewMessageId ?? lane.stream?.messageId()); |
There was a problem hiding this comment.
Don't emit message_sent for regressive finals that skip the edit
finalized === "edited" is broader than a real preview finalization here: tryUpdatePreviewForLane() also returns "edited" from the regressive-skip branch in extensions/telegram/src/lane-delivery-text-deliverer.ts:341-344, where we intentionally keep the longer preview text and never call editPreview. In that case this callback will emit message_sent with the shorter final text even though Telegram is still showing the older preview content (for example when the last partial is "Sure." and the final becomes "Sure"). Any plugin or internal message:sent automation will record content that was not actually sent.
Useful? React with 👍 / 👎.
|
@greptile-apps review |
| if (finalized === "edited") { | ||
| markActivePreviewComplete(laneName); | ||
| params.onPreviewDelivered?.(text, previewMessageId ?? lane.stream?.messageId()); | ||
| return "preview-finalized"; |
There was a problem hiding this comment.
Regressive-skip path passes wrong content to hook
When shouldSkipRegressivePreviewUpdate returns true (i.e., the final text is shorter than the current streamed preview), tryUpdatePreviewForLane returns "edited" without modifying the Telegram message — the longer preview text remains visible. However, onPreviewDelivered is then called with text (the shorter, regressed value), so hook consumers receive content that doesn't match what the user actually sees.
Concretely, the existing test "keeps existing preview when final text regresses" sets lastPartialText = "Recovered final answer." but delivers text = "Recovered final answer" (no period). After this PR, onPreviewDelivered("Recovered final answer", 999) fires even though Telegram still shows "Recovered final answer.".
To align the hook content with the displayed message in this case, consider passing lane.lastPartialText (the actual visible text) instead of text:
// Before emitting, prefer the current preview text if the edit was skipped
// (regressive-skip): the displayed message is the longer preview, not `text`.
params.onPreviewDelivered?.(text, previewMessageId ?? lane.stream?.messageId());
The new onPreviewDelivered tests don't cover this path — adding a case would help catch regressions.
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/telegram/src/lane-delivery-text-deliverer.ts
Line: 528-531
Comment:
**Regressive-skip path passes wrong content to hook**
When `shouldSkipRegressivePreviewUpdate` returns `true` (i.e., the final text is shorter than the current streamed preview), `tryUpdatePreviewForLane` returns `"edited"` without modifying the Telegram message — the longer preview text remains visible. However, `onPreviewDelivered` is then called with `text` (the shorter, regressed value), so hook consumers receive content that doesn't match what the user actually sees.
Concretely, the existing test "keeps existing preview when final text regresses" sets `lastPartialText = "Recovered final answer."` but delivers `text = "Recovered final answer"` (no period). After this PR, `onPreviewDelivered("Recovered final answer", 999)` fires even though Telegram still shows `"Recovered final answer."`.
To align the hook content with the displayed message in this case, consider passing `lane.lastPartialText` (the actual visible text) instead of `text`:
```
// Before emitting, prefer the current preview text if the edit was skipped
// (regressive-skip): the displayed message is the longer preview, not `text`.
params.onPreviewDelivered?.(text, previewMessageId ?? lane.stream?.messageId());
```
The new `onPreviewDelivered` tests don't cover this path — adding a case would help catch regressions.
How can I resolve this? If you propose a fix, please make it concise.|
@greptile-apps review |
|
@greptile-apps review - please review latest commit be0a6cd which addresses the regressive-skip path |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: be0a6cdfd8
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (finalized === "retained") { | ||
| markActivePreviewComplete(laneName); | ||
| // Do not emit onPreviewDelivered for retained paths — the edit | ||
| // failed/was ambiguous, so the visible message may still be stale. | ||
| return "preview-retained"; |
There was a problem hiding this comment.
Emit message_sent for stop-flushed retained previews
When the first visible preview is created during stopDraftLane(), tryEditPreviewMessage() returns "retained" precisely because the final text has already been flushed to Telegram (extensions/telegram/src/lane-delivery-text-deliverer.ts:247-252). This new blanket skip therefore drops message_sent / message:sent hooks for a fully delivered text-only final whenever the follow-up edit fails after that stop flush. A transient post-flush edit failure is enough to reproduce it, so this patch still misses one of the preview-finalization paths it was meant to cover.
Useful? React with 👍 / 👎.
| it("calls onPreviewDelivered when preview is finalized via edit", async () => { | ||
| const harness = createHarness({ answerMessageId: 999 }); | ||
|
|
||
| const result = await deliverFinalAnswer(harness, HELLO_FINAL); | ||
|
|
||
| expect(result).toBe("preview-finalized"); | ||
| expect(harness.onPreviewDelivered).toHaveBeenCalledWith(HELLO_FINAL, 999); | ||
| }); |
There was a problem hiding this comment.
Missing test for
stopCreatesFirstPreview + onPreviewDelivered path
The existing test covers the case where the stream already has a message ID before delivery (answerMessageId: 999), so previewMessageId is captured as a valid number and the ?? lane.stream?.messageId() fallback on lines 536 and 544 of lane-delivery-text-deliverer.ts is never actually exercised.
The path where stop creates the first preview message (i.e., answerMessageIdAfterStop: 777, no initial answerMessageId) is exercised by the existing "primes stop-created previews with final text before editing" test, but that test makes no assertion on onPreviewDelivered. This leaves the previewMessageId ?? lane.stream?.messageId() fallback logic — which was specifically introduced in this PR to surface the message ID for that case — untested.
Consider adding a test similar to the existing "primes stop-created previews with final text before editing" case that also asserts the hook fires with the correct message ID:
it("calls onPreviewDelivered when stop-created first preview is finalized", async () => {
const harness = createHarness({ answerMessageIdAfterStop: 777 });
const result = await harness.deliverLaneText({
laneName: "answer",
text: "Hello final",
payload: { text: "Hello final" },
infoKind: "final",
});
expect(result).toBe("preview-finalized");
expect(harness.onPreviewDelivered).toHaveBeenCalledWith("Hello final", 777);
});Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/telegram/src/lane-delivery.test.ts
Line: 561-568
Comment:
**Missing test for `stopCreatesFirstPreview` + `onPreviewDelivered` path**
The existing test covers the case where the stream already has a message ID before delivery (`answerMessageId: 999`), so `previewMessageId` is captured as a valid number and the `?? lane.stream?.messageId()` fallback on lines 536 and 544 of `lane-delivery-text-deliverer.ts` is never actually exercised.
The path where stop creates the _first_ preview message (i.e., `answerMessageIdAfterStop: 777`, no initial `answerMessageId`) is exercised by the existing "primes stop-created previews with final text before editing" test, but that test makes no assertion on `onPreviewDelivered`. This leaves the `previewMessageId ?? lane.stream?.messageId()` fallback logic — which was specifically introduced in this PR to surface the message ID for that case — untested.
Consider adding a test similar to the existing "primes stop-created previews with final text before editing" case that also asserts the hook fires with the correct message ID:
```ts
it("calls onPreviewDelivered when stop-created first preview is finalized", async () => {
const harness = createHarness({ answerMessageIdAfterStop: 777 });
const result = await harness.deliverLaneText({
laneName: "answer",
text: "Hello final",
payload: { text: "Hello final" },
infoKind: "final",
});
expect(result).toBe("preview-finalized");
expect(harness.onPreviewDelivered).toHaveBeenCalledWith("Hello final", 777);
});
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fe4934e781
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| onPreviewDelivered: (content, messageId) => { | ||
| const hookRunner = getGlobalHookRunner(); | ||
| const hasMessageSentHooks = hookRunner?.hasHooks("message_sent") ?? false; | ||
| emitMessageSentHooks({ |
There was a problem hiding this comment.
Run message_sending hooks before emitting preview message_sent
onPreviewDelivered() emits message_sent/message:sent directly here, but deliverReplies() is still the only Telegram path that runs runMessageSending() and updates or cancels the outgoing content (extensions/telegram/src/bot/delivery.replies.ts:607-633). In deployments that use a message_sending plugin for Telegram, any preview-finalized reply will now report success with the raw preview text even though the sending hook never had a chance to rewrite or veto it, so hook consumers can observe a message that was never processed by the normal outbound pipeline.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 67e7786239
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (info.kind === "final") { | ||
| emitPreviewFinalizedHook(result); |
There was a problem hiding this comment.
Emit preview-finalized hooks for buffered answer finals
When a final payload is split into reasoning plus answer text, the answer segment is buffered at extensions/telegram/src/bot-message-dispatch.ts:609-618 and later sent via flushBufferedFinalAnswer() at extensions/telegram/src/bot-message-dispatch.ts:588-605. This change only emits the new preview-finalized hook on the direct deliverLaneText() result in the main loop, so buffered finals that stay in the preview still never fire message:sent. That means mixed reasoning/answer replies (for example think-tag finals) still miss the hook for the actual answer message, which is the main preview-finalization case this patch was supposed to cover.
Useful? React with 👍 / 👎.
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
67e7786 to
51b3e03
Compare
|
Landed on main. Thanks @bugkill3r. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 51b3e034eb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| emitInternalMessageSentHook({ | ||
| sessionKeyForInternalHooks: deliveryBaseOptions.sessionKeyForInternalHooks, | ||
| chatId: deliveryBaseOptions.chatId, | ||
| accountId: deliveryBaseOptions.accountId, | ||
| content: result.delivery.content, |
There was a problem hiding this comment.
Emit plugin message_sent hooks for preview-finalized replies
When a Telegram text-only final is delivered by finalizing the streaming preview, emitPreviewFinalizedHook() only calls emitInternalMessageSentHook(). The normal send path in extensions/telegram/src/bot/delivery.replies.ts invokes runMessageSent(...) for plugin message_sent hooks after each delivered reply, so preview-finalized replies still never reach plugin-based automations or telemetry. This is especially visible when there is no SessionKey, because the new path becomes a total no-op instead of matching normal delivery behavior.
Useful? React with 👍 / 👎.
openclaw#50917) * fix: emit message:sent hook on Telegram streaming preview finalization * fix: include messageId in preview-delivered hook callback * fix: skip message:sent hook for preview-retained paths * fix: correct JSDoc for onPreviewDelivered callback * fix: pass visible preview text on regressive-skip path * fix: remove dead fallbacks and add stopCreatesFirstPreview test * Update extensions/telegram/src/lane-delivery-text-deliverer.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * fix: align telegram preview sent hooks (openclaw#50917) (thanks @bugkill3r) --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: Ayaan Zaidi <[email protected]>
openclaw#50917) * fix: emit message:sent hook on Telegram streaming preview finalization * fix: include messageId in preview-delivered hook callback * fix: skip message:sent hook for preview-retained paths * fix: correct JSDoc for onPreviewDelivered callback * fix: pass visible preview text on regressive-skip path * fix: remove dead fallbacks and add stopCreatesFirstPreview test * Update extensions/telegram/src/lane-delivery-text-deliverer.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * fix: align telegram preview sent hooks (openclaw#50917) (thanks @bugkill3r) --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: Ayaan Zaidi <[email protected]>
openclaw#50917) * fix: emit message:sent hook on Telegram streaming preview finalization * fix: include messageId in preview-delivered hook callback * fix: skip message:sent hook for preview-retained paths * fix: correct JSDoc for onPreviewDelivered callback * fix: pass visible preview text on regressive-skip path * fix: remove dead fallbacks and add stopCreatesFirstPreview test * Update extensions/telegram/src/lane-delivery-text-deliverer.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * fix: align telegram preview sent hooks (openclaw#50917) (thanks @bugkill3r) --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: Ayaan Zaidi <[email protected]>
openclaw#50917) * fix: emit message:sent hook on Telegram streaming preview finalization * fix: include messageId in preview-delivered hook callback * fix: skip message:sent hook for preview-retained paths * fix: correct JSDoc for onPreviewDelivered callback * fix: pass visible preview text on regressive-skip path * fix: remove dead fallbacks and add stopCreatesFirstPreview test * Update extensions/telegram/src/lane-delivery-text-deliverer.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * fix: align telegram preview sent hooks (openclaw#50917) (thanks @bugkill3r) --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: Ayaan Zaidi <[email protected]>
openclaw#50917) * fix: emit message:sent hook on Telegram streaming preview finalization * fix: include messageId in preview-delivered hook callback * fix: skip message:sent hook for preview-retained paths * fix: correct JSDoc for onPreviewDelivered callback * fix: pass visible preview text on regressive-skip path * fix: remove dead fallbacks and add stopCreatesFirstPreview test * Update extensions/telegram/src/lane-delivery-text-deliverer.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * fix: align telegram preview sent hooks (openclaw#50917) (thanks @bugkill3r) --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: Ayaan Zaidi <[email protected]> (cherry picked from commit 4266e26) # Conflicts: # extensions/telegram/src/bot-message-dispatch.test.ts # extensions/telegram/src/lane-delivery.test.ts
openclaw#50917) * fix: emit message:sent hook on Telegram streaming preview finalization * fix: include messageId in preview-delivered hook callback * fix: skip message:sent hook for preview-retained paths * fix: correct JSDoc for onPreviewDelivered callback * fix: pass visible preview text on regressive-skip path * fix: remove dead fallbacks and add stopCreatesFirstPreview test * Update extensions/telegram/src/lane-delivery-text-deliverer.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * fix: align telegram preview sent hooks (openclaw#50917) (thanks @bugkill3r) --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: Ayaan Zaidi <[email protected]> (cherry picked from commit 4266e26)
Summary
message:senthook when Telegram streaming preview is finalized, not just onsendPayloadpathmessageIdto the preview-delivered callback for parity with normal deliverypreview-retainedpaths where delivery is uncertainCloses #50878