Skip to content

[3483 by Claude Opus 5.5] Durable change-event journal for pull requests and epic queues - #3496

Open
propr-dev[bot] wants to merge 2 commits into
3482-epic-integration-signals-100from
3483/claude-opus-5-5-durable-change-event-jour-20261112-1601-vhp
Open

propr-dev[bot] wants to merge 2 commits into
3482-epic-integration-signals-100from
3483/claude-opus-5-5-durable-change-event-jour-20261112-1601-vhp

Conversation

@propr-dev

@propr-dev propr-dev Bot commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

AI Implementation Summary

Closes #3483

Branch: 3483/claude-opus-5-5-durable-change-event-jour-20261112-1601-vhp
Commits: ✅ Changes committed (111f8b6)


AI Processing Completed

Execution Details:

Summary:
The monitor that was watching the regression run has finished. Its "ALL-DONE" line is the run completing, which I already covered above: 40 of 41 files passed. The one failure was test/planIssueManager.ultrafix.test.ts, which now passes (8/8) after I stubbed the journal module in it. Nothing else needs doing.

Detailed Logs:

  • Conversation: 292 messages
  • Session: 1cd1995a-8869-403a-8ef8-c1b635bd3f7a

Log files stored at:

  • conversation: /usr/src/app/logs/executions/issue-3483-2026-11-12T16-39-45-160Z-conversation.json
  • output: /usr/src/app/logs/executions/issue-3483-2026-11-12T16-39-45-160Z-output.txt
Latest Conversation Messages
ASSISTANT: [content unavailable]

ASSISTANT: Issue #3483 is done: the `repository_events` journal, its writer, and appends at every call site listed in the issue. In the last regression run of 41 files, 40 passed and one failed: `test/planIssueM...

