fix(activity-log): defer publish at 5 remaining transactional logActivity call sites (BLO-21605) - #1026
Conversation
…vity call sites (BLO-21605) logActivity's publisher (in-process EventEmitter + plugin outbox) escapes whatever transaction it's called from. Firing it inline from inside a db.transaction lets a consumer read activity.logged and query the table before the row commits, and turns a rollback into a phantom event. PR #806 (BLO-18614) added an opt-in `{ deferPublish: true }` and fixed one call site; this applies the same fix to the remaining five: two in company-skill-policy.ts (replace/reset), two in heartbeat.ts (workspace-quarantine retry scheduling, workspace-preflight-block), and one in pipelines.ts (stage automation env update). Each transaction now returns the deferred publisher as part of its resolved value so a rollback throws past the publish() call instead of falling through to it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 similar comment
allyblockcast
left a comment
There was a problem hiding this comment.
Approved after green required checks and no unresolved review threads.
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Looks good. The five transactional callers now carry the deferred publisher through the transaction result and invoke it only after commit, so rollback cannot produce a phantom Strengths
Recommended ActionNo Critical or Important findings remain. The PR author is |
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Looks good. The five transactional callers now return their deferred activity publishers through the transaction result and invoke them only after commit. Rollbacks therefore cannot emit phantom Critical Issues (0)Important Issues (0)Strengths
Recommended ActionNo Critical or Important findings remain. The PR author is |
…05-defer-publish-remaining-sites # Conflicts: # server/src/services/heartbeat.ts
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Looks good. Each transactional caller now carries the deferred activity publisher through the successful transaction result and invokes it only after commit. A rollback therefore bypasses publication, while committed state is visible before Critical Issues (0)Important Issues (0)Suggestions (0)Strengths
Recommended ActionNo Critical or Important findings remain. The PR author is |
Thinking Path
Linked Issues or Issue Description
Fixes: BLO-21605 (Paperclip issue; no public GitHub issue — filed against PR #806 / BLO-18614)
What Changed
server/src/services/company-skill-policy.ts:replace()andreset()now calllogActivity(tx, ..., { deferPublish: true })and return the publisher out of the transaction; each invokes it (in a try/catch, logging on failure) only after the transaction resolves.server/src/services/heartbeat.ts:scheduleBoundedRetryForRun) now defers publish and carries the publisher on the"scheduled"outcome variant; invoked only afterdb.transactionresolves.enqueueWakeup's dispatch transaction now defers publish and carries the publisher on the"skipped"outcome; invoked only after the transaction resolves (narrowed via"activityPublish" in outcomesince only that one"skipped"return produces a publisher).server/src/services/pipelines.ts:updateStageAutomationEnv()now defers publish and returns{ updatedRoutine, publish }from the transaction; invoked only after it resolves.publish()call instead of falling through to it.issue-denied-write-recovery-persistence.test.ts):company-skill-policy-service.test.ts: rollback emits zeroactivity.loggedevents; successfulreplace/resetpublish only once the new revision/deletion is visible on a separate connection.pipelines-service.test.ts: rollback (via adb.transactionproxy that runs the real transaction then throws, standing in for a commit-time failure) emits zero events; success publishes only once the routine revision bump is visible.heartbeat-retry-scheduling.test.ts: same rollback/visibility pair for the workspace-quarantine path.heartbeat-workspace-branch-containment.test.ts: same rollback/visibility pair for the workspace-preflight-block path.Verification
pnpm --filter @paperclipai/server exec tsc --noEmit— clean.pnpm --filter @paperclipai/server exec vitest run src/__tests__/company-skill-policy-service.test.ts src/__tests__/company-skill-policy-routes.test.ts src/__tests__/company-skills-routes.test.ts— 61/61 passed.pnpm --filter @paperclipai/server exec vitest run src/__tests__/pipelines-service.test.ts— 34/34 passed.pnpm --filter @paperclipai/server exec vitest run src/__tests__/heartbeat-retry-scheduling.test.ts— 54/54 passed.pnpm --filter @paperclipai/server exec vitest run src/__tests__/heartbeat-workspace-branch-containment.test.ts— 8/8 passed.Risks
logActivitycallers (thedeferPublishoption is opt-in and defaults to firing inline as before). The five touched sites change only when their publish fires (after commit instead of inline) and add a try/catch around a call that previously couldn't fail independently of the transaction — a publish failure is now logged as a warning rather than able to affect the transaction's outcome (it happens after commit).toHaveBeenCalledWithassertions are arity-exact, so any mock oflogActivityat these 5 sites would need updating to expect the third{ deferPublish: true }argument. I checked and none of the existing tests for these call sites mocklogActivitydirectly (they either use the real service against embedded Postgres, or mock a different route file), so nothing needed updating.Model Used
Claude Sonnet 5 (
claude-sonnet-5[1m]), 1M context window, agentic tool use (Read/Edit/Bash/Grep) via Claude Code. No extended-thinking transcript included.Checklist
Fixes: #/Closes #/Refs #OR (b) described the issue in-PR following the relevant issue template