diff --git a/server/cmd/server/autopilot_listeners_test.go b/server/cmd/server/autopilot_listeners_test.go index 8bde24c6a..02a513936 100644 --- a/server/cmd/server/autopilot_listeners_test.go +++ b/server/cmd/server/autopilot_listeners_test.go @@ -117,9 +117,9 @@ func TestAutopilotRunOnlyTaskTerminalEventsUpdateRun(t *testing.T) { ); err != nil { t.Fatalf("mark task dispatched: %v", err) } - task, err := queries.StartAgentTask(ctx, run.TaskID) + task, err := queries.StartAgentTaskWithSupplement(ctx, db.StartAgentTaskWithSupplementParams{TaskID: run.TaskID}) if err != nil { - t.Fatalf("StartAgentTask: %v", err) + t.Fatalf("StartAgentTaskWithSupplement: %v", err) } tc.finalize(task) @@ -239,8 +239,8 @@ func runTaskWithBudget(t *testing.T, queries *db.Queries, taskID pgtype.UUID, ma ); err != nil { t.Fatalf("mark task dispatched: %v", err) } - if _, err := queries.StartAgentTask(context.Background(), taskID); err != nil { - t.Fatalf("StartAgentTask: %v", err) + if _, err := queries.StartAgentTaskWithSupplement(context.Background(), db.StartAgentTaskWithSupplementParams{TaskID: taskID}); err != nil { + t.Fatalf("StartAgentTaskWithSupplement: %v", err) } } diff --git a/server/cmd/server/quick_create_subscriber_test.go b/server/cmd/server/quick_create_subscriber_test.go index b14cf03cb..55a2b2613 100644 --- a/server/cmd/server/quick_create_subscriber_test.go +++ b/server/cmd/server/quick_create_subscriber_test.go @@ -69,8 +69,8 @@ func TestQuickCreateFailure_DoesNotSubscribeRequester(t *testing.T) { ); err != nil { t.Fatalf("dispatch task: %v", err) } - if _, err := queries.StartAgentTask(ctx, task.ID); err != nil { - t.Fatalf("StartAgentTask: %v", err) + if _, err := queries.StartAgentTaskWithSupplement(ctx, db.StartAgentTaskWithSupplementParams{TaskID: task.ID}); err != nil { + t.Fatalf("StartAgentTaskWithSupplement: %v", err) } // No issue with origin_type=quick_create + this task id exists. Completion @@ -139,8 +139,8 @@ func TestQuickCreateFailure_SurfacesAgentOutput(t *testing.T) { ); err != nil { t.Fatalf("dispatch task: %v", err) } - if _, err := queries.StartAgentTask(ctx, task.ID); err != nil { - t.Fatalf("StartAgentTask: %v", err) + if _, err := queries.StartAgentTaskWithSupplement(ctx, db.StartAgentTaskWithSupplementParams{TaskID: task.ID}); err != nil { + t.Fatalf("StartAgentTaskWithSupplement: %v", err) } // No issue is created; the agent exits with the CLI duplicate error as its @@ -211,8 +211,8 @@ func TestQuickCreateLookupFault_WritesUnconfirmedInbox(t *testing.T) { ); err != nil { t.Fatalf("dispatch task: %v", err) } - if _, err := queries.StartAgentTask(ctx, task.ID); err != nil { - t.Fatalf("StartAgentTask: %v", err) + if _, err := queries.StartAgentTaskWithSupplement(ctx, db.StartAgentTaskWithSupplementParams{TaskID: task.ID}); err != nil { + t.Fatalf("StartAgentTaskWithSupplement: %v", err) } // Completion runs against a Queries whose origin lookup always faults. The @@ -292,8 +292,8 @@ func TestQuickCreateFailure_RedactsAgentOutput(t *testing.T) { ); err != nil { t.Fatalf("dispatch task: %v", err) } - if _, err := queries.StartAgentTask(ctx, task.ID); err != nil { - t.Fatalf("StartAgentTask: %v", err) + if _, err := queries.StartAgentTaskWithSupplement(ctx, db.StartAgentTaskWithSupplementParams{TaskID: task.ID}); err != nil { + t.Fatalf("StartAgentTaskWithSupplement: %v", err) } // Fake, non-functional token shaped like a real GitHub PAT. @@ -361,8 +361,8 @@ func TestQuickCreateLookupCancelled_StillWritesUnconfirmedInbox(t *testing.T) { ); err != nil { t.Fatalf("dispatch task: %v", err) } - if _, err := queries.StartAgentTask(setupCtx, task.ID); err != nil { - t.Fatalf("StartAgentTask: %v", err) + if _, err := queries.StartAgentTaskWithSupplement(setupCtx, db.StartAgentTaskWithSupplementParams{TaskID: task.ID}); err != nil { + t.Fatalf("StartAgentTaskWithSupplement: %v", err) } // The completion transaction runs on a healthy context; the context dies at diff --git a/server/internal/daemon/client.go b/server/internal/daemon/client.go index 2643ff7df..4be7d41c5 100644 --- a/server/internal/daemon/client.go +++ b/server/internal/daemon/client.go @@ -204,9 +204,6 @@ func daemonCommonCapabilities() []string { return []string{ protocol.DaemonCapabilitySkillBundlesV1, protocol.DaemonCapabilityCoalescedCommentsV1, - protocol.DaemonCapabilityExecutionManifestV1, - protocol.DaemonCapabilityAgentSkillV1, - protocol.DaemonCapabilityRemoteMCPV1, protocol.DaemonCapabilityLocalWorktreeV1, protocol.DaemonCapabilitySourceContextQuickCreateV1, protocol.DaemonCapabilityRPCV1, diff --git a/server/internal/handler/comment.go b/server/internal/handler/comment.go index c10ba14fc..1f3f7c8f4 100644 --- a/server/internal/handler/comment.go +++ b/server/internal/handler/comment.go @@ -353,14 +353,11 @@ func (h *Handler) ListComments(w http.ResponseWriter, r *http.Request) { var sinceTime pgtype.Timestamptz if v := q.Get("since"); v != "" { + // RFC3339Nano parsing also accepts timestamps without fractional seconds. t, err := time.Parse(time.RFC3339Nano, v) if err != nil { - // Fall back to RFC3339 for backwards-compat with the original CLI. - t, err = time.Parse(time.RFC3339, v) - if err != nil { - writeError(w, http.StatusBadRequest, "invalid since parameter; expected RFC3339 format") - return - } + writeError(w, http.StatusBadRequest, "invalid since parameter; expected RFC3339 format") + return } sinceTime = pgtype.Timestamptz{Time: t, Valid: true} } @@ -489,11 +486,8 @@ func (h *Handler) ListComments(w http.ResponseWriter, r *http.Request) { if beforeTimeStr != "" { t, err := time.Parse(time.RFC3339Nano, beforeTimeStr) if err != nil { - t, err = time.Parse(time.RFC3339, beforeTimeStr) - if err != nil { - writeError(w, http.StatusBadRequest, "invalid before parameter; expected RFC3339 format") - return - } + writeError(w, http.StatusBadRequest, "invalid before parameter; expected RFC3339 format") + return } beforeCursor = pgtype.Timestamptz{Time: t, Valid: true} uuid, perr := util.ParseUUID(beforeIDStr) diff --git a/server/internal/handler/comment_list_test.go b/server/internal/handler/comment_list_test.go index 3d1bd7393..80fe3ee78 100644 --- a/server/internal/handler/comment_list_test.go +++ b/server/internal/handler/comment_list_test.go @@ -229,6 +229,37 @@ func TestListComments_RootsOnlyWithSinceReturnsNewTopLevelComments(t *testing.T) } } +// TestListComments_TimestampParamsAcceptSecondPrecision pins that RFC3339 +// timestamps without fractional seconds still parse after the single +// RFC3339Nano parse, and that malformed values keep their 400 responses. +func TestListComments_TimestampParamsAcceptSecondPrecision(t *testing.T) { + if testHandler == nil || testPool == nil { + t.Skip("database not available") + } + fx := newCommentListFixture(t) + + v := url.Values{} + v.Set("roots_only", "true") + v.Set("since", fx.Base.Add(5*time.Minute).UTC().Format(time.RFC3339)) + w, rows := listComments(t, fx.IssueID, v.Encode()) + if w.Code != http.StatusOK { + t.Fatalf("second-precision since: status %d body %s", w.Code, w.Body.String()) + } + eqIDs(t, ids(rows), []string{fx.Root2}, "second-precision since") + + for _, tc := range []struct { + name, query, wantMessage string + }{ + {"since", "since=2026-01-02+03:04:05", "invalid since parameter"}, + {"before", cursorQuery(1, "2026-01-02T03:04", fx.Root1), "invalid before parameter"}, + } { + w, _ := listComments(t, fx.IssueID, tc.query) + if w.Code != http.StatusBadRequest || !strings.Contains(w.Body.String(), tc.wantMessage) { + t.Fatalf("invalid %s: status %d body %s", tc.name, w.Code, w.Body.String()) + } + } +} + // TestListComments_RootsOnlyReturnsThreadStats pins the orientation metadata: // each root returned by roots_only carries reply_count (recursive descendant // count) and last_activity_at (MAX created_at over the subtree), so an agent diff --git a/server/internal/service/autopilot.go b/server/internal/service/autopilot.go index bb68b91dc..5b7de029d 100644 --- a/server/internal/service/autopilot.go +++ b/server/internal/service/autopilot.go @@ -1166,7 +1166,7 @@ func (s *AutopilotService) SyncRunFromIssue(ctx context.Context, issue db.Issue) // settles the run; do not treat "not yet dispatched" as success. return } - updatedRun, err := s.completeAutopilotRun(ctx, db.UpdateAutopilotRunCompletedParams{ + updatedRun, err := s.completeAutopilotRun(ctx, autopilotRunCompletion{ ID: run.ID, }) if err != nil { @@ -1177,7 +1177,7 @@ func (s *AutopilotService) SyncRunFromIssue(ctx context.Context, issue db.Issue) s.publishRunDone(wsID, updatedRun, "completed") case "cancelled": reason := "issue " + issue.Status - updatedRun, err := s.failAutopilotRun(ctx, db.UpdateAutopilotRunFailedParams{ + updatedRun, err := s.failAutopilotRun(ctx, autopilotRunFailure{ ID: run.ID, FailureReason: pgtype.Text{String: reason, Valid: true}, }) @@ -1209,7 +1209,7 @@ func (s *AutopilotService) SyncRunFromTask(ctx context.Context, task db.AgentTas switch task.Status { case "completed": - updatedRun, err := s.completeAutopilotRun(ctx, db.UpdateAutopilotRunCompletedParams{ + updatedRun, err := s.completeAutopilotRun(ctx, autopilotRunCompletion{ ID: run.ID, Result: task.Result, }) @@ -1221,7 +1221,7 @@ func (s *AutopilotService) SyncRunFromTask(ctx context.Context, task db.AgentTas s.publishRunDone(wsID, updatedRun, "completed") case "failed", "cancelled": reason := taskFailureReasonForAutopilotRun(task) - updatedRun, err := s.failAutopilotRun(ctx, db.UpdateAutopilotRunFailedParams{ + updatedRun, err := s.failAutopilotRun(ctx, autopilotRunFailure{ ID: run.ID, FailureReason: pgtype.Text{String: reason, Valid: reason != ""}, }) @@ -1311,7 +1311,7 @@ func (s *AutopilotService) settleRunFromDispatchTask(ctx context.Context, run db } reason := taskFailureReasonForAutopilotRun(task) - updatedRun, err := s.failAutopilotRun(ctx, db.UpdateAutopilotRunFailedParams{ + updatedRun, err := s.failAutopilotRun(ctx, autopilotRunFailure{ ID: run.ID, FailureReason: pgtype.Text{String: reason, Valid: reason != ""}, }) @@ -1336,7 +1336,7 @@ func (s *AutopilotService) completeRunFromLinkedIssueTask(ctx context.Context, r if err != nil { return } - updatedRun, err := s.completeAutopilotRun(ctx, db.UpdateAutopilotRunCompletedParams{ + updatedRun, err := s.completeAutopilotRun(ctx, autopilotRunCompletion{ ID: run.ID, }) if err != nil { @@ -1525,7 +1525,7 @@ func (s *AutopilotService) handleDispatchSkip(ctx context.Context, ap db.Autopil } func (s *AutopilotService) failRun(ctx context.Context, runID pgtype.UUID, reason string) { - if _, err := s.failAutopilotRun(ctx, db.UpdateAutopilotRunFailedParams{ + if _, err := s.failAutopilotRun(ctx, autopilotRunFailure{ ID: runID, FailureReason: pgtype.Text{String: reason, Valid: true}, ReasonCode: pgtype.Text{String: string(dispatch.ReasonInternalError), Valid: true}, diff --git a/server/internal/service/autopilot_quota.go b/server/internal/service/autopilot_quota.go index 8a547989e..e3310574e 100644 --- a/server/internal/service/autopilot_quota.go +++ b/server/internal/service/autopilot_quota.go @@ -245,7 +245,21 @@ func settleAutopilotQuota(ctx context.Context, q *db.Queries, reservationID pgty return err == nil, err } -func (s *AutopilotService) completeAutopilotRun(ctx context.Context, params db.UpdateAutopilotRunCompletedParams) (db.AutopilotRun, error) { +// autopilotRunCompletion and autopilotRunFailure carry the terminal inputs +// that UpdateAutopilotRunTerminalWithQuota settles atomically with the run's +// quota reservation. +type autopilotRunCompletion struct { + ID pgtype.UUID + Result []byte +} + +type autopilotRunFailure struct { + ID pgtype.UUID + FailureReason pgtype.Text + ReasonCode pgtype.Text +} + +func (s *AutopilotService) completeAutopilotRun(ctx context.Context, params autopilotRunCompletion) (db.AutopilotRun, error) { row, err := s.Queries.UpdateAutopilotRunTerminalWithQuota(ctx, db.UpdateAutopilotRunTerminalWithQuotaParams{ TerminalStatus: "completed", Result: params.Result, @@ -255,7 +269,7 @@ func (s *AutopilotService) completeAutopilotRun(ctx context.Context, params db.U return autopilotRunFromTerminalRow(row), err } -func (s *AutopilotService) failAutopilotRun(ctx context.Context, params db.UpdateAutopilotRunFailedParams) (db.AutopilotRun, error) { +func (s *AutopilotService) failAutopilotRun(ctx context.Context, params autopilotRunFailure) (db.AutopilotRun, error) { row, err := s.Queries.UpdateAutopilotRunTerminalWithQuota(ctx, db.UpdateAutopilotRunTerminalWithQuotaParams{ TerminalStatus: "failed", FailureReason: params.FailureReason, diff --git a/server/internal/service/autopilot_quota_test.go b/server/internal/service/autopilot_quota_test.go index a9803aad8..ed647da6c 100644 --- a/server/internal/service/autopilot_quota_test.go +++ b/server/internal/service/autopilot_quota_test.go @@ -707,7 +707,7 @@ func TestAutopilotQuotaObserveToEnforceAndOffFinalization(t *testing.T) { // under observe/enforce. Terminal finalization relies on the persisted slot. fixture.setPolicy(entitlement.ActionOff, fixture.periodStart, fixture.periodEnd, 1) for _, run := range runs { - if _, err := fixture.service.failAutopilotRun(ctx, db.UpdateAutopilotRunFailedParams{ + if _, err := fixture.service.failAutopilotRun(ctx, autopilotRunFailure{ ID: run.ID, FailureReason: pgtype.Text{String: "test cleanup", Valid: true}, }); err != nil { t.Fatalf("finalize while policy off: %v", err) @@ -738,7 +738,7 @@ func TestAutopilotQuotaConsumedCreateIssueCannotBeRefunded(t *testing.T) { if _, err := settleAutopilotQuota(ctx, fixture.queries, cancelled.QuotaReservationID, true); err != nil { t.Fatalf("consume cancelled run: %v", err) } - if _, err := fixture.service.failAutopilotRun(ctx, db.UpdateAutopilotRunFailedParams{ + if _, err := fixture.service.failAutopilotRun(ctx, autopilotRunFailure{ ID: cancelled.ID, FailureReason: pgtype.Text{String: "issue cancelled", Valid: true}, }); err != nil { t.Fatalf("fail consumed run: %v", err) @@ -777,7 +777,7 @@ func TestAutopilotQuotaTerminalUpdateNeedsNoTransactionStarter(t *testing.T) { t.Fatalf("create off-path run: %v", err) } serviceWithoutTx := &AutopilotService{Queries: fixture.queries} - updated, err := serviceWithoutTx.completeAutopilotRun(ctx, db.UpdateAutopilotRunCompletedParams{ID: run.ID}) + updated, err := serviceWithoutTx.completeAutopilotRun(ctx, autopilotRunCompletion{ID: run.ID}) if err != nil { t.Fatalf("complete off-path run: %v", err) } @@ -798,7 +798,7 @@ func TestAutopilotQuotaSettlementStaysInOriginalPeriod(t *testing.T) { secondStart := fixture.periodEnd secondEnd := secondStart.Add(19 * time.Hour) fixture.setPolicy(entitlement.ActionEnforce, secondStart, secondEnd, 1) - if _, err := fixture.service.completeAutopilotRun(ctx, db.UpdateAutopilotRunCompletedParams{ID: run.ID}); err != nil { + if _, err := fixture.service.completeAutopilotRun(ctx, autopilotRunCompletion{ID: run.ID}); err != nil { t.Fatalf("complete after period rollover: %v", err) } @@ -861,7 +861,9 @@ func TestAutopilotQuotaReconcilerSettlesTerminalOnlyOnce(t *testing.T) { } // Simulate a crash after the run reached terminal state but before the old // two-step finalizer settled its reservation. - if _, err := fixture.queries.UpdateAutopilotRunCompleted(ctx, db.UpdateAutopilotRunCompletedParams{ID: terminal.ID}); err != nil { + if _, err := fixture.pool.Exec(ctx, ` + UPDATE autopilot_run SET status = 'completed', completed_at = now() WHERE id = $1 + `, terminal.ID); err != nil { t.Fatalf("seed terminal crash window: %v", err) } diff --git a/server/pkg/db/generated/agent.sql.go b/server/pkg/db/generated/agent.sql.go index 140a2f1f2..72c5d4b56 100644 --- a/server/pkg/db/generated/agent.sql.go +++ b/server/pkg/db/generated/agent.sql.go @@ -6917,8 +6917,9 @@ type MarkAgentTaskWaitingLocalDirectoryParams struct { // alongside the status. // // The CHECK only allows the transition from 'dispatched' so a daemon can't -// mark an already-running or terminal task as waiting; the StartAgentTask -// mutation handles the reverse transition once the lock is acquired. +// mark an already-running or terminal task as waiting; the +// StartAgentTaskWithSupplement mutation handles the reverse transition once +// the lock is acquired. func (q *Queries) MarkAgentTaskWaitingLocalDirectory(ctx context.Context, arg MarkAgentTaskWaitingLocalDirectoryParams) (AgentTaskQueue, error) { row := q.db.QueryRow(ctx, markAgentTaskWaitingLocalDirectory, arg.ID, arg.WaitReason, arg.PrepareLeaseSecs) var i AgentTaskQueue @@ -8710,91 +8711,6 @@ func (q *Queries) SettleDelegatedFailureRecoveryComment(ctx context.Context, com return result.RowsAffected(), nil } -const startAgentTask = `-- name: StartAgentTask :one -UPDATE agent_task_queue -SET status = 'running', - started_at = now(), - wait_reason = NULL, - prepare_lease_expires_at = NULL -WHERE agent_task_queue.id = $1 AND agent_task_queue.status IN ('dispatched', 'waiting_local_directory') -RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step -` - -// Legacy single-winner transition; generation-aware callers lock the claim first. -// Transitions a task to running. Accepts either 'dispatched' (the normal -// claim → run flow) or 'waiting_local_directory' (the daemon held the row in -// a wait state while another task owned the local_directory path lock; once -// the lock was acquired the daemon flips here). wait_reason is cleared on -// the transition so a future read can't conflate "currently waiting" with -// "previously waited". -func (q *Queries) StartAgentTask(ctx context.Context, id pgtype.UUID) (AgentTaskQueue, error) { - row := q.db.QueryRow(ctx, startAgentTask, id) - var i AgentTaskQueue - err := row.Scan( - &i.ID, - &i.AgentID, - &i.IssueID, - &i.Status, - &i.Priority, - &i.DispatchedAt, - &i.StartedAt, - &i.CompletedAt, - &i.Result, - &i.Error, - &i.CreatedAt, - &i.Context, - &i.RuntimeID, - &i.SessionID, - &i.WorkDir, - &i.TriggerCommentID, - &i.ChatSessionID, - &i.AutopilotRunID, - &i.Attempt, - &i.MaxAttempts, - &i.ParentTaskID, - &i.FailureReason, - &i.TriggerSummary, - &i.ForceFreshSession, - &i.IsLeaderTask, - &i.WaitReason, - &i.InitiatorUserID, - &i.HandoffNote, - &i.PrepareLeaseExpiresAt, - &i.SquadID, - &i.RuntimeMcpOverlay, - &i.EscalationForTaskID, - &i.FireAt, - &i.OriginatorUserID, - &i.RuntimeConnectedApps, - &i.CoalescedCommentIds, - &i.DeliveredCommentIds, - &i.ChatInputTaskID, - &i.ChatFinalizeDeferredAt, - &i.OriginatorSource, - &i.DelegatedFromTaskID, - &i.RetryOfTaskID, - &i.RerunOfTaskID, - &i.RuleVersionID, - &i.TriggerEvidenceKind, - &i.TriggerEvidenceRefID, - &i.AccountableUserID, - &i.SessionRolloutMissing, - &i.RetiredSessionID, - &i.QuickActionsDisabled, - &i.RegenerateQuickActionsFor, - &i.BranchName, - &i.DurableWorkDir, - &i.ChannelContextRevision, - &i.CommentThreadID, - &i.CancelledByType, - &i.CancelledByID, - &i.CancelledByName, - &i.IssueSnapshot, - &i.WorkflowStep, - ) - return i, err -} - const updateAgent = `-- name: UpdateAgent :one UPDATE agent SET name = COALESCE($2, name), diff --git a/server/pkg/db/generated/autopilot.sql.go b/server/pkg/db/generated/autopilot.sql.go index 72c3e5f94..726943208 100644 --- a/server/pkg/db/generated/autopilot.sql.go +++ b/server/pkg/db/generated/autopilot.sql.go @@ -2267,88 +2267,6 @@ func (q *Queries) UpdateAutopilotLastRunAt(ctx context.Context, id pgtype.UUID) return err } -const updateAutopilotRunCompleted = `-- name: UpdateAutopilotRunCompleted :one -UPDATE autopilot_run -SET status = 'completed', completed_at = now(), result = $2 -WHERE id = $1 -RETURNING id, autopilot_id, trigger_id, source, status, issue_id, task_id, triggered_at, completed_at, failure_reason, trigger_payload, result, created_at, squad_id, planned_at, webhook_delivery_id, quota_reservation_id, reason_code -` - -type UpdateAutopilotRunCompletedParams struct { - ID pgtype.UUID `json:"id"` - Result []byte `json:"result"` -} - -// Quota safety: only use for a run known to have no reservation. Normal -// terminal paths must call UpdateAutopilotRunTerminalWithQuota instead. -func (q *Queries) UpdateAutopilotRunCompleted(ctx context.Context, arg UpdateAutopilotRunCompletedParams) (AutopilotRun, error) { - row := q.db.QueryRow(ctx, updateAutopilotRunCompleted, arg.ID, arg.Result) - var i AutopilotRun - err := row.Scan( - &i.ID, - &i.AutopilotID, - &i.TriggerID, - &i.Source, - &i.Status, - &i.IssueID, - &i.TaskID, - &i.TriggeredAt, - &i.CompletedAt, - &i.FailureReason, - &i.TriggerPayload, - &i.Result, - &i.CreatedAt, - &i.SquadID, - &i.PlannedAt, - &i.WebhookDeliveryID, - &i.QuotaReservationID, - &i.ReasonCode, - ) - return i, err -} - -const updateAutopilotRunFailed = `-- name: UpdateAutopilotRunFailed :one -UPDATE autopilot_run -SET status = 'failed', completed_at = now(), failure_reason = $2, - reason_code = $3 -WHERE id = $1 -RETURNING id, autopilot_id, trigger_id, source, status, issue_id, task_id, triggered_at, completed_at, failure_reason, trigger_payload, result, created_at, squad_id, planned_at, webhook_delivery_id, quota_reservation_id, reason_code -` - -type UpdateAutopilotRunFailedParams struct { - ID pgtype.UUID `json:"id"` - FailureReason pgtype.Text `json:"failure_reason"` - ReasonCode pgtype.Text `json:"reason_code"` -} - -// Quota safety: only use for a run known to have no reservation. Normal -// terminal paths must call UpdateAutopilotRunTerminalWithQuota instead. -func (q *Queries) UpdateAutopilotRunFailed(ctx context.Context, arg UpdateAutopilotRunFailedParams) (AutopilotRun, error) { - row := q.db.QueryRow(ctx, updateAutopilotRunFailed, arg.ID, arg.FailureReason, arg.ReasonCode) - var i AutopilotRun - err := row.Scan( - &i.ID, - &i.AutopilotID, - &i.TriggerID, - &i.Source, - &i.Status, - &i.IssueID, - &i.TaskID, - &i.TriggeredAt, - &i.CompletedAt, - &i.FailureReason, - &i.TriggerPayload, - &i.Result, - &i.CreatedAt, - &i.SquadID, - &i.PlannedAt, - &i.WebhookDeliveryID, - &i.QuotaReservationID, - &i.ReasonCode, - ) - return i, err -} - const updateAutopilotRunIssueCreated = `-- name: UpdateAutopilotRunIssueCreated :one UPDATE autopilot_run SET status = 'issue_created', issue_id = $2 @@ -2473,50 +2391,6 @@ func (q *Queries) UpdateAutopilotRunSkipped(ctx context.Context, arg UpdateAutop return i, err } -const updateAutopilotRunSkippedWithResult = `-- name: UpdateAutopilotRunSkippedWithResult :one -UPDATE autopilot_run -SET status = 'skipped', - completed_at = now(), - failure_reason = $2, - result = $3 -WHERE id = $1 -RETURNING id, autopilot_id, trigger_id, source, status, issue_id, task_id, triggered_at, completed_at, failure_reason, trigger_payload, result, created_at, squad_id, planned_at, webhook_delivery_id, quota_reservation_id, reason_code -` - -type UpdateAutopilotRunSkippedWithResultParams struct { - ID pgtype.UUID `json:"id"` - FailureReason pgtype.Text `json:"failure_reason"` - Result []byte `json:"result"` -} - -// Quota safety: legacy no-reservation helper. New terminal paths must call -// UpdateAutopilotRunTerminalWithQuota instead. -func (q *Queries) UpdateAutopilotRunSkippedWithResult(ctx context.Context, arg UpdateAutopilotRunSkippedWithResultParams) (AutopilotRun, error) { - row := q.db.QueryRow(ctx, updateAutopilotRunSkippedWithResult, arg.ID, arg.FailureReason, arg.Result) - var i AutopilotRun - err := row.Scan( - &i.ID, - &i.AutopilotID, - &i.TriggerID, - &i.Source, - &i.Status, - &i.IssueID, - &i.TaskID, - &i.TriggeredAt, - &i.CompletedAt, - &i.FailureReason, - &i.TriggerPayload, - &i.Result, - &i.CreatedAt, - &i.SquadID, - &i.PlannedAt, - &i.WebhookDeliveryID, - &i.QuotaReservationID, - &i.ReasonCode, - ) - return i, err -} - const updateAutopilotRunTerminalWithQuota = `-- name: UpdateAutopilotRunTerminalWithQuota :one WITH updated_run AS ( UPDATE autopilot_run AS ar diff --git a/server/pkg/db/generated/channel.sql.go b/server/pkg/db/generated/channel.sql.go index f33620b7d..b8f7dc1a6 100644 --- a/server/pkg/db/generated/channel.sql.go +++ b/server/pkg/db/generated/channel.sql.go @@ -420,17 +420,6 @@ func (q *Queries) ClearChannelChatContextPendingFresh(ctx context.Context, arg C return err } -const clearChannelChatSessionPendingFresh = `-- name: ClearChannelChatSessionPendingFresh :exec -UPDATE channel_chat_session_binding -SET pending_fresh = FALSE -WHERE chat_session_id = $1 -` - -func (q *Queries) ClearChannelChatSessionPendingFresh(ctx context.Context, chatSessionID pgtype.UUID) error { - _, err := q.db.Exec(ctx, clearChannelChatSessionPendingFresh, chatSessionID) - return err -} - const clearChannelChatSessionPendingFreshForRevision = `-- name: ClearChannelChatSessionPendingFreshForRevision :exec UPDATE channel_chat_session_binding SET pending_fresh = FALSE @@ -2476,22 +2465,6 @@ func (q *Queries) LockChannelChatSessionBindingForContext(ctx context.Context, c return i, err } -const lockChannelChatSessionPendingFresh = `-- name: LockChannelChatSessionPendingFresh :one -SELECT pending_fresh FROM channel_chat_session_binding -WHERE chat_session_id = $1 -FOR UPDATE -` - -// EnqueueChatTask reads this under the same row lock and transaction that -// creates the task. A concurrent `/clear` therefore lands either before this -// task and is consumed by it, or after this task and remains for the next one. -func (q *Queries) LockChannelChatSessionPendingFresh(ctx context.Context, chatSessionID pgtype.UUID) (bool, error) { - row := q.db.QueryRow(ctx, lockChannelChatSessionPendingFresh, chatSessionID) - var pending_fresh bool - err := row.Scan(&pending_fresh) - return pending_fresh, err -} - const lockChannelInstallationAppIDSlot = `-- name: LockChannelInstallationAppIDSlot :exec SELECT pg_advisory_xact_lock( hashtext($1::text), @@ -2592,23 +2565,6 @@ func (q *Queries) LockCurrentChannelChatSessionBindingBySession(ctx context.Cont return i, err } -const markChannelChatSessionPendingFresh = `-- name: MarkChannelChatSessionPendingFresh :one -UPDATE channel_chat_session_binding -SET pending_fresh = TRUE -WHERE chat_session_id = $1 AND retired_at IS NULL -RETURNING pending_fresh -` - -// Persists a channel `/clear` intent until the next chat task is successfully -// created. RETURNING makes a missing binding an error instead of silently -// acknowledging a fresh start that was never stored. -func (q *Queries) MarkChannelChatSessionPendingFresh(ctx context.Context, chatSessionID pgtype.UUID) (bool, error) { - row := q.db.QueryRow(ctx, markChannelChatSessionPendingFresh, chatSessionID) - var pending_fresh bool - err := row.Scan(&pending_fresh) - return pending_fresh, err -} - const markChannelInboundDedupProcessed = `-- name: MarkChannelInboundDedupProcessed :execrows UPDATE channel_inbound_message_dedup SET processed_at = now() diff --git a/server/pkg/db/queries/agent.sql b/server/pkg/db/queries/agent.sql index 4f3bcdc48..16b9d2080 100644 --- a/server/pkg/db/queries/agent.sql +++ b/server/pkg/db/queries/agent.sql @@ -963,22 +963,6 @@ WHERE id = $1 AND started_at IS NULL RETURNING *; --- name: StartAgentTask :one --- Legacy single-winner transition; generation-aware callers lock the claim first. --- Transitions a task to running. Accepts either 'dispatched' (the normal --- claim → run flow) or 'waiting_local_directory' (the daemon held the row in --- a wait state while another task owned the local_directory path lock; once --- the lock was acquired the daemon flips here). wait_reason is cleared on --- the transition so a future read can't conflate "currently waiting" with --- "previously waited". -UPDATE agent_task_queue -SET status = 'running', - started_at = now(), - wait_reason = NULL, - prepare_lease_expires_at = NULL -WHERE agent_task_queue.id = $1 AND agent_task_queue.status IN ('dispatched', 'waiting_local_directory') -RETURNING *; - -- name: LockAgentTaskStartClaim :one -- Serialize start/replay with reclaim and cancellation. A stale delivery must -- never start or acknowledge a newer claim, even on the same runtime. @@ -995,8 +979,9 @@ FOR UPDATE; -- alongside the status. -- -- The CHECK only allows the transition from 'dispatched' so a daemon can't --- mark an already-running or terminal task as waiting; the StartAgentTask --- mutation handles the reverse transition once the lock is acquired. +-- mark an already-running or terminal task as waiting; the +-- StartAgentTaskWithSupplement mutation handles the reverse transition once +-- the lock is acquired. UPDATE agent_task_queue SET status = 'waiting_local_directory', wait_reason = $2, diff --git a/server/pkg/db/queries/autopilot.sql b/server/pkg/db/queries/autopilot.sql index f1c0bc2b3..cb10c2ec5 100644 --- a/server/pkg/db/queries/autopilot.sql +++ b/server/pkg/db/queries/autopilot.sql @@ -441,23 +441,6 @@ SET status = 'running', task_id = $2 WHERE id = $1 RETURNING *; --- name: UpdateAutopilotRunCompleted :one --- Quota safety: only use for a run known to have no reservation. Normal --- terminal paths must call UpdateAutopilotRunTerminalWithQuota instead. -UPDATE autopilot_run -SET status = 'completed', completed_at = now(), result = sqlc.narg('result') -WHERE id = $1 -RETURNING *; - --- name: UpdateAutopilotRunFailed :one --- Quota safety: only use for a run known to have no reservation. Normal --- terminal paths must call UpdateAutopilotRunTerminalWithQuota instead. -UPDATE autopilot_run -SET status = 'failed', completed_at = now(), failure_reason = $2, - reason_code = sqlc.narg('reason_code') -WHERE id = $1 -RETURNING *; - -- name: UpdateAutopilotRunSkipped :one -- Quota safety: this is only for pre-admission skipped rows, which never have -- reservations. Post-admission skips use UpdateAutopilotRunTerminalWithQuota. @@ -532,17 +515,6 @@ WITH updated_run AS ( ) SELECT * FROM updated_run; --- name: UpdateAutopilotRunSkippedWithResult :one --- Quota safety: legacy no-reservation helper. New terminal paths must call --- UpdateAutopilotRunTerminalWithQuota instead. -UPDATE autopilot_run -SET status = 'skipped', - completed_at = now(), - failure_reason = $2, - result = sqlc.narg('result') -WHERE id = $1 -RETURNING *; - -- ===================== -- Scheduler Queries -- ===================== diff --git a/server/pkg/db/queries/channel.sql b/server/pkg/db/queries/channel.sql index e7a7f4659..83078a404 100644 --- a/server/pkg/db/queries/channel.sql +++ b/server/pkg/db/queries/channel.sql @@ -791,23 +791,6 @@ SET last_message_id = sqlc.narg('last_message_id'), FROM current_route WHERE binding.id = current_route.id; --- name: MarkChannelChatSessionPendingFresh :one --- Persists a channel `/clear` intent until the next chat task is successfully --- created. RETURNING makes a missing binding an error instead of silently --- acknowledging a fresh start that was never stored. -UPDATE channel_chat_session_binding -SET pending_fresh = TRUE -WHERE chat_session_id = $1 AND retired_at IS NULL -RETURNING pending_fresh; - --- name: LockChannelChatSessionPendingFresh :one --- EnqueueChatTask reads this under the same row lock and transaction that --- creates the task. A concurrent `/clear` therefore lands either before this --- task and is consumed by it, or after this task and remains for the next one. -SELECT pending_fresh FROM channel_chat_session_binding -WHERE chat_session_id = $1 -FOR UPDATE; - -- name: LockChannelChatSessionBindingForContext :one -- Context mutations acquire this row after chat_session and before any -- channel_chat_context_generation row. Keeping the statements separate makes @@ -816,11 +799,6 @@ SELECT * FROM channel_chat_session_binding WHERE chat_session_id = $1 FOR UPDATE; --- name: ClearChannelChatSessionPendingFresh :exec -UPDATE channel_chat_session_binding -SET pending_fresh = FALSE -WHERE chat_session_id = $1; - -- name: ClearChannelChatSessionPendingFreshForRevision :exec UPDATE channel_chat_session_binding SET pending_fresh = FALSE diff --git a/server/pkg/protocol/messages.go b/server/pkg/protocol/messages.go index 147c2898f..a98b435d7 100644 --- a/server/pkg/protocol/messages.go +++ b/server/pkg/protocol/messages.go @@ -5,9 +5,6 @@ import "encoding/json" const ( DaemonCapabilitySkillBundlesV1 = "skill-bundles-v1" DaemonCapabilityCoalescedCommentsV1 = "coalesced-comments-v1" - DaemonCapabilityExecutionManifestV1 = "execution-manifest-v1" - DaemonCapabilityAgentSkillV1 = "agent-skill-v1" - DaemonCapabilityRemoteMCPV1 = "remote-mcp-v1" // DaemonCapabilityLocalWorktreeV1 advertises that the daemon implements // worktree mode for local_directory resources (execution_mode=worktree). //