fix(activity-log): enlist plugin outbox writes in caller tx (BLO-19132) - #1031
fix(activity-log): enlist plugin outbox writes in caller tx (BLO-19132)#1031kkroo wants to merge 6 commits into
Conversation
Route inline plugin outbox writes through the db handle passed to logActivity so an enclosing transaction commits or rolls back the activity row and outbox row together. Unlike the global best-effort publisher, an explicitly enlisted handle now propagates enqueue failures. That makes a transactional outbox failure abort the transaction visibly instead of swallowing the original PostgreSQL error after the transaction has already been marked failed. Keep deferred publication on the boot-time global handle because it runs after commit, when the caller transaction handle is released. Add embedded-Postgres coverage for commit, rollback, deferred publication, non-transactional publication, and an outbox insert failure that rolls back the activity row. Refs #953. Refs BLO-19132.
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
🔗 Paperclip issue: BLO-19132 |
1 similar comment
|
🔗 Paperclip issue: BLO-19132 |
|
Hey @kkroo! Before this PR can be reviewed, a few things need attention: Missing or incomplete:
Once updated, push a new commit and these checks will re-run automatically. — commitperclip |
Superseded at bf9b6ae: the App review found an unresolved Important issue on this exact head.
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: bf9b6ae
Important Issues (1)
- [pr-review-toolkit/native-codex]
server/src/services/activity-log.ts:305— Every non-deferredlogActivitycall passes its requireddbargument intopublishPluginDomainEvent, and that function treats any non-null handle as transaction-enlisted (db != null). A normal top-levelDbis not a transaction: if the activity insert commits and the subsequent outbox insert fails,logActivitynow rejects after durable partial success. Callers can report failure or retry even though the activity row already exists, which breaks the documented best-effort behavior for callers outside transactions.- Make strict error propagation/enlistment explicit instead of inferring it from the presence of a handle, or ensure ordinary calls wrap the activity and outbox writes in one transaction. Add a regression test that forces an outbox failure through a plain
Dband asserts the intended activity-row and error behavior.
- Make strict error propagation/enlistment explicit instead of inferring it from the presence of a handle, or ensure ordinary calls wrap the activity and outbox writes in one transaction. Add a regression test that forces an outbox failure through a plain
Strengths
- The enlisted transaction path now propagates the original PostgreSQL statement failure instead of swallowing it and surfacing a misleading later transaction failure.
- Embedded-Postgres coverage verifies commit, rollback, deferred publication, and the forced enlisted-insert failure.
- Deferred post-commit publication correctly avoids reusing a released transaction handle.
Recommended Action
- Address the Important issue before merge.
- Re-run the embedded-Postgres regression suite after making enlistment explicit.
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 8e93e76
Prior Findings Dispositioned (1)
- prior:bf9b6ae important 1 — still-present —
server/src/services/activity-log.ts:92—logActivitystill passes every non-deferred caller handle throughemit(db)at line 310, whiledb != nullclassifies both transactions and ordinary top-level handles as enlisted and routes failures through the rejectingawait insertpath at lines 99-100.
Important Issues (1)
- prior:bf9b6ae important 1 [pr-review-toolkit/gstack-review/native-codex]
server/src/services/activity-log.ts:92— Ordinary top-levellogActivity(db, ...)calls are still treated as transaction-enlisted. If the activity insert autocommits and the subsequent outbox insert fails, the call rejects after durable partial success; callers can report failure or retry even though the activity row already exists.- Make transaction enlistment explicit rather than inferring it from a non-null
Db. Keep plain top-level handles on the best-effort path, reserve propagated failures for explicitly transactional calls, and add an ordinary-Dboutbox-failure regression test.
- Make transaction enlistment explicit rather than inferring it from a non-null
Strengths
- The enlisted transaction path preserves the original PostgreSQL failure and rolls back the activity and outbox rows together.
- Embedded-Postgres coverage exercises commit, rollback, deferred publication, disabled-outbox behavior, and forced enlisted-insert failure.
- Deferred publication correctly avoids reusing a released transaction handle.
Recommended Action
- Resolve the remaining Important issue before merge.
- Re-run the embedded-Postgres regression suite after making transaction enlistment explicit.
allyblockcast
left a comment
There was a problem hiding this comment.
Reviewed current head; no active unresolved review threads.
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 5f4ffdb
Prior Findings Dispositioned (1)
- prior:bf9b6ae important 1 — still-present —
server/src/services/activity-log.ts:92— The exact head still derives strict transaction enlistment fromdb != null, while every non-deferredlogActivitypasses its caller handle throughemit(db)at line 310. This still classifies an ordinary top-levelDbas transactional.
Important Issues (2)
- prior:bf9b6ae important 1 [pr-review-toolkit/gstack-review/native-codex]
server/src/services/activity-log.ts:92— Ordinary top-levellogActivity(db, ...)calls are still treated as transaction-enlisted. If the activity insert autocommits and the subsequent outbox insert fails, the call rejects after durable partial success and after the live event has fired, so callers can report failure or retry work whose activity row already exists.- Make transaction enlistment explicit instead of inferring it from a non-null handle. Keep plain top-level handles on the best-effort path, reserve propagated failures for explicitly transactional calls, and add a plain-
Dboutbox-failure regression test.
- Make transaction enlistment explicit instead of inferring it from a non-null handle. Keep plain top-level handles on the best-effort path, reserve propagated failures for explicitly transactional calls, and add a plain-
- [gstack-review/native-codex]
server/src/services/activity-log.ts:307— The deferredActivityPublishcallback now starts asyncemit(null)withvoid, so synchronous listener errors are converted into an unobserved rejected promise. The existing exact-head caller atserver/src/routes/issues.ts:4358-4366invokes the callback insidetry/catchspecifically to log publication failures, but that catch can no longer observe them; under strict unhandled-rejection handling this can terminate the process.- Change
ActivityPublishto returnPromise<void>and await it at callers, or attach a terminal.catch(...)inside the deferred callback. Add a regression test with a throwing live-event subscriber.
- Change
Strengths
- The enlisted transaction path preserves the original PostgreSQL insert failure and rolls back the activity and outbox rows together.
- Embedded-Postgres coverage exercises commit, rollback, deferred publication, disabled-outbox behavior, and a forced enlisted-insert failure.
- Deferred publication correctly avoids reusing a released transaction handle.
Recommended Action
- Resolve both Important issues before merge.
- Re-run the embedded-Postgres regression suite with plain-handle failure and deferred-listener failure coverage.
…activity-tx-carrier # Conflicts: # server/src/services/activity-log.ts
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 22f8101
Prior Findings Dispositioned (2)
- prior:bf9b6ae important 1 — fixed —
server/src/services/activity-log.ts:102— Strict propagation now requires the explicitenlistedflag, while the ordinary path catches and logs insert failures at lines 113-115; the plain-Dbregression test covers the intended committed-activity/best-effort-outbox behavior. - prior:5f4ffdb important 2 — fixed —
server/src/services/activity-log.ts:329— The deferred callback now returnsemit(null, false)instead of discarding its promise, andserver/src/routes/issues.ts:4403awaits it inside the existing notification-failure handler.
Important Issues (2)
- [pr-review-toolkit/native-codex]
server/src/services/activity-log.ts:331—enlistPluginOutboxcontrols rejection behavior but not handle selection: every non-deferred call still passes the caller'sdbinto the outbox insert. An unannotated transaction therefore enlists implicitly; if that insert fails, the catch at lines 113-115 swallows the original error after PostgreSQL has already aborted the transaction, so the caller gets a misleading later statement/commit failure. This contradicts the documented explicit-opt-in carrier.- Pass the caller handle only when transaction enlistment is explicitly requested; otherwise use the global outbox handle. Add a forced-failure test for a transaction handle without
enlistPluginOutbox.
- Pass the caller handle only when transaction enlistment is explicitly requested; otherwise use the global outbox handle. Add a forced-failure test for a transaction handle without
- [gstack-review/native-codex]
server/src/services/activity-log.ts:303— The live event fires before the strict enlisted outbox insert. If that insert rejects, the enclosing transaction rolls back but subscribers have already observedactivity.loggedfor state that does not exist. The enlisted-failure test verifies only database rows, so this phantom side effect is currently untested.- Perform the strict enlisted insert before publishing the live event, or defer all publication until commit. Extend the failure test to assert that no live event escapes.
Strengths
- The ordinary top-level
Dbpath now preserves best-effort outbox semantics and has focused regression coverage. - Deferred publication is awaitable, and listener failures are observable by the post-commit caller.
- The tests cover commit, rollback, disabled outbox, strict insert failure, ordinary-handle failure, and deferred publication.
Recommended Action
- Fix the two transaction-side ordering/carrier issues before merge.
- Run the embedded-Postgres suite with the missing transaction-without-enlistment and no-phantom-live-event assertions.
Thinking Path
Linked Issues or Issue Description
What Changed
masterunder an independent author.publishPluginDomainEvent(event, db)so explicit caller handles propagate enqueue errors, while omitted/null handles still log and swallow best-effort failures.Verification
git diff --check- passed.pnpm --filter @paperclipai/server typecheck- passed.pnpm exec vitest run server/src/__tests__/activity-log-transactional-publish.test.ts- test file loaded successfully; embedded Postgres is unsupported in this local Mac session, so the 5 embedded tests were skipped locally and must run in CI.Risks
Moderate but scoped to plugin-mapped activity logging. Inline transactional callers can now see the original outbox enqueue error instead of a swallowed error followed by an implicit rollback/commit failure. Deferred and global publication remain best-effort.
Model Used
OpenAI Codex, GPT-5 family, with GitHub CLI inspection, local worktree patching, Vitest startup, and TypeScript verification.
Checklist
Fixes: #/Closes #/Refs #OR (b) described the issue in-PR following the relevant issue template