Skip to content

feat: implement phase 2 causal event graph - #2

Merged
DSCmatter merged 4 commits into
mainfrom
feat/graph
Aug 19, 2026
Merged

feat: implement phase 2 causal event graph#2
DSCmatter merged 4 commits into
mainfrom
feat/graph

Conversation

@DSCmatter

@DSCmatter DSCmatter commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

User description

  • add PostgreSQL snapshots table and graph indexes
  • implement explicit cross-agent causal parent assignment
  • add PostgreSQL-backed ancestors(event_id) traversal
  • add real three-agent Phase 2 integration coverage
  • validate planner and worker branches against the fixture
  • update CI, setup, contribution, and Neon verification docs

CodeAnt-AI Description

Add cross-agent causal graph tracking and reliable event retries

What Changed

  • Events can explicitly link to multiple results from different agents, allowing planner merges to preserve both worker branches.
  • PostgreSQL can return an event and all reachable causal ancestors, including shared ancestors only once.
  • Invalid, duplicate, missing, or cross-run causal parents are rejected.
  • Retrying an idempotent event returns the original event without allocating another sequence number, including after failed or concurrent writes.
  • Added Phase 2 database structures, integration coverage, and verification guidance for three-agent graphs.

Impact

✅ Traceable planner-to-worker merges
✅ Complete deduplicated ancestry queries
✅ Safe idempotent event retries

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

- add PostgreSQL snapshots table and graph indexes
- implement explicit cross-agent causal parent assignment
- add PostgreSQL-backed ancestors(event_id) traversal
- add real three-agent Phase 2 integration coverage
- validate planner and worker branches against the fixture
- update CI, setup, contribution, and Neon verification docs
@DSCmatter DSCmatter self-assigned this Aug 18, 2026
@DSCmatter DSCmatter added documentation Improvements or additions to documentation enhancement New feature or request labels Aug 18, 2026
@codeant-ai

codeant-ai Bot commented Aug 18, 2026

Copy link
Copy Markdown

🤖 CodeAnt AI — Review Status

Status Commit Started (UTC) Finished (UTC)
✅ Incremental review completed e8efdeb Aug 19, 2026 · 13:59 13:59
✅ Reviewed your PR 6efa671 Aug 18, 2026 · 06:26 06:29

@codeant-ai

codeant-ai Bot commented Aug 18, 2026

Copy link
Copy Markdown

Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@codeant-ai codeant-ai Bot added the size:XL This PR changes 500-999 lines, ignoring generated files label Aug 18, 2026
Comment thread core/graph.py Outdated
) -> Event:
"""Append an event whose explicit parents were used by the caller."""
parent_ids = list(causal_parents)
logical_seq = assign_causal_parents(agent_id, clock, parent_ids, log)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: Sequence allocation occurs before log.append(event). With PostgresEventStore, allocation commits the agent's lamport_offset in a separate transaction, so a failed append permanently consumes a sequence, and retrying an idempotent event allocates another sequence even though the append returns the previously stored event. This leaves durable Lamport state ahead of persisted events and creates unnecessary gaps; allocation and idempotent append need to share one transaction or the existing idempotent event must be checked before allocation. [logic error]

Severity Level: Major ⚠️
- ⚠️ Durable Lamport offsets can exceed persisted event sequences.
- ⚠️ Failed writes and idempotent retries create unnecessary sequence gaps.
- ⚠️ Sequence-based event inspection sees unexplained missing positions.

Fix in Cursor Fix in VSCode Claude

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** core/graph.py
**Line:** 47:47
**Comment:**
	*Logic Error: Sequence allocation occurs before `log.append(event)`. With `PostgresEventStore`, allocation commits the agent's `lamport_offset` in a separate transaction, so a failed append permanently consumes a sequence, and retrying an idempotent event allocates another sequence even though the append returns the previously stored event. This leaves durable Lamport state ahead of persisted events and creates unnecessary gaps; allocation and idempotent append need to share one transaction or the existing idempotent event must be checked before allocation.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment thread storage/postgres.py Outdated
Comment on lines +218 to +220
SELECT e.id, e.causal_parent_ids
FROM events e
JOIN ancestors a ON e.id = ANY(a.causal_parent_ids)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The recursive traversal follows every referenced UUID globally and does not constrain the joined event to the target event's run_id. Because record_causal_event only verifies that each parent exists, an event in one run can reference an event from another run, causing ancestors() to merge unrelated run histories. Validate parent run ownership before insertion and/or carry the root run ID through the recursive query. [api mismatch]

Severity Level: Major ⚠️
- ❌ Run-specific causal graphs include unrelated history.
- ⚠️ Phase 2 planner and worker provenance can be misattributed.
- ⚠️ Ancestor-based debugging returns cross-run events.

