Fix cron text announce delivery for Telegram targets#40575
Conversation
🔒 Aisle Security AnalysisWe found 3 potential security issue(s) in this PR:
1. 🟡 Sensitive information leakage via unredacted direct-cron delivery error logging
Description
Vulnerable code: function summarizeDirectCronDeliveryError(error: unknown): string {
if (error instanceof Error) {
return error.message || "error";
}
if (typeof error === "string") {
return error;
}
try {
return JSON.stringify(error) || String(error);
} catch {
return String(error);
}
}
...
logWarn(
`[cron:${params.jobId}] transient direct announce delivery failure, retrying ...: ${summarizeDirectCronDeliveryError(err)}`,
);RecommendationRedact and minimize error details before logging.
Example fix: import { formatErrorMessage } from "../../infra/errors.js";
function summarizeDirectCronDeliveryError(error: unknown): string {
// Produces a short message and applies best-effort secret redaction.
return formatErrorMessage(error);
}
// Optionally: cap length to prevent huge logs
const detail = summarizeDirectCronDeliveryError(err).slice(0, 500);
logWarn(`[cron:${jobId}] transient direct delivery failure, retrying ...: ${detail}`);Additionally, consider logging structured fields (error code, channel) separately and omitting recipient identifiers ( 2. 🟡 Non-idempotent transient retry in cron direct delivery can duplicate outbound messages
Description
Why this is risky:
Impact:
Vulnerable code (retry loop): } catch (err) {
const delayMs = retryDelaysMs[retryIndex];
if (delayMs == null || !isTransientDirectCronDeliveryError(err) || params.signal?.aborted) {
throw err;
}
retryIndex += 1;
await sleepWithAbort(delayMs, params.signal);
}Transient patterns include non-idempotent network failures: /\b(econnreset|econnrefused|etimedout|enotfound|ehostunreach|network error)\b/iThe retry is enabled for text delivery:
RecommendationMake retries safe for non-idempotent outbound sends. Options (preferred first):
Concrete minimal change example (tighten transient matching): const TRANSIENT_DIRECT_CRON_DELIVERY_ERROR_PATTERNS: readonly RegExp[] = [
/no active .* listener/i,
/gateway not connected/i,
/gateway closed \(1006/i,
/gateway timeout/i,
// Only pre-connect style errors (avoid etimedout/econnreset):
/\b(econnrefused|enotfound|ehostunreach)\b/i,
];Or implement a channel-aware predicate: function isSafeToRetryDirectSend(err: unknown, channel: string): boolean {
if (channel === "telegram") return isSafeToRetrySendError(err);
// others: implement similarly or default false
return false;
}3. 🟡 Cron text-only direct delivery bypasses announce-flow suppression (ANNOUNCE_SKIP / NO_REPLY decision layer)
DescriptionThe cron delivery dispatcher was changed to send text-only cron output via direct outbound adapters ( This introduces a suppression/consent bypass risk:
Vulnerable code (new behavior): if (synthesizedText.toUpperCase() === SILENT_REPLY_TOKEN.toUpperCase()) {
return params.withRunSession({ status: "ok", summary, outputText, delivered: true, ... });
}
...
return await deliverViaDirect(delivery, { retryTransient: true });This is especially impactful because the change explicitly shifts text-only cron delivery away from internal announce routing (which could be suppressed) to unconditional direct sending. RecommendationDecide explicitly whether cron text delivery should honor the same suppression/consent decisions as the announce flow. Options:
import { isAnnounceSkip } from "../../agents/tools/sessions-send-helpers.js";
import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../../auto-reply/tokens.js";
const text = synthesizedText?.trim() ?? "";
if (isAnnounceSkip(text) || isSilentReplyText(text, SILENT_REPLY_TOKEN)) {
return params.withRunSession({ status: "ok", summary, outputText, delivered: true, ...params.telemetry });
}
Analyzed PR: #40575 at commit Last updated on: 2026-03-09T06:05:04Z |
Greptile SummaryThis PR fixes a silent correctness bug where cron text-only Core logic is sound. All early-return guard paths ( Test coverage is nearly complete, but one edge case is untested: text-only delivery with Confidence Score: 4/5
Last reviewed commit: 305fa38 |
...cron/isolated-agent.skips-delivery-without-whatsapp-recipient-besteffortdeliver-true.test.ts
Show resolved
Hide resolved
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 305fa38f9a
ℹ️ 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".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c1aa31b0bb
ℹ️ 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".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 539e9e2ed0
ℹ️ 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".
| const deliveryResults = options?.retryTransient | ||
| ? await retryTransientDirectCronDelivery({ | ||
| jobId: params.job.id, | ||
| signal: params.abortSignal, | ||
| run: runDelivery, | ||
| }) |
There was a problem hiding this comment.
Add idempotency guard before retrying direct cron sends
The new retry branch replays deliverOutboundPayloads for transient errors, but this direct send path does not carry any idempotency key, so a timeout/reset that occurs after the first provider send can cause the same cron text to be delivered twice on retry. This is most likely under the exact transient conditions matched here (gateway timeout, econnreset, UNAVAILABLE), and it regresses the old announce flow behavior that routed through an idempotent announce key before retrying.
Useful? React with 👍 / 👎.
539e9e2 to
54b1513
Compare
|
Merged via squash.
Thanks @obviyus! |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 54b1513c78
ℹ️ 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".
| } | ||
| return null; | ||
| try { | ||
| return await deliverViaDirect(delivery, { retryTransient: true }); |
There was a problem hiding this comment.
Suppress ANNOUNCE_SKIP before direct cron delivery
Text-only announce now flows directly to deliverViaDirect here unless the synthesized text matches SILENT_REPLY_TOKEN, so a run that returns ANNOUNCE_SKIP will be delivered verbatim to the channel. The previous implementation routed this path through runSubagentAnnounceFlow, which explicitly suppresses ANNOUNCE_SKIP via isAnnounceSkip(...) in src/agents/subagent-announce.ts, so this change introduces a control-token leak whenever that skip marker is emitted (for example by session-send helper prompts).
Useful? React with 👍 / 👎.
Merged via squash. Prepared head SHA: 54b1513 Co-authored-by: obviyus <[email protected]> Co-authored-by: obviyus <[email protected]> Reviewed-by: @obviyus
Merged via squash. Prepared head SHA: 54b1513 Co-authored-by: obviyus <[email protected]> Co-authored-by: obviyus <[email protected]> Reviewed-by: @obviyus
Merged via squash. Prepared head SHA: 54b1513 Co-authored-by: obviyus <[email protected]> Co-authored-by: obviyus <[email protected]> Reviewed-by: @obviyus
Merged via squash. Prepared head SHA: 54b1513 Co-authored-by: obviyus <[email protected]> Co-authored-by: obviyus <[email protected]> Reviewed-by: @obviyus
Merged via squash. Prepared head SHA: 54b1513 Co-authored-by: obviyus <[email protected]> Co-authored-by: obviyus <[email protected]> Reviewed-by: @obviyus
Merged via squash. Prepared head SHA: 54b1513 Co-authored-by: obviyus <[email protected]> Co-authored-by: obviyus <[email protected]> Reviewed-by: @obviyus
Merged via squash. Prepared head SHA: 54b1513 Co-authored-by: obviyus <[email protected]> Co-authored-by: obviyus <[email protected]> Reviewed-by: @obviyus
Merged via squash. Prepared head SHA: 54b1513 Co-authored-by: obviyus <[email protected]> Co-authored-by: obviyus <[email protected]> Reviewed-by: @obviyus
Summary
delivery.mode="announce"text-only targets were finalized through internal announce routing instead of the real outbound adapters.delivered: trueeven when no Telegram message was actually sent.deliverOutboundPayloads; updated cron delivery regressions and added a text-send failure regression.Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
User-visible / Behavior Changes
Security Impact (required)
Yes/No) NoYes/No) NoYes/No) NoYes/No) NoYes/No) NoYes, explain risk + mitigation:Repro + Verification
Environment
delivery.mode="announce",channel="telegram", explicittotargetSteps
agentTurnjob withdelivery.mode="announce",channel:"telegram",to:"123".bestEffort: true.Expected
delivered: true.delivered: false.Actual
delivered: true.Evidence
Attach at least one:
Human Verification (required)
What you personally verified (not just CI), and how:
channel:"last"Telegram target, forum topic target, descendant-output finalization, best-effort text-send failure.Review Conversations
If a bot review conversation is addressed by this PR, resolve that conversation yourself. Do not leave bot review conversation cleanup for maintainers.
Compatibility / Migration
Yes/No) YesYes/No) NoYes/No) NoFailure Recovery (if this breaks)
3ea6f855b.src/cron/isolated-agent/delivery-dispatch.tsRisks and Mitigations
List only real risks for this PR. Add/remove entries as needed. If none, write
None.channel:"last", forum topics, descendant output consolidation, best-effort failure accounting, and no-double-deliver guard behavior.