Skip to content

feat: implement Phase 1 capture and CI gate - #1

Merged
DSCmatter merged 4 commits into
mainfrom
capture-sdk
Aug 17, 2026
Merged

feat: implement Phase 1 capture and CI gate#1
DSCmatter merged 4 commits into
mainfrom
capture-sdk

Conversation

@DSCmatter

@DSCmatter DSCmatter commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

User description

Implement Event and AgentClock with thread-safe logical sequence allocation.

Add Anthropic model-call capture, tool invocation/result capture, memory operations, and agent spawning with spawned_at_event_id.

Add PostgreSQL storage, unit tests, integration tests, CI automation, local check scripts, and contributor documentation.

Keep Phase 2+ functionality out of scope.

Validation: 8 tests passed, Ruff passed, and ty passed.


CodeAnt-AI Description

Add Phase 1 event capture for multi-agent workflows

What Changed

  • Records Anthropic model calls, including inputs, outputs, errors, and latency, without changing the returned response
  • Captures tool calls and results, preserves causal links, and prevents repeated invocations from running twice
  • Records memory reads, writes, and deletes with previous and resulting values
  • Tracks agent spawning, parent-child relationships, causal parents, and per-agent logical ordering
  • Stores captured events and agents in memory or PostgreSQL with idempotent retries
  • Adds unit and PostgreSQL integration tests, local checks, CI validation, and setup guidance

Impact

✅ Traceable model, tool, and memory activity
✅ Safe retries without duplicate tool execution
✅ Reliable parent-child event relationships

💡 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.

Implement Event and AgentClock with thread-safe logical sequence allocation.

Add Anthropic model-call capture, tool invocation/result capture, memory operations, and agent spawning with spawned_at_event_id.

Add PostgreSQL storage, unit tests, integration tests, CI automation, local check scripts, and contributor documentation.

Keep Phase 2+ functionality out of scope.

Validation: 8 tests passed, Ruff passed, and ty passed.
@DSCmatter DSCmatter self-assigned this Aug 17, 2026
@DSCmatter DSCmatter added documentation Improvements or additions to documentation enhancement New feature or request labels Aug 17, 2026
@codeant-ai

codeant-ai Bot commented Aug 17, 2026

Copy link
Copy Markdown

🤖 CodeAnt AI — Review Status

Status Commit Started (UTC) Finished (UTC)
✅ Reviewed your PR 007af9d Aug 17, 2026 · 07:07 07:11

@codeant-ai

codeant-ai Bot commented Aug 17, 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:XXL This PR changes 1000+ lines, ignoring generated files label Aug 17, 2026
Comment thread sdk/lifecycle.py Outdated
Comment on lines +69 to +72
if existing_event is not None:
existing_agent = agent_store.get_by_spawn_event(existing_event.id)
if existing_agent is not None:
return existing_agent, existing_event, AgentClock(existing_agent.lamport_offset)

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 retry path reconstructs a child clock from the persisted lamport_offset, but later child allocations are not written back to that record. After a retry or process restart, the returned clock can allocate sequence numbers already used by the child, causing duplicate logical sequences and PostgreSQL uniqueness failures. Persist or otherwise recover the child's latest logical sequence before returning a reconstructed clock. [stale reference]

Severity Level: Major ⚠️
- ❌ Retried child captures can fail PostgreSQL sequence uniqueness.
- ⚠️ Subsequent child tool or model events may not persist.
- ⚠️ In-memory retries also reconstruct stale counters.

Fix in Cursor Fix in VSCode Claude

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