Fix in Cursor Fix in VSCode Claude

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** storage/postgres.py
**Line:** 218:220
**Comment:**
	*Api Mismatch: The recursive traversal follows every referenced UUID globally and does not constrain the joined event to the target event's `run_id`. Because `record_causal_event` only verifies that each parent exists, an event in one run can reference an event from another run, causing `ancestors()` to merge unrelated run histories. Validate parent run ownership before insertion and/or carry the root run ID through the recursive query.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

@DSCmatter

Copy link
Copy Markdown
Contributor Author

@CodeAnt-AI does my new push fix those changes?

@codeant-ai

codeant-ai Bot commented Aug 18, 2026

Copy link
Copy Markdown

Question: does my new push fix those changes?

Answer:
The push fixes most of the requested Phase 2 functionality, but there is still one important correctness issue in the PostgreSQL idempotency path.

Remaining issue: rollback can invalidate the fetched row

In PostgresEventStore.append, when the insert conflicts on an idempotency key, the code does this:

row = cursor.fetchone()

if row is None and event.idempotency_key is not None:
    cursor.execute(...)
    row = cursor.fetchone()
    if row is not None:
        self.connection.rollback()

if row is not None:
    self.connection.commit()

After rollback(), the transaction is reset, but the code then commits and converts the previously fetched row:

return self._row_to_event(row)

Depending on the psycopg behavior and transaction state, this is fragile and also unnecessarily commits after a rollback. More importantly, the rollback may undo unrelated work in the same transaction, including the sequence allocation—which is intentional for this operation—but it can also discard other caller changes if the store is used inside a larger transaction.

A safer approach is to distinguish the conflict case and return immediately after rollback:

if row is None and event.idempotency_key is not None:
    cursor.execute(...)
    row = cursor.fetchone()
    if row is not None:
        self.connection.rollback()
        return self._row_to_event(row)

Then commit only for a newly inserted event:

self.connection.commit()
return self._row_to_event(row)

Alternatively, use a savepoint around the insert/idempotency lookup so a retry does not roll back the caller’s entire transaction.

Also worth fixing

The new append validation checks:

owned_parent_ids != set(parent_ids)

This validates that every parent exists in the same run, but it does not reject duplicate causal parent IDs. If duplicate edges are invalid by contract, validate that explicitly:

if len(parent_ids) != len(set(parent_ids)):
    raise ValueError("causal parent IDs must be unique")

Conclusion

The push appears to address the graph traversal, explicit cross-agent parents, shared ancestor deduplication, schema/indexes, and integration coverage. However, I would not consider it fully fixed until the idempotent PostgreSQL retry transaction handling is changed, preferably using an immediate return after rollback or a savepoint.

@codeant-ai

codeant-ai Bot commented Aug 19, 2026

Copy link
Copy Markdown

Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@codeant-ai codeant-ai Bot added size:XXL This PR changes 1000+ lines, ignoring generated files and removed size:XL This PR changes 500-999 lines, ignoring generated files labels Aug 19, 2026
@DSCmatter

Copy link
Copy Markdown
Contributor Author

@CodeAnt-AI what do you think now?

@codeant-ai

codeant-ai Bot commented Aug 19, 2026

Copy link
Copy Markdown

Question: what do you think now?

Answer:
The Phase 2 implementation looks substantially better: the causal edges are explicit, ancestry is deduplicated with a recursive UNION, sequence allocation now participates in the append transaction, and the integration coverage exercises a realistic three-agent merge.

I do see one important idempotency edge case:

PostgresEventStore.append() validates parents before resolving an idempotency conflict

In append(), causal-parent validation happens before the INSERT ... ON CONFLICT DO NOTHING and subsequent lookup of the existing idempotent event:

if parent_ids:
    cursor.execute(...)
    ...
    if owned_parent_ids != set(parent_ids):
        raise ValueError(...)

cursor.execute(sql, ...)

This means a retry using the same (agent_id, idempotency_key) can fail instead of returning the original event if the retry supplies different, missing, duplicate, or cross-run parents. The retry contract says that an idempotent retry should return the original event without allocating another sequence number, regardless of the retry payload.

This can occur in a race too: the initial get_by_idempotency_key() check may return no row, another writer commits the event, and this call then validates its retry parents before discovering the conflict.

Consider checking for an existing idempotency key inside the same transaction before parent validation, or handling the conflict lookup before rejecting retry-specific metadata. For example, the append path should effectively do:

  1. Look up the existing idempotent event.
  2. Return it immediately if present.
  3. Only validate parents and insert when no existing event exists.
  4. Preserve the current rollback behavior for sequence allocation.

I would also move the duplicate-parent validation into assign_causal_parents() itself, not only record_causal_event(), since assign_causal_parents() is public and the requirement is that invalid duplicate causal parents be rejected through either API.

@DSCmatter
DSCmatter merged commit 2817601 into main Aug 19, 2026
2 checks passed
@DSCmatter
DSCmatter deleted the feat/graph branch August 19, 2026 14:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request size:XXL This PR changes 1000+ lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant