fix: address delivery dedupe review follow-ups#44666
Conversation
🔒 Aisle Security AnalysisWe found 2 potential security issue(s) in this PR:
1. 🟠 Client-controlled idempotencyKey can suppress mirrored transcript entries (audit/integrity bypass)
Description
Trust boundary problem:
Impact:
Vulnerable code: if (
params.idempotencyKey &&
(await transcriptHasIdempotencyKey(sessionFile, params.idempotencyKey))
) {
return { ok: true, sessionFile };
}RecommendationDo not use a client-controlled idempotency key as the authority for transcript de-duplication. Mitigations (pick one or combine):
Example (server-side key): // When you have a stable delivery identifier:
const transcriptIdempotencyKey = `mirror:v1:${sessionKey}:${result.messageId}`;
await appendAssistantMessageToSessionTranscript({
sessionKey,
text: mirrorText,
idempotencyKey: transcriptIdempotencyKey,
});And enforce bounds in the gateway schema, e.g.: const IdempotencyKey = Type.String({ minLength: 1, maxLength: 128, pattern: "^[a-zA-Z0-9:_-]+$" });2. 🟠 Unbounded transcript scan for idempotency key causes O(n) per append (DoS risk)
Description
Impact:
This is reachable in normal operation because outbound delivery mirroring passes an idempotency key through to transcript appends, and gateway callers can supply idempotency keys as arbitrary non-empty strings. Vulnerable code: const raw = await fs.promises.readFile(transcriptPath, "utf-8");
for (const line of raw.split(/\r?\n/)) {
...
const parsed = JSON.parse(line);
...
}RecommendationAvoid full-file reads/parses on the hot path. Recommended mitigations (choose one or combine):
import readline from "node:readline";
async function transcriptHasIdempotencyKey(transcriptPath: string, key: string): Promise<boolean> {
const stream = fs.createReadStream(transcriptPath, { encoding: "utf-8" });
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
for await (const line of rl) {
if (!line.trim()) continue;
try {
const parsed = JSON.parse(line) as { message?: { idempotencyKey?: unknown } };
if (parsed.message?.idempotencyKey === key) return true;
} catch {
// optionally count/log bad lines
}
}
return false;
}
Analyzed PR: #44666 at commit Last updated on: 2026-03-13T09:16:00Z |
Greptile SummaryThis PR addresses two follow-up issues from a delivery deduplication review: it fixes cache pruning for completed direct cron deliveries so the 2 000-entry cap is reliably enforced, and it makes the transcript mirror idempotency scan async and tolerant of malformed JSONL lines. It also consolidates previously duplicated mirror-payload type definitions into a single Key changes:
Confidence Score: 4/5
Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/cron/isolated-agent/delivery-dispatch.ts
Line: 333-337
Comment:
**Cloned results allocated but immediately discarded**
`getCompletedDirectCronDelivery` deep-clones the cached `OutboundDeliveryResult[]` via `cloneDeliveryResults`, but the returned value is only inspected for truthiness at the call site — the actual array is never read. This allocates a fresh array + object spread on every cache hit for no benefit.
Consider returning a simple boolean from a helper, or refactoring `getCompletedDirectCronDelivery` to return `boolean` when the caller only needs existence:
```suggestion
const isCachedDelivery = (() => {
pruneCompletedDirectCronDeliveries(Date.now());
return COMPLETED_DIRECT_CRON_DELIVERIES.has(deliveryIdempotencyKey);
})();
if (isCachedDelivery) {
// Cached entries are only recorded after a successful non-empty delivery.
delivered = true;
return null;
}
```
Or, more minimally, just inline:
```ts
if (getCompletedDirectCronDelivery(deliveryIdempotencyKey)) {
delivered = true;
return null;
}
```
and have `getCompletedDirectCronDelivery` return `boolean` instead of `OutboundDeliveryResult[] | undefined`.
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: src/config/sessions/transcript.ts
Line: 183-188
Comment:
**TOCTOU window between idempotency check and append**
The `transcriptHasIdempotencyKey` read and the `sessionManager.appendMessage` call are not atomic. Two concurrent invocations with the same `idempotencyKey` could both read the file, both find no match, and both append — defeating the deduplication. This is an inherent limitation of the file-based approach without a write lock.
If the transcript write path already holds an exclusive advisory lock (e.g. in `SessionManager.open`), this is fine and can be documented with a comment. If it does not, a note explaining that this is best-effort deduplication would make the intent clearer for future contributors.
How can I resolve this? If you propose a fix, please make it concise.Last reviewed commit: 5baa548 |
| const cachedResults = getCompletedDirectCronDelivery(deliveryIdempotencyKey); | ||
| if (cachedResults) { | ||
| // Cached entries are only recorded after a successful non-empty delivery. | ||
| delivered = true; | ||
| return null; |
There was a problem hiding this comment.
Cloned results allocated but immediately discarded
getCompletedDirectCronDelivery deep-clones the cached OutboundDeliveryResult[] via cloneDeliveryResults, but the returned value is only inspected for truthiness at the call site — the actual array is never read. This allocates a fresh array + object spread on every cache hit for no benefit.
Consider returning a simple boolean from a helper, or refactoring getCompletedDirectCronDelivery to return boolean when the caller only needs existence:
| const cachedResults = getCompletedDirectCronDelivery(deliveryIdempotencyKey); | |
| if (cachedResults) { | |
| // Cached entries are only recorded after a successful non-empty delivery. | |
| delivered = true; | |
| return null; | |
| const isCachedDelivery = (() => { | |
| pruneCompletedDirectCronDeliveries(Date.now()); | |
| return COMPLETED_DIRECT_CRON_DELIVERIES.has(deliveryIdempotencyKey); | |
| })(); | |
| if (isCachedDelivery) { | |
| // Cached entries are only recorded after a successful non-empty delivery. | |
| delivered = true; | |
| return null; | |
| } |
Or, more minimally, just inline:
if (getCompletedDirectCronDelivery(deliveryIdempotencyKey)) {
delivered = true;
return null;
}and have getCompletedDirectCronDelivery return boolean instead of OutboundDeliveryResult[] | undefined.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/cron/isolated-agent/delivery-dispatch.ts
Line: 333-337
Comment:
**Cloned results allocated but immediately discarded**
`getCompletedDirectCronDelivery` deep-clones the cached `OutboundDeliveryResult[]` via `cloneDeliveryResults`, but the returned value is only inspected for truthiness at the call site — the actual array is never read. This allocates a fresh array + object spread on every cache hit for no benefit.
Consider returning a simple boolean from a helper, or refactoring `getCompletedDirectCronDelivery` to return `boolean` when the caller only needs existence:
```suggestion
const isCachedDelivery = (() => {
pruneCompletedDirectCronDeliveries(Date.now());
return COMPLETED_DIRECT_CRON_DELIVERIES.has(deliveryIdempotencyKey);
})();
if (isCachedDelivery) {
// Cached entries are only recorded after a successful non-empty delivery.
delivered = true;
return null;
}
```
Or, more minimally, just inline:
```ts
if (getCompletedDirectCronDelivery(deliveryIdempotencyKey)) {
delivered = true;
return null;
}
```
and have `getCompletedDirectCronDelivery` return `boolean` instead of `OutboundDeliveryResult[] | undefined`.
How can I resolve this? If you propose a fix, please make it concise.| if ( | ||
| params.idempotencyKey && | ||
| (await transcriptHasIdempotencyKey(sessionFile, params.idempotencyKey)) | ||
| ) { | ||
| return { ok: true, sessionFile }; | ||
| } |
There was a problem hiding this comment.
TOCTOU window between idempotency check and append
The transcriptHasIdempotencyKey read and the sessionManager.appendMessage call are not atomic. Two concurrent invocations with the same idempotencyKey could both read the file, both find no match, and both append — defeating the deduplication. This is an inherent limitation of the file-based approach without a write lock.
If the transcript write path already holds an exclusive advisory lock (e.g. in SessionManager.open), this is fine and can be documented with a comment. If it does not, a note explaining that this is best-effort deduplication would make the intent clearer for future contributors.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/config/sessions/transcript.ts
Line: 183-188
Comment:
**TOCTOU window between idempotency check and append**
The `transcriptHasIdempotencyKey` read and the `sessionManager.appendMessage` call are not atomic. Two concurrent invocations with the same `idempotencyKey` could both read the file, both find no match, and both append — defeating the deduplication. This is an inherent limitation of the file-based approach without a write lock.
If the transcript write path already holds an exclusive advisory lock (e.g. in `SessionManager.open`), this is fine and can be documented with a comment. If it does not, a note explaining that this is best-effort deduplication would make the intent clearer for future contributors.
How can I resolve this? If you propose a fix, please make it concise.b9bbd2f to
90b5940
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 415a8a7382
ℹ️ 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".
| Text("Pairing Required", style = onboardingHeadlineStyle, color = onboardingWarning) | ||
| Text("Run these on your gateway host:", style = onboardingCalloutStyle, color = onboardingTextSecondary) | ||
| } |
There was a problem hiding this comment.
Show actual connection failure instead of generic pairing prompt
This branch now treats every attempted-but-disconnected state as "Pairing Required," but statusText is no longer surfaced in FinalStep, so users lose the real failure reason. The runtime reports non-pairing errors such as Failed: invalid manual host/port and TLS probe failures in apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt (e.g. around lines 788 and 732), so this UI change can direct users to run pairing commands when the fix is actually endpoint or TLS configuration.
Useful? React with 👍 / 👎.
415a8a7 to
2f17f1a
Compare
2f17f1a to
8e6d254
Compare
Merged via squash. Prepared head SHA: 8e6d254 Co-authored-by: frankekn <[email protected]> Co-authored-by: frankekn <[email protected]> Reviewed-by: @frankekn
Merged via squash. Prepared head SHA: 8e6d254 Co-authored-by: frankekn <[email protected]> Co-authored-by: frankekn <[email protected]> Reviewed-by: @frankekn
Merged via squash. Prepared head SHA: 8e6d254 Co-authored-by: frankekn <[email protected]> Co-authored-by: frankekn <[email protected]> Reviewed-by: @frankekn
Merged via squash. Prepared head SHA: 8e6d254 Co-authored-by: frankekn <[email protected]> Co-authored-by: frankekn <[email protected]> Reviewed-by: @frankekn
Summary
Context
This replaces the closed #44109 follow-up branch with a fresh replacement PR.
Testing