**Path:** sdk/lifecycle.py
**Line:** 69:72
**Comment:**
	*Stale Reference: The retry path reconstructs a child clock from the persisted `lamport_offset`, but later child allocations are not written back to that record. After a retry or process restart, the returned clock can allocate sequence numbers already used by the child, causing duplicate logical sequences and PostgreSQL uniqueness failures. Persist or otherwise recover the child's latest logical sequence before returning a reconstructed clock.

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 sdk/memory.py Outdated
Comment on lines +58 to +66
def set(self, key: str, value: Any, *, causal_parent_ids: Iterable[str] = ()) -> None:
before = self.store.get(key, _MISSING)
self.store[key] = value
self._event(
"memory_write",
{
"operation": "set",
"key": key,
"before": None if before is _MISSING else before,

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: Memory mutation and event recording are separate unsynchronized operations. Concurrent writers can compute before from the same old value, overwrite each other, and emit events whose before/after metadata does not describe the actual serialized state transition. Protect the store mutation and corresponding event creation with one lock or use an atomic storage operation. [race condition]

Severity Level: Major ⚠️
- ⚠️ Shared memory writes can report incorrect transition history.
- ⚠️ Concurrent agents may overwrite each other's values.
- ⚠️ Replay or audit consumers receive inconsistent memory events.

Fix in Cursor Fix in VSCode Claude

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

**Path:** sdk/memory.py
**Line:** 58:66
**Comment:**
	*Race Condition: Memory mutation and event recording are separate unsynchronized operations. Concurrent writers can compute `before` from the same old value, overwrite each other, and emit events whose before/after metadata does not describe the actual serialized state transition. Protect the store mutation and corresponding event creation with one lock or use an atomic storage operation.

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 sdk/tools.py Outdated
Comment on lines +15 to +22
def _find_events(log: Any, invocation_id: str) -> list[Event]:
if hasattr(log, "events"):
events = log.events()
elif hasattr(log, "__iter__"):
events = list(log)
else:
events = []
return [event for event in events if event.payload.get("invocation_id") == invocation_id]

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: Retry detection does not query stores that expose only get_by_idempotency_key, such as PostgresEventStore. _find_events therefore returns no prior events for PostgreSQL retries, so the wrapped tool executes again and repeats its side effects even though the subsequent event inserts are deduplicated. Query the store by agent and invocation idempotency key before invoking the tool. [api mismatch]

Severity Level: Major ⚠️
- ❌ PostgreSQL retries can repeat non-idempotent tool actions.
- ⚠️ Deduplicated events conceal the repeated execution.
- ⚠️ External side effects are not replay-safe.

Fix in Cursor Fix in VSCode Claude

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

**Path:** sdk/tools.py
**Line:** 15:22
**Comment:**
	*Api Mismatch: Retry detection does not query stores that expose only `get_by_idempotency_key`, such as `PostgresEventStore`. `_find_events` therefore returns no prior events for PostgreSQL retries, so the wrapped tool executes again and repeats its side effects even though the subsequent event inserts are deduplicated. Query the store by agent and invocation idempotency key before invoking the tool.

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 sdk/tools.py Outdated
Comment on lines +34 to +39
agent_id = kwargs.pop("agent_id", configured_agent_id)
clock = kwargs.pop("clock", configured_clock)
log = kwargs.pop("log", configured_log)
run_id = kwargs.pop("run_id", configured_run_id)
causal_parent_ids = kwargs.pop("causal_parent_ids", ())
invocation_id = kwargs.pop("invocation_id", None) or str(uuid4())

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: These reserved keyword removals make the decorator incompatible with tools that legitimately declare parameters named agent_id, clock, log, run_id, causal_parent_ids, or invocation_id: those arguments are consumed as capture context and are never passed to the wrapped function, causing missing arguments or altered tool behavior. Use a separate context mechanism rather than unconditionally removing names from the tool's call arguments. [api mismatch]

Severity Level: Major ⚠️
- ❌ Tools cannot receive reserved context-named inputs.
- ⚠️ Required tool parameters can raise `TypeError`.
- ⚠️ Valid tool calls may silently change behavior.

Fix in Cursor Fix in VSCode Claude

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

**Path:** sdk/tools.py
**Line:** 34:39
**Comment:**
	*Api Mismatch: These reserved keyword removals make the decorator incompatible with tools that legitimately declare parameters named `agent_id`, `clock`, `log`, `run_id`, `causal_parent_ids`, or `invocation_id`: those arguments are consumed as capture context and are never passed to the wrapped function, causing missing arguments or altered tool behavior. Use a separate context mechanism rather than unconditionally removing names from the tool's call arguments.

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 sdk/tools.py Outdated
Comment on lines +43 to +49
prior = _find_events(log, invocation_id)
prior_result = next((event for event in prior if event.event_type == "tool_result"), None)
if prior_result is not None:
return prior_result.payload.get("output")
prior_error = next((event for event in prior if event.event_type == "agent_error"), None)
if prior_error is not None:
raise RuntimeError(prior_error.payload.get("error", "captured tool failed"))

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: Prior events are matched only by invocation_id, so a shared log can return another agent's result or error when two agents reuse the same invocation identifier. Include the current agent_id and, where applicable, run_id in the lookup criteria to match the persistence idempotency scope. [incorrect condition logic]

Severity Level: Major ⚠️
- ❌ Shared logs can return another agent's tool output.
- ⚠️ Agents may skip required tool execution.
- ⚠️ Agent errors can incorrectly abort unrelated work.

Fix in Cursor Fix in VSCode Claude

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

**Path:** sdk/tools.py
**Line:** 43:49
**Comment:**
	*Incorrect Condition Logic: Prior events are matched only by `invocation_id`, so a shared log can return another agent's result or error when two agents reuse the same invocation identifier. Include the current `agent_id` and, where applicable, `run_id` in the lookup criteria to match the persistence idempotency scope.

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 sdk/client.py
Comment on lines +54 to +64
latency_ms = int((time.monotonic() - start) * 1000)
record_event(
agent_id=self._owner.agent_id,
clock=self._owner.clock,
log=self._owner.log,
event_type="model_call",
payload={
"model": kwargs.get("model"),
"input": kwargs.get("messages"),
"output": _dump_response(response),
"latency_ms": latency_ms,

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: When messages.create is called with Anthropic streaming enabled, the returned object is a stream that is consumed after this method returns. Calling _dump_response immediately records the stream object's representation instead of the generated message, and errors occurring during iteration are never captured as agent_error events. The wrapper must finalize or wrap the stream so capture occurs when consumption completes. [api mismatch]

Severity Level: Major ⚠️
- ❌ Streaming outputs are absent from model-call events.
- ⚠️ Streaming iteration failures are not captured.
- ⚠️ Recorded latency excludes stream consumption time.

Fix in Cursor Fix in VSCode Claude

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

**Path:** sdk/client.py
**Line:** 54:64
**Comment:**
	*Api Mismatch: When `messages.create` is called with Anthropic streaming enabled, the returned object is a stream that is consumed after this method returns. Calling `_dump_response` immediately records the stream object's representation instead of the generated message, and errors occurring during iteration are never captured as `agent_error` events. The wrapper must finalize or wrap the stream so capture occurs when consumption completes.

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
Comment on lines +43 to +49
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
run_id uuid NOT NULL REFERENCES runs(id),
agent_id uuid NOT NULL REFERENCES agents(id),
logical_seq bigint NOT NULL,
wall_time timestamptz NOT NULL DEFAULT now(),
event_type event_type NOT NULL,
causal_parent_ids uuid[] NOT NULL DEFAULT '{}',

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: These PostgreSQL columns require UUID values, but the SDK accepts arbitrary strings for Event.id, run_id, agent_id, and causal_parent_ids. Events using valid in-memory identifiers such as planner or parent-1 will fail with a PostgreSQL UUID cast error. Align the schema with the SDK's string contract or validate and convert identifiers before persistence. [type error]

Severity Level: Major ⚠️
- ❌ PostgreSQL capture fails for non-UUID SDK identifiers.
- ❌ Events cannot persist with default or local-style IDs.
- ⚠️ In-memory and PostgreSQL backends have incompatible contracts.

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:** 43:49
**Comment:**
	*Type Error: These PostgreSQL columns require UUID values, but the SDK accepts arbitrary strings for `Event.id`, `run_id`, `agent_id`, and `causal_parent_ids`. Events using valid in-memory identifiers such as `planner` or `parent-1` will fail with a PostgreSQL UUID cast error. Align the schema with the SDK's string contract or validate and convert identifiers before persistence.

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
(event.agent_id, event.idempotency_key),
)
row = cursor.fetchone()
self.connection.commit()

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 insert can fail on the separate unique (agent_id, logical_seq) constraint, but this method has no rollback path before returning or propagating the database exception. A caller that catches the exception and reuses the connection will encounter PostgreSQL's aborted-transaction state on every subsequent operation. Roll back the connection on append failures before propagating the error. [possible bug]

Severity Level: Major ⚠️
- ❌ One failed append poisons subsequent connection operations.
- ⚠️ Retry and recovery workflows require external rollback.
- ⚠️ Shared worker connections may remain unusable.

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:** 115:115
**Comment:**
	*Possible Bug: The insert can fail on the separate unique `(agent_id, logical_seq)` constraint, but this method has no rollback path before returning or propagating the database exception. A caller that catches the exception and reuses the connection will encounter PostgreSQL's aborted-transaction state on every subsequent operation. Roll back the connection on append failures before propagating the error.

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
👍 | 👎

Recover the latest child-agent logical sequence when handling spawn retries.

Synchronize memory mutations with their captured before/after events and scope tool retry lookups by agent and idempotency key.

Preserve wrapped-tool parameters when capture context is configured, while supporting explicit capture overrides.

Capture Anthropic streaming responses after consumption and record streaming failures.

Validate UUID identifiers before PostgreSQL persistence and roll back failed event or agent transactions.

Add regression tests covering retries, concurrency, streaming, UUID validation, transaction rollback, and reserved tool parameters.

Validation:
- 15 tests passed
- Ruff passed
- ty passed
- Neon PostgreSQL integration passed
@DSCmatter

Copy link
Copy Markdown
Contributor Author

@CodeAnt-AI can you review this again? I have pushed those fixes and validated them

@codeant-ai

codeant-ai Bot commented Aug 17, 2026

Copy link
Copy Markdown

Question: can you review this again? i have pushed those fixes and validated them

Answer:
I found two remaining correctness issues worth fixing before merging.

1. Tool idempotency is not safe for concurrent retries

File: sdk/tools.py, around the prior = _find_events(...) check and the subsequent fn(*args, **kwargs) call.

The retry protection works for sequential retries, but two concurrent calls with the same invocation_id can both observe no existing result and both execute the underlying tool:

prior = _find_events(log, agent_id, invocation_id)
...
invoke = record_event(...)
result = fn(*args, **kwargs)

InMemoryEventLog.append() deduplicates the tool_call event, but it does not coordinate execution of fn. PostgreSQL has the same race: both transactions can attempt the insert, and both callers can proceed to execute the tool.

This violates the stated guarantee that repeated invocations are prevented from running twice. A lock or claim/lease mechanism is needed around the invocation lifecycle. For PostgreSQL, this likely requires an atomic claim table or a transaction/advisory lock keyed by (agent_id, invocation_id), followed by storing and retrieving the completed result.

Please add a concurrent retry test, for example using a barrier inside the tool and asserting that the underlying function's call count remains 1.

2. Spawn retries without an explicit child ID are not idempotent

File: sdk/lifecycle.py, in spawn_agent():

child_id = child_agent_id or str(uuid4())
spawn_key = idempotency_key or f"agent-spawn:{child_id}"

When neither child_agent_id nor idempotency_key is supplied, every retry generates a new UUID and therefore a new idempotency key. The docstring says that retries are stable when a stable child ID or idempotency key is supplied, so this may be intentional, but the broader PR description says spawning supports idempotent retries and the default behavior is easy to misuse.

Consider either:

  • requiring one of child_agent_id or idempotency_key,
  • documenting prominently that retries are only idempotent when one is supplied, or
  • deriving the idempotency key from a caller-provided operation/request ID.

Also add a test covering the retry behavior when only idempotency_key is provided.

The sequential tests and validation results are useful, but they do not exercise the main remaining race: concurrent duplicate tool invocations.

- serialize duplicate tool invocations per agent and invocation ID
- add PostgreSQL advisory locking for concurrent retries
- require retry-stable identity when spawning agents
- add concurrent tool and spawn retry regression tests
- update PostgreSQL setup documentation
- validate with unit tests, Neon integration, Ruff, and ty
@DSCmatter

Copy link
Copy Markdown
Contributor Author

@CodeAnt-AI can you review this again? I have pushed those fixes and validated them

@codeant-ai

codeant-ai Bot commented Aug 17, 2026

Copy link
Copy Markdown

Question: can you review this again? i have pushed those fixes and validated them

Answer:
I reviewed the updated implementation. The core Phase 1 behavior looks substantially improved, but I found a few remaining issues worth addressing before merging.

1. PostgreSQL event idempotency does not make logical sequence allocation safe

record_event() allocates a sequence before calling PostgresEventStore.append():

sequence = next_seq(clock, causal_parent_seqs)
return _append(log, Event(...))

The PostgreSQL schema enforces:

UNIQUE (agent_id, logical_seq)

However, two processes using separate AgentClock instances for the same agent can allocate the same sequence concurrently. One insert will then fail with a unique-constraint error. The idempotency key only protects retries after an event has already been inserted; it does not coordinate sequence allocation.

This is especially relevant because the PostgreSQL tool lock serializes one invocation identity, not all event writes for an agent.

Consider one of these approaches:

  • Allocate the next sequence in PostgreSQL under an agent-scoped advisory lock.
  • Maintain a per-agent sequence row and increment it transactionally.
  • Document that each agent must have exactly one shared AgentClock across all writers and reject/process duplicate-sequence conflicts explicitly.

Without this, the storage layer does not fully guarantee the “no duplicate logical sequences within an agent” invariant under multi-process usage.

2. spawn_agent() can return an object inconsistent with the persisted agent

When child_agent_id is supplied with a different idempotency key, create_agent() uses:

ON CONFLICT (id) DO NOTHING

but returns the newly constructed AgentRecord regardless of whether the insert occurred. For example:

  1. A child with ID child-1 is persisted from spawn event A.
  2. A later request uses the same child_agent_id but a different idempotency key.
  3. The new spawn event B is recorded.
  4. create_agent() does nothing because child-1 already exists.
  5. spawn_agent() returns an in-memory child pointing to event B, while PostgreSQL still points to event A.

The returned record can therefore disagree with the database. PostgresEventStore.create_agent() should return the existing persisted row when the insert is skipped, or spawn_agent() should detect an existing agent by ID and treat conflicting spawn requests as an error/idempotent retry.

3. Memory events conflate “missing” with “stored None

CapturedMemory correctly distinguishes missing values for the return value, but its event payload does not:

"before": None if before is _MISSING else before

A missing key and a key whose value is actually None both produce the same payload. The same applies to reads, where "value": None is paired with "found", but writes and deletes have no equivalent presence indicator.

For reliable reconstruction, include fields such as:

"before_found": before is not _MISSING,
"after_found": True,

and for deletes:

"after_found": False,

Otherwise consumers cannot determine whether a write changed a missing key or replaced an existing None.

4. Stream cleanup can lose the model event

_CapturedStream.close() only closes the underlying stream:

def close(self) -> None:
    ...
    close()

It does not call _finish() or _fail(). If a caller partially consumes a stream and then closes it—as is common when cancelling or abandoning a streaming response—no capture event is recorded.

Also, in __exit__, the underlying stream’s __exit__ is called before _finish()/_fail(). If the underlying __exit__ raises, the capture event is skipped.

The wrapper should finalize in close(), and ideally use try/finally in __exit__ so capture is persisted even if underlying cleanup fails. Partial/cancelled streams should be represented explicitly, rather than silently disappearing.

These are not issues with the basic unit-test paths, but they affect the concurrency, retry, and persistence guarantees described in the PR.

- allocate PostgreSQL logical sequences atomically per agent
- preserve persisted agent records across spawn conflicts
- distinguish missing memory values from stored None
- capture abandoned and cleanup-failed streams
- add regression and PostgreSQL concurrency tests
- validate with tests, Neon, Ruff, and ty
@DSCmatter

Copy link
Copy Markdown
Contributor Author

@CodeAnt-AI now all good?

@codeant-ai

codeant-ai Bot commented Aug 17, 2026

Copy link
Copy Markdown

Question: now all good?

Answer:
Not quite. I found one CI-blocking issue:

  • .github/workflows/ci.yml runs uv sync --locked, but the PR does not add an uv.lock file.
    uv sync --locked fails when no lockfile exists, so both CI jobs will fail before running tests. Generate and commit the lockfile:

    uv lock
    git add uv.lock

    Alternatively, change CI to uv sync, though committing the lockfile is preferable for a CI gate.

One additional concern:

  • record_event() allocates a sequence before checking idempotency. A retry with the same idempotency key returns the existing event but still advances the in-memory/PostgreSQL Lamport state, leaving sequence gaps. That may be acceptable if gaps are explicitly allowed, but if logical sequences are intended to represent every persisted event contiguously, idempotency lookup should happen before allocation.

@DSCmatter
DSCmatter merged commit 7e3db4c into main Aug 17, 2026
2 checks passed
@DSCmatter
DSCmatter deleted the capture-sdk branch August 17, 2026 11:31
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