Skip to content

🧷 fix: Finalize Stopped Compactions and Verify Their Anchor - #16551

Open
berry-13 wants to merge 23 commits into
followup/33-follow-up-a-cancelled-compaction-persists-withoufrom
followup/33-compaction-abort-finalize
Open

berry-13 wants to merge 23 commits into
followup/33-follow-up-a-cancelled-compaction-persists-withoufrom
followup/33-compaction-abort-finalize

Conversation

@berry-13

@berry-13 berry-13 commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

Pull Request

Summary

Related to berry-13#33. Stacked on #16436, which marks a stopped manual compaction with its identity; review that one first.

With only the identity marking in place, a compaction's disconnect snapshot could become the turn's final row: when the run errored afterwards, saveErrorTurn returned early on the existing partial row and never applied the terminal outcome, so the row kept its live unfinished shape. The abort route also skipped the anchor upsert unconditionally, so a Stop that won before the branch loaded wrote a response parented on a row that was never persisted, and then published a normal FINAL for it.

This moves the stopped turn's persistence into one planAbortedTurnPersistence operation in packages/api, which verifies the compaction anchor with an id-only read, withholds the final (publishing a reconciliation frame) only when a row needed writing and could not be anchored, and keeps checkpoint cleanup running when the anchor read fails. The failed-turn settlement also moves into the compaction module: an existing snapshot is finalized with the typed failure and error envelope, falsy writes are rejected, an anchor-shaped error id is never mistaken for the anchor, and snapshots are not written over a settled job.

Type of change

  • Bug fix

Testing

Tested environments/configuration: jest for the unit, controller and route suites; MongoDB and the in-memory job store.

Automated tests:

  • packages/api: npx jest src/agents/compaction.spec.ts src/stream/__tests__/abortCompactionIdentity.spec.ts plus src/stream/__tests__/abortCompactionIdentity.spec.ts src/stream/__tests__/RedisJobStore.spec.ts: 130 passed
  • npx tsc --noEmit -p packages/api/tsconfig.json: clean
  • api: npx jest on the abort route, request partial disconnect and resume metadata suites: 194 passed

Screenshots / recordings

No user-facing change.

Risk / compatibility

Ordinary (non-compaction) aborts keep the anchor upsert and their FINAL. The abort route adds one id-only anchor read for compaction jobs.

Checklist

  • I reviewed my own changes
  • Relevant tests have been added or updated
  • Existing relevant tests pass
  • The change does not introduce new warnings or errors
  • Required documentation PR: N/A

@berry-13
berry-13 added this pull request to stack #16567 September 30, 2026 13:14
@codegraph-librechat codegraph-librechat Bot added the 🗺️ Backend Platform codegraph: the taxonomy area this belongs to (classifier, confidence ≥ 0.9) label Oct 1, 2026
Copilot AI balanced review requested due to automatic review settings October 2, 2026 09:10
@berry-13
berry-13 force-pushed the followup/33-compaction-abort-finalize branch from f0336da to fbaf453 Compare October 2, 2026 09:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@berry-13
berry-13 marked this pull request as ready for review October 2, 2026 09:10
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-02T10:06:39.096838Z 170672d New commits
🔒 Security Review ✅ Completed 2026-10-02T09:16:01.670596Z fbaf453 Draft marked ready
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fbaf453a9b

ℹ️ 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".

jobData?.userMessage?.messageId &&
jobData?.responseMessageId &&
shouldPersistAbortedTurn
abortPersistencePlan.writeResponseRow

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Persist stopped compactions as settled

When a compaction with a persisted anchor is stopped, this branch writes the response with unfinished: true at line 821. Since abortJob has already won terminal ownership, the request controller exits without a later final save, so the row remains unfinished after reload and the new E2E assertion in compaction-abort-finalize.spec.ts:102 cannot pass. Persist compaction aborts with a settled envelope while retaining the unfinished behavior for ordinary stopped replies.

AGENTS.md reference: AGENTS.md:L49-L52

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c95e57e. The abort plan now carries responseUnfinished (false for a compaction, true for an ordinary turn) and the route writes it. The e2e scenario stopped-compaction-settles-under-its-anchor reproduced this on fbaf453 (stored row unfinished: true on all three projects) and passes on 170672d; stopped-reply-keeps-turn-and-partial-answer covers the ordinary path.

