fix(activity-log): enlist plugin outbox writes in caller tx (BLO-19132) - #1031
Conversation
|
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.
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.
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: b82fe3e
Prior Findings Dispositioned (2)
- prior:22f8101 important 1 — fixed —
server/src/services/activity-log.ts:303— Handle selection is now explicit: onlyshouldEnlistPluginOutboxpasses the caller handle, while the ordinary path callspublishPluginDomainEventwithdb: nullat lines 332-335 and therefore uses the global best-effort handle. - prior:22f8101 important 2 — fixed —
server/src/services/activity-log.ts:306— The strict enlisted insert is awaited at lines 306-313 beforepublishLiveEventruns, and the failure regression asserts that no live event escapes when that insert rejects.
Critical Issues (0)
Important Issues (1)
- [gstack-review/native-codex]
server/src/services/activity-log.ts:340—deferPublishreturns before performing the enlisted outbox write, so it cannot be combined withenlistPluginOutbox(oratomicPluginEvent). Consequently, every newly annotated transactional caller must publishactivity.loggedinline at line 316 to get an atomic outbox row. If any later statement or commit fails, the database activity and outbox rows roll back but live subscribers have already observed a phantom activity. The rollback test captures live events but never asserts their absence. - Recommendation: split transactional outbox enqueue from live publication so
{ enlistPluginOutbox: true, deferPublish: true }enqueues before commit and returns a live-only post-commit callback; use that composition at the transactional call sites and assert no live event on rollback after a successful enqueue.
Strengths
- Plain and unannotated handles preserve the global best-effort outbox behavior.
- Strict outbox insert failures now propagate the original database error and do not emit a live event first.
- Deferred publication is awaitable, making listener failures observable to its caller.
Recommended Action
- Fix the Important transaction/publication ordering issue before merge.
- Run the embedded-Postgres regression suite with a rollback-after-successful-enqueue assertion.
Ally's Important finding is correct — and it has a live instance neither of us had named. One correction to its framing.@kkroo — I authored the #1024 change this PR carries, so I verified the finding against current head Confirmed: the composition genuinely cannot be expressed
The live instance:
|
| annotated site | action | plugin event? |
|---|---|---|
company-skill-policy.ts:262 |
company.skill_policy_replaced |
none |
company-skill-policy.ts:288 |
company.skill_policy_reset |
none |
heartbeat.ts:14097 |
execution_workspace.workspace_validation_quarantined |
none |
heartbeat.ts:24641 |
issue.workspace_preflight_blocked |
none |
pipelines.ts:3790 |
pipeline.stage_automation_env_updated |
none |
None is in PLUGIN_EVENT_TYPES or ACTIVITY_ACTION_TO_PLUGIN_EVENT, so eventTypeForActivityAction returns null, pluginEvent is null, and the enlisted branch is dead code at all five. They write no outbox row, so the finding's "the database activity and outbox rows roll back" clause is vacuous for them. Their phantom live event is real but pre-existing and unchanged by this PR — they published inline before the annotation and still do; enlistPluginOutbox: true does not alter live-event timing.
The actual instance is a caller neither review mentioned — approvals.ts:423 withdraw, from #850 (BLO-19079):
- inside
db.transaction, and it has alreadyterminate()d the bound agent by that point; approval.withdrawn→approval.decidedviaACTIVITY_ACTION_TO_PLUGIN_EVENT, sopluginEventis non-null;- sets
atomicPluginEvent: true(so the outbox row is correctly bound) with nodeferPublish.
So it binds the durable event and then announces activity.logged before commit. A failed commit leaves subscribers a withdrawal that did not happen. That is exactly Ally's scenario, on master today.
Severity, stated honestly: the escaping event is an in-memory SSE refresh hint, not the durable outbox row — it reaches connected sessions and self-corrects on the next fetch. That is materially lower severity than the phantom plugin domain event this PR already fixed. I would call it worth closing now because the composition is what this PR's first real consumer needs, not because the leak is severe.
Ally is also right about the test
activity-log-transactional-publish.test.ts:84 calls captureLiveEvents() and live.unsubscribe() and then never asserts on live.seen. The phantom escapes undetected.
Patch: cto/blo-19132-defer-plus-enlist-proposed @ cec4e7b
git cherry-pick cec4e7b041f8b23c253d70afa1eb45b7e682f975
Hoists the enlisted enqueue above both publication paths — it has to run while db is live, and a live event cannot be un-sent, so a failed insert must abort before anything publishes. The deferred callback then withholds only the live event, and re-enqueues on the global handle only when the row was not already enlisted. Adopts the composition at approvals.ts withdraw.
Deliberately not adopted at the five sites above: they write no outbox row, and hoisting a deferred publish out of those heartbeat.ts transactions is a far larger change than a stale refresh hint justifies. Behaviour matrix after the patch:
| options | outbox enqueue | live event |
|---|---|---|
| none | global, best-effort, inline | inline |
deferPublish |
global, best-effort, after commit | after commit |
enlistPluginOutbox |
caller handle, strict, atomic | inline (documented accepted phantom) |
| both | caller handle, strict, atomic | after commit |
Verification — tsc --noEmit -p server/tsconfig.json clean; 11 passed (11) on the suite (8 existing + 3 new), plus 71 passed over 7 approval/activity suites including approval-withdraw-plugin-event.test.ts, and 18 passed over the outbox/issue-activity ones.
Note the two pre-existing withdrawApprovalSchema typecheck errors are a stale local @paperclipai/shared dist, identical with and without my change; pnpm --filter @paperclipai/shared build clears them.
Mutation-checked, and I will be precise about which assertion carries the weight. Restoring the old semantics fails exactly one case:
× enlisted + deferred enqueues inside the transaction but publishes live after commit
AssertionError: expected [] to have a length of 1 but got +0
Tests 1 failed | 10 passed (11)
The in-transaction visibility check is the discriminator. The rollback case does not discriminate — under the old code the deferred callback simply never ran, so nothing escaped either way; it asserts the correct end state, not the fix.
Why a branch and not a commit on yours
This PR supersedes my App-authored #1024 specifically so the work has an independent author. A commit from allyblockcast[bot] would remove the property the carrier exists to provide. You have also answered all four prior review rounds in 1h35m / 7h20m / 12h45m / 8h04m, so at ~10.5h you are inside your own range and I have no reason to think you are stalled. The patch is yours to take, amend, or reject. I have not re-requested Ally — your push re-triggers review on its own.
76df7eb to
c8ec5d1
Compare
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: c8ec5d1
Prior Findings Dispositioned (1)
- prior:b82fe3e important 1 — fixed —
server/src/services/activity-log.ts:341— The outbox insert now runs on the caller's transaction handle before either publication path, and lines 345-354 return a deferred callback that emits only the live event without re-enqueuing.server/src/services/approvals.ts:423-449uses this composition and invokes the callback only after commit.
Critical Issues (0)
Important Issues (1)
- [pr-review-toolkit/gstack-review/native-codex]
server/src/services/activity-log.ts:358— Ordinary, non-enlisted publication now awaits the global-handle outbox insert. WhenlogActivityruns inside a transaction, that transaction retains one pooled connection while waiting for_outboxDbto acquire another; enough concurrent transactions can occupy the entire pool and wait indefinitely on connections they themselves retain. The previous fire-and-forget path allowed each transaction to commit and release its connection. - Recommendation: await only enlisted inserts. Keep best-effort global publication detached with a terminal error handler, or explicitly schedule it after commit, and add a constrained-pool regression test for concurrent unannotated transactions.
Strengths
- Combined enlistment and deferral now preserve durable outbox atomicity without leaking a live event before commit.
- The exact-head tests distinguish in-transaction enqueue visibility, rollback behavior, post-commit live publication, and duplicate prevention.
- The approval-withdraw path awaits the deferred live publisher and contains listener failures after the durable transaction commits.
Recommended Action
- Restore non-blocking behavior for non-enlisted global outbox publication before merge.
- Add and run the constrained-pool regression test.
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: 3a4be4b
Prior Findings Dispositioned (1)
- prior:c8ec5d1 important 1 — fixed —
server/src/services/activity-log.ts:360— Non-enlisted publication is now detached while a transaction is open, so it no longer waits on the global outbox connection before the transaction can release its pooled connection.
Critical Issues (0)
Important Issues (2)
- [pr-review-toolkit/gstack-review/native-codex]
server/src/services/activity-log.ts:360—isis Drizzle's SQLISpredicate builder, not a JavaScript runtime type guard. It returns a truthy SQL expression for everyDb, so this branch detaches the global outbox enqueue for ordinary top-level handles as well as transactions. Consequentlyawait logActivity(db, ...)can return before the promised outbox enqueue completes, contradicting the synchronous ordinary-caller contract at lines 373-374 and making the inline-publication regression test race.- Use a real runtime transaction discriminator, such as
db instanceof PostgresJsTransaction, and add a plain-Dbtest that proveslogActivityhas completed its global enqueue when it resolves.
- Use a real runtime transaction discriminator, such as
- [pr-review-toolkit/gstack-review/native-codex]
server/src/services/company-skill-policy.ts:261— This new transactional caller opts intoenlistPluginOutboxbut notdeferPublish.logActivitytherefore sendsactivity.loggedinline atactivity-log.ts:358; a later statement or commit failure rolls back the policy change and enlisted outbox row after subscribers have observed a live event for state that never committed. The same unsafe combination is newly added atserver/src/services/heartbeat.ts:14543,server/src/services/heartbeat.ts:25881, andserver/src/services/pipelines.ts:3771.- Combine enlistment with
deferPublish, return the deferred callback from each transaction, and invoke it only after commit. Add rollback coverage for each transactional publication pattern to assert no live event escapes.
- Combine enlistment with
Suggestions (0)
Strengths
- The outbox insert is now explicitly enlisted only on opted-in paths, preserving best-effort semantics for normal callers.
- The approval withdrawal path composes enlistment with post-commit live publication and awaits listener failures in its existing recovery handler.
Recommended Action
- Fix both Important issues before merge.
|
@ally please submit the formal App and User reviews for current 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: 795ba48
Critical Issues (0)
Important Issues (0)
Suggestions (0)
Strengths
- Enlisted plugin-outbox writes happen before any live publication, while deferred transactional callers publish the live refresh only after commit.
- Plain handles retain best-effort outbox behavior; unannotated transactions detach the global enqueue to avoid pool-exhaustion deadlocks.
- Focused embedded-Postgres coverage exercises rollback, strict outbox failure, deferred listener errors, ordinary callers, and constrained-pool behavior.
Recommended Action
- Merge when the remaining required checks complete.
allyblockcast
left a comment
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: 795ba48
Critical Issues (0)
Important Issues (0)
Suggestions (0)
Strengths
- Enlisted plugin-outbox writes happen before any live publication, while deferred transactional callers publish the live refresh only after commit.
- Plain handles retain best-effort outbox behavior; unannotated transactions detach the global enqueue to avoid pool-exhaustion deadlocks.
- Focused embedded-Postgres coverage exercises rollback, strict outbox failure, deferred listener errors, ordinary callers, and constrained-pool behavior.
Recommended Action
- Merge when the remaining required checks complete.
795ba48 to
141995d
Compare
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: 141995d
Critical Issues (0)
Important Issues (0)
Suggestions (0)
Strengths
- Enlisted plugin-outbox writes occur before live publication, and transactional callers defer the live refresh until after commit.
- The ordinary-handle path retains best-effort outbox behavior while unannotated transactions avoid pool-exhaustion deadlocks.
- Focused embedded-Postgres coverage exercises commit, rollback, strict outbox failure, deferred listener errors, ordinary callers, and constrained-pool behavior.
Recommended Action
- Address the separate policy-test runner cancellation, then merge when required checks are green.
allyblockcast
left a comment
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: 141995d
Critical Issues (0)
Important Issues (0)
Suggestions (0)
Strengths
- Enlisted plugin-outbox writes occur before live publication, and transactional callers defer the live refresh until after commit.
- The ordinary-handle path retains best-effort outbox behavior while unannotated transactions avoid pool-exhaustion deadlocks.
- Focused embedded-Postgres coverage exercises commit, rollback, strict outbox failure, deferred listener errors, ordinary callers, and constrained-pool behavior.
Recommended Action
- Address the separate policy-test runner cancellation, then merge when required checks are green.
…erdict Follow-up to the consolidated review on #1385. Three suggestions, all confirmed against the live repo. 1. `duplicateBodyAcrossIdentities` compared bodies with `===` alone, so two bodiless approvals under two seats (`null === null`) were reported as "one verdict, posted twice". That is the wrong diagnosis with the wrong remedy: there is no verdict, it is BLO-22916's Defect 2, and I2d already reports the missing attestation. Guard on a truthy body. 2. Document the parameter contract. Unlike its neighbour `isRequiredApprovalPair(reviews, headSha)`, this function takes an ALREADY-filtered operative set; passing a raw `pr.reviews` would compare dismissed and stale-head reviews and answer a different question. 3. Restore the `describe`/`it` line break collapsed when the new block was inserted above it. Verification: - `node --test scripts/check-ally-review-consistency.test.mjs` -> 70/70 pass (69 before; +1 pinning the bodiless case across null/""/undefined). - Mutation-checked: dropping the truthiness guard fails that test. - Live audit on this branch is byte-identical to the reviewed head's: 8 violations, #1176/#1073/#1031 duplicate-shape wording, #1316 (distinct bodies) unchanged. No real-world behavior change -- Defect 2 is fixed, so there are no bodiless approvals at head to re-label. Refs BLO-22916
141995d to
7a9c76e
Compare
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: 7a9c76e
Critical Issues (0)
Important Issues (1)
- [pr-review-toolkit/gstack-review/native-codex]
server/src/services/pipelines.ts:4513— The newActivityPublishtype returnsPromise<void>, but this post-commit call still invokespublish()withoutawaitinside atry/catch. A throwing live-event subscriber therefore creates an unhandled rejected promise, while the catch that is intended to log notification failures never runs. This is the same async failure mode the other updated callers in this PR avoid.- Change this to
await publish()so deferred publication failures are observed and logged, and add or extend a regression test for a throwing live-event subscriber on this pipeline path.
- Change this to
Suggestions (0)
Strengths
- Enlisted plugin-outbox writes now occur through the caller transaction before live publication, preserving rollback atomicity.
- Transactional callers consistently defer live publication until after commit, while ordinary and unannotated transaction paths retain best-effort behavior.
- The embedded-Postgres coverage exercises rollback, strict outbox failures, deferred publication, listener failures, ordinary callers, and constrained-pool behavior.
Recommended Action
- Address the Important issue before merge.
- Re-run the affected pipeline and activity-log tests.
|
@ally head Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current 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: b750d31
Prior Findings Dispositioned (1)
- prior:7a9c76e important 1 — fixed —
server/src/services/pipelines.ts:4513— The post-commit pipeline publisher now awaitspublish(), so live-subscriber failures reach the surrounding logger.
Critical Issues (0)
Important Issues (1)
- [pr-review-toolkit/gstack-review/native-codex]
server/src/services/activity-log.ts:97—publishPluginDomainEventreturns before using an explicitly suppliedoptions.dbwhenever the module-global_outboxDbis unset. That regresses the existingatomicPluginEventcontract: a caller can successfully insert the activity row, publish the in-memoryactivity.loggedevent, and silently omit the enlisted plugin outbox row instead of making the activity and outbox write atomic.- Check the selected outbox handle before the global-configuration guard, or otherwise allow
{ db, enlisted: true }to perform its insert without requiring_outboxDb; add a test with the global handle unset and an explicit transaction handle.
- Check the selected outbox handle before the global-configuration guard, or otherwise allow
Suggestions (0)
Strengths
- Transactional callers now enlist plugin outbox writes on their transaction handle and defer live publication until after commit.
- The pipeline regression from the previous Ally review is covered by an awaited post-commit publisher and a throwing-subscriber test.
- The new embedded-Postgres coverage exercises rollback, strict outbox failures, deferred publication, and constrained-pool behavior.
Recommended Action
- Fix the Important issue before merge.
- Re-run the affected activity-log and pipeline tests.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2aaf7f2 to
554425e
Compare
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: 554425e
Prior Findings Dispositioned (1)
- prior:b750d31 important 1 — fixed —
server/src/services/activity-log.ts:100— The explicitly supplied transaction handle is selected before the module-global outbox guard, and the focused test atserver/src/__tests__/activity-log-transactional-publish.test.ts:203verifies that an enlisted transaction writes the outbox row when the global handle is unset.
Critical Issues (0)
Important Issues (0)
Suggestions (0)
Strengths
- Enlisted outbox writes occur through the caller transaction before live publication, preserving rollback atomicity.
- Transactional callers defer live publication until after commit, while ordinary and unannotated transaction paths retain best-effort behavior.
- The focused embedded-Postgres coverage exercises rollback, strict outbox failures, deferred publication, listener failures, ordinary callers, constrained-pool behavior, and the unset-global-handle case.
Recommended Action
- Merge when the remaining required checks complete.
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