fix: keep err.stack in sync when pad.check() adds context - #8138
fix: keep err.stack in sync when pad.check() adds context#8138JohnMcLear wants to merge 1 commit into
Conversation
|
ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing |
PR Summary by QodoFix Pad.check context so err.stack includes pad/revision details
AI Description
Diagram
High-Level Assessment
Files changed (2)
|
Code Review by Qodo
1. Cleanup assumes contiguous revisions
|
| } catch (err:any) { | ||
| err.message = `(pad ${this.id} revision ${r}) ${err.message}`; | ||
| throw err; | ||
| throw addErrorContext(err, `(pad ${this.id} revision ${r})`); | ||
| } |
There was a problem hiding this comment.
1. Cleanup assumes contiguous revisions 📎 Requirement gap ☼ Reliability
deleteRevisions() computes a contiguous revision range to load and delete, but it does not tolerate missing revision numbers, so gaps can cause it to throw and abort rather than reliably retaining the requested keepRevisions. Because it also calls pad.check() and propagates its assertion failures when revision metadata is missing, the cleanup cannot complete in a controlled manner as required.
Agent Prompt
## Issue description
Cleanup revision deletion/retention is not gap-tolerant: `deleteRevisions()` iterates over a computed contiguous revision range and can throw when a revision record is missing, and the admin cleanup flow also calls `pad.check()` which asserts on missing timestamps, causing the cleanup operation to abort. Update the cleanup logic so it can complete in a controlled manner and still enforce (or clearly define) `keepRevisions` even when intermediate revision entries are missing.
## Issue Context
Compliance requirements (PR Compliance IDs 1 and 2) state that cleanup must not abort due to missing `pad:<id>:revs:<n>` records and must support retaining `X` revisions despite discontinuities in revision numbering. The current behavior fails these requirements because gaps lead to thrown errors during `pad.getRevision(rev)` iteration and/or assertion failures from `pad.check()`.
## Fix Focus Areas
- src/node/utils/Cleanup.ts[43-69]
- src/node/utils/Cleanup.ts[52-69]
- src/node/utils/Cleanup.ts[65-68]
- src/node/db/Pad.ts[998-1000]
- src/node/db/Pad.ts[1004-1012]
- src/tests/backend/specs/padCheckErrorContext.ts[83-92]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| if (oldMessage && typeof err.stack === 'string' && err.stack.includes(oldMessage)) { | ||
| err.stack = err.stack.replace(oldMessage, err.message); | ||
| } |
There was a problem hiding this comment.
2. Stack context can duplicate 🐞 Bug ◔ Observability
addErrorContext() always replaces oldMessage inside err.stack even if err.stack is generated after err.message is updated (lazy stack formatting), which can cause the context prefix to be duplicated in the stack header. This risks corrupting/duplicating the most important diagnostic line for errors thrown during pad.check() (including DB read failures).
Agent Prompt
### Issue description
`addErrorContext()` sets `err.message` and then conditionally does `err.stack.replace(oldMessage, err.message)` based on `err.stack.includes(oldMessage)`. If `err.stack` is lazily rendered (common), the first access inside `addErrorContext()` can already include the *new* prefixed message, which still contains `oldMessage` as a substring, so the replace duplicates the prefix.
### Issue Context
This helper is used in `Pad.check()` to add pad/revision/chat context to errors coming from multiple sources (assertions and DB reads). We want to update the stack header **only when the stack still reflects the old message**.
### Fix Focus Areas
- src/node/db/Pad.ts[54-72]
Suggested approach:
- After updating `err.message`, only run the `replace(oldMessage, err.message)` when the stack does **not** already contain the new message, for example:
- `if (oldMessage && typeof err.stack === 'string' && err.stack.includes(oldMessage) && !err.stack.includes(err.message)) { ... }`
- (Optional) Consider capturing the current `err.stack` into a local before modifying `err.stack` to avoid multiple getter evaluations.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
pad.check() prefixes failures with `(pad <id> revision <n>)` so admins know which record is bad, but it only assigned to `err.message`. `err.stack` is rendered from the message when the error is constructed, so the stack kept showing the original, context-free text. Both places that report a failed check log `err.stack` -- Cleanup .checkTodos and the admin `cleanupPadRevisions` handler -- so the revision number never reached the log. The reporter on #8134 had to bisect their database by hand to find the missing revision: [ERROR] adminSettings - Error in pad eu-<id>: AssertionError: The expression evaluated to a falsy value: assert(timestamp != null) at Pad.check (src/node/db/Pad.ts:969:9) Fix it at the source with an addErrorContext() helper so every reporter of a check() failure benefits, including plugins that log err.stack. Refs #8134 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
c9335ff to
d744bd3
Compare
Problem
pad.check()carefully prefixes failures with(pad <id> revision <n>)so an admin knows which record is bad — but it only assigns toerr.message.err.stackis rendered from the message when the error is constructed, so the stack keeps showing the original, context-free text.Both places that report a failed check log
err.stack:Cleanup.checkTodos()—logger.error(\Error in pad ${padId}: ${err.stack || err}`)`cleanupPadRevisionssocket handler — same shape…so the revision number never reaches the log. The reporter on #8134 had to bisect their database by hand to work out which revision was missing:
Nothing there says revision 600, even though
check()knew.Fix
Fix it at the source rather than at each log site: an
addErrorContext()helper that prefixes the message and rewrites the matching text inerr.stack. All three context-adding sites inPad.check()(revision load, revision replay, chat message) now go through it, so every reporter of acheck()failure benefits — including plugins that logerr.stackthemselves.The helper replaces only the first occurrence (the stack's header line) and no-ops on an empty message, which would otherwise match at offset 0 and corrupt the stack.
Tests
src/tests/backend/specs/padCheckErrorContext.tsbuilds a pad in exactly the shape from #8134 —headpointing past a revision whosepad:<id>:revs:<n>record is absent — and asserts the context reacheserr.stack, isn't duplicated, and that the original assertion text and stack frames survive. Also covers the chat-message path and thedeleteRevisions()entry point from the issue report.Verified red→green: 4 of the 7 fail without the
Pad.tschange, all 7 pass with it. Full backend suite: 1628 passing, 0 failing.Scope
Diagnostics only — this does not repair pads with a missing revision, and does not address how the hole forms. Both are tracked separately off #8134.
Refs #8134
🤖 Generated with Claude Code