ASSISTANT: The monitor that was watching the regression run has finished. Its "ALL-DONE" line is the run completing, which I already covered above: 40 of 41 files passed. The one failure was `test/planIssueManag...


This PR was created automatically by ProPR after processing issue #3483.


💡 Need changes?

Comment on this PR to request refinements — the AI agent monitors comments and will update the implementation based on your feedback. Keep iterating until you're satisfied!

…LL-DONE" line is the run completing, which I already covered above: 40 of 41 files passed. The one failure was `test/planIssueManager.ultrafix.test.ts`, which now passes (8/8) after I stubbed the journal module in it. Nothing else needs doing.
@propr-dev

propr-dev Bot commented Nov 12, 2026

Copy link
Copy Markdown
Contributor Author

/ultrafix goal=8 max=10
Triggered automatically by the requested execution settings.

@propr-dev propr-dev Bot added the ultrafix label Nov 12, 2026
@propr-dev

propr-dev Bot commented Nov 12, 2026

Copy link
Copy Markdown
Contributor Author

🔄 Ultrafix loop started (goal: 8/10, max cycles: 10)

First action: /review

💡 Tip: Remove the ultrafix label from this PR to stop further ultrafix cycles.

@propr-dev

propr-dev Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor Author

✅ AI Code Review Complete requested by @propr-ultrafix

Posted 1 review:

View Task Details

@propr-dev

propr-dev Bot commented Nov 12, 2026

Copy link
Copy Markdown
Contributor Author

🔍 AI Code Review — codex:gpt-6-astra

Overall Evaluation

This PR adds a repository event journal, instruments lifecycle writers, and supplies retention, activity notifications, documentation, and focused tests. It needs changes before merge because retention can delete recent events and several journal paths can omit real transitions or report incorrect ones.

✅ Transactional event writes — Review scores, PR outcomes, and guarded queue advancement append events within their database transactions.

✅ Failure isolation coverage — Added tests explicitly exercise missing journal tables while confirming that the underlying operations still succeed.

✅ Shared event vocabulary — Event domains and Ultrafix outcomes are centralized, including validation of the new activity domain.

This review is a static trace of the supplied code; no commands were run. The authoritative checks show no failures and five pending checks.

Merge blockers

Every finding below was introduced by this PR and must be resolved before merging.

F1: 🔴 Retention deletes unexpired events

  • Required behavior: The journal’s 30-day retention must preserve events that have not expired.

  • Evidence: packages/core/src/services/repositoryEventJournal.ts:pruneRepositoryEvents, particularly the deletion by seq <= maxSeq.

    1. A repository accumulates recent events.
    2. A delayed delivery appends an older event at a higher sequence number. This is supported directly by journalPullRequestWebhook, which uses the PR’s original created_at, and by outcome writers, which use merge or close timestamps.
    3. The sweep identifies that higher sequence as expired and deletes every preceding event for the repository, including recent events.

    The result is premature loss of retained history, potentially the repository’s entire recent journal. The transaction makes the deletion atomic but does not constrain it by age. The existing retention test inserts expired events before fresh events, so it misses this ordering.

    static trace: Verified from the timestamp-producing call sites and the sweep’s age-selection followed by sequence-only deletion.

  • Minimum fix: Delete only rows satisfying the expiration predicate. Preserve a monotonic cursor-expiration boundary based on deleted sequences without deleting unexpired rows merely to make retention a contiguous prefix.

F2: 🔴 Timestamp deduplication loses repeated status transitions

  • Required behavior: Distinct persisted plan-issue transitions must produce distinct journal events.

  • Evidence: packages/core/src/config/planIssueStatusJournal.ts:journalPlanIssueStatus and packages/core/src/config/planIssueManager.ts:updatePlanIssueStatus.

    1. A plan issue is updated to processing, leaving updated_at at SQLite’s current timestamp.
    2. Within the same second, it transitions to under_review, back to processing, and again to under_review.
    3. Both processing → under_review appends use the same issue ID, source status, destination status, and prior timestamp.
    4. The unique dedupe index rejects the second transition as a duplicate even though the status write succeeded.

    SQLite’s default current timestamp has second precision; these writers use db.fn.now(). Sequentially awaiting every operation does not prevent the collision. The added test manually assigns distinct timestamps between writes, avoiding the production behavior that exposes it.

    static trace: Verified from the dedupe-key construction and timestamp assignments. The same key construction affects all three instrumented status writers.

  • Minimum fix: Give actual status transitions an identity that cannot repeat within one clock tick, such as an atomically incremented revision. Suppress unchanged writes using the actual persisted transition rather than timestamp uniqueness.

F3: 🔴 Status events can describe the wrong mutation

  • Required behavior: A status event must identify the row actually changed and its immediately preceding persisted status.

  • Evidence: packages/core/src/config/planIssueManager.ts:updatePlanIssueStatus, updatePlanIssue, and updatePlanIssueByPR; packages/core/src/config/planIssueStatusJournal.ts:readPlanIssueForJournal.

    1. An issue starts in processing. Writer A requests under_review; writer B requests processing.
    2. Both complete the awaited readPlanIssueForJournal before either update, so both capture processing.
    3. A persists under_review; B then persists processing.
    4. B’s journal helper returns immediately because its stale before.status equals its requested destination. The database ends in processing, while the journal omits that return transition.

    The interruption point is between the awaited read and subsequent update. There is no transaction or compare-and-set guard coupling those operations in any of the three writers; the helper’s equality check therefore cannot establish that the write was a no-op.

    There is also a deterministic affected-row mismatch in updatePlanIssueStatus: repository/issue numbers can match historical drafts, but the helper reads only the newest row. If that newest row is protected by onLinkedReview while an older matching row is eligible, the update succeeds on the older row and the append falsely reports a transition for the unchanged newest row. updatePlanIssueByPR likewise updates all matches while journaling only one.

    static trace: Verified from the separate reads, update predicates, linked-review filter, and supplied comments explicitly allowing multiple historical matches.

  • Minimum fix: Capture the affected rows and their prior statuses within the same transaction as the guarded update and append. Journal each actual changed row, preserving the existing update filters, and emit nothing for rows excluded by those filters.

F4: 🔴 Returning to a previous head is silently omitted

  • Required behavior: The change journal must record genuine PR head transitions while distinguishing them from webhook redelivery.

  • Evidence: packages/core/src/webhook/pullRequestJournalEvents.ts:journalPullRequestWebhook, specifically dedupeKey: pr:${prNumber}:head:${headSha}.

    1. A PR receives commit B through a synchronize delivery; the journal records head_changed for B.
    2. Another push moves it to C, producing a second event.
    3. A force-push restores B. GitHub delivers a new synchronize event with before: C and head B.
    4. The existing B dedupe key suppresses this genuine transition.

    A journal reader now sees C as the last recorded head even though the PR is at B. Because duplicate appends return before publication, this path also emits no journal activity hint. The unique index distinguishes destination commits, not deliveries or transitions; the supplied test covers redelivery but never a return to a previously observed commit.

    static trace: Verified directly from synchronize handling, the SHA-only key, and the duplicate return in appendRepositoryEvent.

  • Minimum fix: Deduplicate synchronize events using a stable identity for the actual webhook delivery or transition occurrence, passed through the handler. A destination SHA—or even a source/destination pair—cannot uniquely identify repeated transitions.

Suggestions

These are optional follow-ups and are not sent to /fix.

No suggestions.

Score

The implementation has useful coverage and clear integration points, but the verified defects compromise the journal’s retained history and accuracy. The required corrections are localized; pending checks are not themselves blockers.

Score: 5/10


🤖 Review Details

  • Model: GPT-6 Astra
  • Time: 1m 22s
  • Tokens: 81,912 (79,874 in / 2,038 out)
  • Cost: $0.79

View Task


💡 Next step: Comment /fix to address every F# merge blocker, /fix all for every pending blocker and suggestion, or name records explicitly, as in /fix F3 S5.
F# and S# IDs increment across review comments and remain permanent, so selectors such as /fix F3 F5 stay unambiguous across cycles. S# suggestions stay optional: they are implemented only when you name them or request /fix all, and they never relax a merge blocker.

Copy and edit to choose records from this review:

/fix F1 F2 F3 F4

🤖 Review by ProPR

@propr-dev

propr-dev Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor Author

✅ Applied the requested follow-up changes in commit 0c724d6

Addressed 1 AI review comment (IDs: 6829897848)

Addressed findings F1, F2, F3, F4

Summary of Changes

I fixed F1–F4 and each has a regression test. All six new tests fail on the previous branch code and pass now; the full set of 20 tests in the three journal suites passes. Typecheck (core and api) and lint with zero warnings pass on the changed files.

F1 – Retention deleted unexpired events. pruneRepositoryEvents now deletes only rows older than the 30-day cutoff. The floor (the cursor-expiration boundary) is still one past the highest deleted seq and never moves backwards. A cursor below it may have missed pruned events, and unexpired rows below it stay readable. New test: recent events, then an older-stamped late event at a higher seq, then another recent one. Only the late event is deleted.

F2 – Repeated transitions within one second were lost. Status events no longer use a timestamp-based dedupe key. Each event is identified by its own seq. A write that doesn't change the status (a redelivered webhook) is skipped by comparing the row's actual previous status. New test: five transitions with no timestamp manipulation, including processing → under_review twice, produce five events.

F3 – Status events could describe the wrong row or prior status. A new helper, updatePlanIssuesWithStatusJournal in planIssueStatusJournal.ts, does three things in one transaction: reads the affected rows and their prior statuses using the writer's own filters (including the linked-review guard), updates exactly those rows, and appends one event per row that actually changed. All three writers use it: updatePlanIssue, updatePlanIssueStatus and updatePlanIssueByPR.

  • The production database opens transactions with BEGIN IMMEDIATE, so this holds across processes too.
  • stillApplies still runs before the transaction, because it can do its own database work.

New tests cover a write landing between another writer's lookup and its update, a newest row held by its linked review while an older row is updated, and a PR update matching several rows.

F4 – A force-push back to an earlier head was dropped. head_changed is now keyed by the GitHub delivery ID: pr:{n}:head_changed:delivery:{id}. A redelivery carries the same ID, while a new push is a new delivery. The ID now reaches the journal from both entry points:

  • Direct webhook: server.ts passes it to processWebhookEvent.
  • Routing relay: RoutingWebSocketIntakeService passes it the same way.

Without a delivery ID the event is written without a key, because neither the commit nor the before/after pair identifies one occurrence. In production both paths always have an ID. New test: B → C → B → C → B is fully recorded, a redelivery adds nothing, and the last recorded head matches the PR.

Other changes:

  • docs/docs/architecture/repository-event-journal.md is updated to the new retention, status-event and dedupe-key behaviour.
  • The fake database in test/planIssueManager.ultrafix.test.ts now supports transactions; it passes 8/8.
  • To keep handlePlanIssueTracking under the four-parameter lint limit, it now builds its own logger from the correlation ID.

Wider check: 42 test files that touch these paths, including the plan issue, webhook, routing intake and epic queue suites, pass except one. packages/core/test/planPRClosedRecovery.test.ts passes every test it runs but never exits and times out. It does the same on the base code, so this change didn't cause it.

Separate discovery (not changed): linkPRToPlanIssue sets a plan issue's status to under_review but writes no journal event. It was never one of the three journaled writers, so I left it.

There's no visual change, so I made no previews.


🤖 Implementation Details

  • Model: Claude Opus 5.5 (medium)
  • Requested By: @propr-ultrafix
  • Time: 21m 8s
  • Tokens: 4,770,014 (4,737,940 in / 32,074 out)
  • Cost: $2.16
  • Subscription usage: Fable +1%

View Task Execution


💡 ProPR Slash Commands
Command Description Example
/merge Merge target branch into this PR and resolve conflicts /merge
/review Request an AI code review /review or /review claude-sonnet
/fix Address review blockers, or blockers and suggestions with /fix all /fix, /fix all, or /fix F3 S5
/switch Change the AI model for this PR /switch claude-opus
/use Override the model for a single follow-up run /use claude-sonnet
/ultrafix Loop review→fix cycles until score goal is met /ultrafix or /ultrafix goal=8 max=10

🤖 Generated by ProPR

…ests fail on the previous branch code and pass now; the full set of 20 tests in the three journal suites passes. Typecheck (core and api) and lint with zero warnings pass on the changed files.

I fixed F1–F4 and each has a regression test. All six new tests fail on the previous branch code and pass now; the full set of 20 tests in the three journal suites passes. Typecheck (core and api) and lint with zero warnings pass on the changed files.

**F1 – Retention deleted unexpired events.** `pruneRepositoryEvents` now deletes only rows older than the 30-day cutoff. The floor (the cursor-expiration boundary) is still one past the highest deleted `seq` and never moves backwards. A cursor below it may have missed pruned events, and unexpired rows below it stay readable. New test: recent events, then an older-stamped late event at a higher `seq`, then another recent one. Only the late event is deleted.

**F2 – Repeated transitions within one second were lost.** Status events no longer use a timestamp-based dedupe key. Each event is identified by its own `seq`. A write that doesn't change the status (a redelivered webhook) is skipped by comparing the row's actual previous status. New test: five transitions with no timestamp manipulation, including `processing → under_review` twice, produce five events.

**F3 – Status events could describe the wrong row or prior status.** A new helper, `updatePlanIssuesWithStatusJournal` in `planIssueStatusJournal.ts`, does three things in one transaction: reads the affected rows and their prior statuses using the writer's own filters (including the linked-review guard), updates exactly those rows, and appends one event per row that actually changed. All three writers use it: `updatePlanIssue`, `updatePlanIssueStatus` and `updatePlanIssueByPR`.
- The production database opens transactions with `BEGIN IMMEDIATE`, so this holds across processes too.
- `stillApplies` still runs before the transaction, because it can do its own database work.

New tests cover a write landing between another writer's lookup and its update, a newest row held by its linked review while an older row is updated, and a PR update matching several rows.

**F4 – A force-push back to an earlier head was dropped.** `head_changed` is now keyed by the GitHub delivery ID: `pr:{n}:head_changed:delivery:{id}`. A redelivery carries the same ID, while a new push is a new delivery. The ID now reaches the journal from both entry points:
- **Direct webhook:** `server.ts` passes it to `processWebhookEvent`.
- **Routing relay:** `RoutingWebSocketIntakeService` passes it the same way.

Without a delivery ID the event is written without a key, because neither the commit nor the before/after pair identifies one occurrence. In production both paths always have an ID. New test: B → C → B → C → B is fully recorded, a redelivery adds nothing, and the last recorded head matches the PR.

**Other changes:**
- `docs/docs/architecture/repository-event-journal.md` is updated to the new retention, status-event and dedupe-key behaviour.
- The fake database in `test/planIssueManager.ultrafix.test.ts` now supports transactions; it passes 8/8.
- To keep `handlePlanIssueTracking` under the four-parameter lint limit, it now builds its own logger from the correlation ID.

**Wider check:** 42 test files that touch these paths, including the plan issue, webhook, routing intake and epic queue suites, pass except one. `packages/core/test/planPRClosedRecovery.test.ts` passes every test it runs but never exits and times out. It does the same on the base code, so this change didn't cause it.

**Separate discovery (not changed):** `linkPRToPlanIssue` sets a plan issue's status to `under_review` but writes no journal event. It was never one of the three journaled writers, so I left it.

There's no visual change, so I made no previews.

PR: #3496
Comment by: @propr-ultrafix (ID: 0)
Model: Claude Opus 5.5
@propr-dev

propr-dev Bot commented Nov 12, 2026

Copy link
Copy Markdown
Contributor Author

⏳ Ultrafix is waiting for CI before the next /review.

These blocking checks on 0c724d6 are holding the loop back:

  • Validate Changes — not finished
  • Classify changed surfaces — not finished
  • Full Test Suite Shard 3/4 — not finished
  • Full Test Suite Shard 4/4 — not finished
  • Full Test Suite Shard 2/4 — not finished
  • Full Test Suite Shard 1/4 — not finished
  • Classify changed surfaces — not finished
  • Classify changed surfaces — not finished
  • Classify changed surfaces — not finished
  • Classify changed surfaces — not finished

Checks matching this repository's non-blocking checks never gate Ultrafix. The loop continues automatically once the blocking checks pass; if they have not settled within 2 hours, Ultrafix stops.

@propr-dev

propr-dev Bot commented Nov 12, 2026

Copy link
Copy Markdown
Contributor Author

CI failed: Full Test Suite Shard 4/4

Please investigate and fix this CI failure.

  • Check: Full Test Suite Shard 4/4
  • Result: failure
  • Commit: 0c724d69adbe (0c724d69adbe0d5422eadbe7eae1bcd67efa74b2)
  • Details: View CI failure

Failure evidence

.github:11
Process completed with exit code 1.

.github:41428
Process completed with exit code 1.

.github:2
Node.js 20 is deprecated. The following actions target Node.js 20 but are being forced to run on Node.js 24: actions/checkout@11d5960. For more information see: https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/

@propr-dev

propr-dev Bot commented Nov 12, 2026

Copy link
Copy Markdown
Contributor Author

🔄 Starting work on follow-up changes requested by @propr-dev[bot]

I'll analyze the 1 request and implement the necessary changes.

View Task Progress


Processing comment ID: 6830820488✓

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants