Persist execution audit fields and enforce the in-flight guard in SQL
Findings 4 and 7 of REVIEW-2026-07-30.md, as a single user_version step (11 -> 12) because they alter the same table. confirmation_id was written by the INSERT and absent from all three executions SELECTs, so it always read back empty: "which confirmation authorized this destructive act" was unanswerable from the API. causation_id was set on the struct by the engine with no column to land in, and capability_version was missing entirely despite spec §4.1 and the precedent on confirmations. All three are now persisted and selected back. The one-in-flight-per-(capability, target) rule was check-then-insert with no constraint between, so two concurrent requests could both proceed. It is now a partial UNIQUE index; the engine's pre-insert query is demoted to an advisory fast path and a constraint violation maps to ErrExecutionInFlight (409), kept distinct from the idempotency-key replay case. Note the migration also rewrites pre-existing duplicate in-flight rows to status='unknown', keeping the lowest id per pair — creating the index would otherwise fail outright on any database holding stale started rows, which the old non-unique index permitted indefinitely. That is a write to existing audit rows, not just DDL. Rows predating this migration get capability_version=0, which is indistinguishable from a genuine 0; backfill is not possible. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uea55zaiWuEByEDC4UBSdd
This commit is contained in:
@@ -53,7 +53,10 @@ func (e *Engine) Execute(req *domain.ExecuteRequest) (*ExecuteResult, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// One in-flight execution per (capability_id, target_entity_id).
|
||||
// One in-flight execution per (capability_id, target_entity_id). This check
|
||||
// is advisory — it gives a clean error before doing any work — but the
|
||||
// authoritative guard is the partial unique index enforced at INSERT below,
|
||||
// which closes the check-then-insert window between the two.
|
||||
if _, err := e.store.GetInFlightExecution(req.CapabilityID, req.TargetEntityID); err == nil {
|
||||
return nil, domain.ErrExecutionInFlight
|
||||
}
|
||||
@@ -67,6 +70,7 @@ func (e *Engine) Execute(req *domain.ExecuteRequest) (*ExecuteResult, error) {
|
||||
exec := &domain.Execution{
|
||||
ID: domain.NewExecutionID(),
|
||||
CapabilityID: req.CapabilityID,
|
||||
CapabilityVersion: capability.Version,
|
||||
TargetEntityID: req.TargetEntityID,
|
||||
EntityVersion: req.EntityVersion,
|
||||
Arguments: req.Arguments,
|
||||
|
||||
Reference in New Issue
Block a user