Feature/enhance automation nodes - #375
Conversation
…orkflows - Added WaitScheduler to periodically poll for wait action nodes and resume their paused graph walks. - Introduced new database migrations to support pending agent waits and delays. - Enhanced existing test cases to cover scenarios involving wait nodes and agent conversations. - Updated e2e tests to validate the behavior of automation workflows with wait nodes and agent interactions. - Refactored fake repository implementations to accommodate new methods for handling run steps and pending waits.
- Changed automation status from three states (draft, active, archived) to two (active, inactive). - Updated related API endpoints and service methods to reflect the new status model. - Removed references to draft and archived statuses in the codebase. - Adjusted UI components to handle the new active/inactive toggle. - Updated database schema to support the new status definitions. - Modified tests to ensure compatibility with the new automation lifecycle.
CI lint job was failing on E501 in executor.py and streams.py. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Reviewed PR #375. This is a large but well-scoped change: sprint-scoped triggers/conditions/actions, wait/trigger_ai_agent pause/resume, variable interpolation, and collapsing the automation lifecycle to active/inactive.
The test coverage is strong and the migrations are safe, but I found a critical durability bug in the pause/resume path and a high-severity fan-out AI-agent semantics bug that should be fixed before merge. I’ve also flagged a handful of medium/low issues inline.
Blocking issues
- Pause/resume can permanently stall a run — both
ClaimPendingAgentWaitandClaimDueDelaysdelete the pending row before the graph walk resumes. If the process crashes orresumeWalk/resumeAfterDelayerrors after the delete, the row is gone and the run can never continue. - Fan-out
trigger_ai_agentresumes downstream on the first finished conversation — when the action fans out to multiple conversations, each terminal status event walks the same outgoing edges, so downstream actions run before the remaining conversations have resolved.
Other concerns
- Front-end
waitnode accepts fractional minutes while the backend stores an*int. *target = *updatedinapplyUpdateSprintmutates a repository-returned pointer in place.call_apiinterpolates user values into URLs/headers without encoding/sanitization.- Sprint status from the activity stream is cast without enum validation.
WaitScheduler.Starttakes acontext.Contextit never uses.- Custom-field diff uses
fmt.Sprintf("%v", …)comparison. - Removing
/archiveand/revert-to-draftis a breaking API change; it should be documented or aliased for one release.
Please address the two blocking issues at minimum; the rest can be handled here or in follow-ups.
Kimi K2 (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
Following up on the review above with line-anchored details on the blocking correctness/durability issues. Please address at least the two blocking items (pause/resume durability and fan-out AI-agent semantics) before merging; the rest are inline suggestions.
Kimi K2 (free via Pullfrog for OSS) | 𝕏
|
Line-anchored follow-up to the review above. The two blocking issues are #1 and #2; please address them before merging. 1. Critical — pause/resume can permanently stall a run (
|
The persistence fixture mocked publish_realtime and publish_event but was never updated for the newer publish_conversation_status, so test_llm_failure_marks_conversation_failed fell through to a real Valkey connection and failed on DNS resolution in CI (no valkey service in that job — this suite mocks out Postgres/Valkey entirely by design, per its own module docstring). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… 6 other issues
Verified and fixed the 9 issues from the automated PR review:
- Critical: ClaimPendingAgentWait/ClaimDueDelays deleted their row before
the resume was confirmed to succeed, so a crash or error mid-resume lost
it forever with no retry possible. Replaced with find/list-then-delete-
on-success across both the trigger_ai_agent and wait node paths, and
fixed the resulting finalizeRunIfDone double-counting (it now runs after
the delete, not before).
- High: a trigger_ai_agent node fanned out to several conversations
sharing one NodeID, and each terminal-status event independently walked
that node's outgoing edges — so the first conversation to finish fired
downstream actions while its siblings were still in flight. Added
CountPendingAgentWaitsForNode to gate on every sibling resolving (and
none failing) before continuing.
- Medium: the wait node's minutes input accepted fractional values in the
UI while the backend stores an int — now floored, with step=1.
- Medium: applyUpdateSprint/applyCompleteSprint mutated a resolved sprint
pointer in place unconditionally, including when it came from
sprintRepo (possibly cache-shared) rather than the walker's own object —
now only mutates the walker-owned case.
- Low: call_api interpolated {{variable}} values directly into URLs and
headers with no escaping. Added vartemplate.RenderEscaped, applied via
url.QueryEscape for the URL and newline-stripping for headers.
- Low: sprint status from the activity stream was cast to SprintStatus
with no validation — now checked against ValidSprintStatuses.
- Low: WaitScheduler.Start accepted a context it never used — now wired
through so cancellation actually stops the scheduler.
- Low: custom-field diffing compared values via fmt.Sprintf("%v", ...),
which treats a number and its string form as equal — switched to
JSON-marshal comparison.
- Note: the /archive and /revert-to-draft removal is a deliberate,
already-documented breaking change (see the PR description) with no
external consumers — no code change needed.
New regression tests for the fan-out gate and custom-field comparison
verified to fail without their respective fixes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
ℹ️ One concurrency issue remains. The prior review's blocking issues (pause/resume durability, fan-out gating, wait-minutes mismatch, call_api escaping, sprint cache aliasing, sprint status validation, custom-field diff, WaitScheduler context) are all resolved in the commits since the last review.
The remaining issue is a read-modify-write race in the fan-out gate: resumeWalk counts pending agent waits before deleting the completed row, so two concurrent consumers can both see stillPending > 1 and skip calling resumeWalkFrom, leaving the run stalled. The fix is to delete the resolved row first, then count remaining siblings.
Suggested inline at services/api/internal/worker/automation_consumer.go:1079.
Kimi K2 (free via Pullfrog for OSS) | 𝕏
|
Blocking feedback converted to a PR comment: the fan-out gate in At Suggested change: // Delete this wait before counting so the count reflects only siblings,
// and concurrent resumes see a consistent remaining total.
if err := c.repo.DeleteAgentWaitForAutomationNode(ctx, wait.ID); err != nil {
return fmt.Errorf("delete completed agent wait: %w", err)
}
stillPending, err := c.repo.CountPendingAgentWaitsForNode(ctx, wait.RunID, wait.NodeID)
if err != nil {
return fmt.Errorf("count pending agent waits for node: %w", err)
}
if stillPending > 0 {
return nil
}This deletes the resolved wait first, then resumes the parent only when no pending siblings remain.
|
Two sibling trigger_ai_agent waits resolving concurrently on different consumer replicas could each read a stale sibling count via the old separate count-then-delete/delete-then-count pattern, causing the run to either stall forever or fire the downstream node twice. Replace CountPendingAgentWaitsForNode with a single atomic DeletePendingAgentWaitAndCountRemaining that deletes-and-counts inside one transaction serialized by a Postgres advisory lock keyed on (run_id, node_id). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- Closed the remaining read-modify-write race in the
trigger_ai_agentfan-out gate by replacing the separate count-then-delete with an atomicDeletePendingAgentWaitAndCountRemainingoperation. A transaction-scoped Postgres advisory lock keyed on(runID, nodeID)serializes sibling resolutions across consumer replicas, so the last wait always seesremaining == 0and proceeds exactly once.
Kimi K2 (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
ℹ️ No critical issues — minor suggestions inline.
Reviewed changes
- Hardened
WaitScheduler's Redis leader lock with per-acquisition tokens, compare-and-delete release, and per-delay lease renewal so a long backlog can't outlive the lock. - Made
StopConversationpublish a terminal "stopped" status toStreamAgentConversationStatusso atrigger_ai_agentnode paused waiting on this conversation resumes when the conversation is explicitly stopped. - Added E2E regression coverage for the explicit-stop resume path.
Kimi K2 (free via Pullfrog for OSS) | 𝕏

Closes #366
Summary
Adds synchronous AI-agent execution and the rest of the automation node types requested in #366, then simplifies the automation status model and cleans up a few things found along the way.
New automation capabilities
trigger_ai_agentaction now pauses the graph walk until the conversation it started actually finishes, instead of continuing to the next node the instant the conversation is created. Resumes via a new durableStreamAgentConversationStatusstream once the conversation reaches a terminal status.WaitScheduler(same leader-lock/ticker pattern as the existing due-date/cron schedulers).update_sprint,complete_sprint), fed by a new durableStreamSprintActivitiesstream so sprint events reach the automation engine the same way task events already do.{{variable}}interpolation — node config fields (agent messages, Call API URL/body/headers, task titles, sprint name/goal) can reference{{task.title}},{{sprint.name}}, and similar placeholders from the current run.Automation status: active/inactive only
Replaced the three-state
draft/active/archivedlifecycle with a singleactive/inactivetoggle.archivedpreviously locked a graph from being edited at all, which no longer made sense next to the pause/resume nodes above — inactive automations stay fully editable, matching howdraftalready worked. The three lifecycle endpoints (activate/archive/revert-to-draft) collapse to two idempotent ones (activate/deactivate), and the builder page's badge-plus-three-buttons cluster becomes a single switch.Other fixes and cleanup
trigger_ai_agentnode lost its sprint context on resume, failing the downstreamupdate_sprint/complete_sprintaction with "no sprint in context". The resume path only ever reconstructed the walk's task, never its sprint.task_id, thensprint_id) into a singlecontextJSONB column, so a future context type doesn't need its own migration.Testing
go build ./... && go vet ./... && gofmt -l . && go test ./...go test ./test/e2e/...), including new coverage for synchronous agent execution, Wait nodes, Sprint triggers/conditions/actions, variable interpolation, and the active/inactive lifecycletsc --noEmitandbiome check .