Comment thread api/server/routes/agents/index.js Outdated
Comment on lines +775 to +781
const abortPersistencePlan = planAbortedTurnPersistence(
await resolveAbortedTurnAnchorDecision(jobData, {
messageExists: (messageId, conversationId) =>
getMessages({ user: req?.user?.id, messageId, conversationId }, '_id').then(
(rows) => rows.length > 0,
),
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Move the anchor decision out of legacy CJS

This adds backend behavior directly to the legacy route: it performs the anchor database read, interprets the result, and converts it into terminal-publication failure state. The repository boundary requires branches and service calls added under /api to live in packages/api, with the CJS file limited to route wiring and a call into the TypeScript module; move this DB-backed orchestration behind an injected TypeScript service rather than splitting the invariant across both layers.

AGENTS.md reference: AGENTS.md:L77-L81

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c95e57e. resolveAbortedTurnPersistence in packages/api now takes the injected getMessages, does the id-only anchor read, plans the rows and returns the persistence errors; the route makes one call and spreads persistenceErrors.

Comment on lines +484 to +485
if (coveredByExistingRow) {
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Announce the finalized partial error row

When a disconnected compaction has already saved a partial row and generation then fails, settleExistingRowsBeforeErrorTurn finalizes that row and this early return skips both saveConvo and announceErrorTurn below. Consequently lastResponseAt is not stamped and another device has no notification that the persisted turn ended, even though the ordinary error-row path explicitly performs that announcement. Announce the finalized live row before returning so failure and restored-session behavior remain consistent.

AGENTS.md reference: AGENTS.md:L49-L52

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c95e57e. settleExistingRowsBeforeErrorTurn takes an injected announceSettledTurn and calls it only when it actually finalized the live row; request.js passes announceErrorTurn with stampConvoLastResponse. compaction.spec.ts covers both the announce and the no-write case.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 170672dff0

ℹ️ 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".

Comment thread api/server/routes/agents/index.js
Comment thread api/server/controllers/agents/request.js
Comment on lines +326 to +330
try {
const anchorExists = await messageExists(anchorId, jobData.conversationId);
return anchorExists ? 'skip-anchor' : 'skip-turn';
} catch {
return 'skip-turn';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Propagate anchor lookup failures

When the injected message lookup rejects, this catch converts a database outage into the same skip-turn result as a confirmed missing anchor. The caller then discards the original operational error, reports only the synthetic “anchor unavailable” persistence failure, and returns a successful abort with no stopped row, preventing the job boundary and logs from distinguishing an outage from absence. Let the rejection reach the existing beforePublish failure boundary or preserve it in a typed result.

AGENTS.md reference: AGENTS.md:L116-L121

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3650e89. The read still resolves to skip-turn so checkpoint cleanup runs, but resolveAbortedTurnPersistence now returns the original read error first in persistenceErrors, which the route throws at the beforePublish boundary, so an outage stays distinguishable from an absent anchor. compaction.spec.ts asserts the outage object reaches persistenceErrors.

Comment on lines +266 to 267
if ((!hasOutcome || removedUnfinishedRound) && synthesizeFailure) {
contentParts.push(...compactionFailureContent());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Synthesize failure only for the latest compaction round

When content contains an empty placeholder from an earlier failed or retried round followed by a usable summary from a later round, removing the older placeholder sets removedUnfinishedRound, and this condition appends COMPACTION_FAILED even though the latest summary completed before Stop. Multiple summary rounds are supported and checkpoints are last-summary-wins, so this produces a false failure beside a successful compaction; only a removed round after the latest usable outcome should trigger the synthesized failure.

AGENTS.md reference: AGENTS.md:L49-L52

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a5c4ac3. Walking backwards, a removed placeholder now counts only when no later part has already supplied an outcome, so an earlier abandoned round superseded by a later usable summary no longer gets a synthesized failure. compaction.spec.ts adds that case; the later-empty case still records the failure.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🗺️ Backend Platform codegraph: the taxonomy area this belongs to (classifier, confidence ≥ 0.9)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants