From a56793046667ecbfb1eb1dc3b7b1125d83046adf Mon Sep 17 00:00:00 2001 From: dtoro Date: Thu, 9 Jul 2026 23:42:49 +0200 Subject: [PATCH] fix: make execution names unique, move approval bar above input - Execution entity name now includes UUID suffix: 'pct_create on host:strong (abc12345)' so the (type,name) UNIQUE constraint doesn't block subsequent executions for the same target+action. Dedup now uses JOIN + LIKE prefix match to find only pending_approval executions. - Move persistent approval bar from top of messages area to just above the chat input box (bottom-fixed position, above the textarea form). --- internal/mcp/server.go | 27 ++++++++--------- web/src/pages/Chat.svelte | 63 +++++++++++++++++++-------------------- 2 files changed, 43 insertions(+), 47 deletions(-) diff --git a/internal/mcp/server.go b/internal/mcp/server.go index b3cbade..bc78c19 100644 --- a/internal/mcp/server.go +++ b/internal/mcp/server.go @@ -284,18 +284,17 @@ func newServer(pool *db.Pool, agentID uuid.UUID) *mcp.Server { // Deduplicate: if a pending execution already exists for the same // target+action, return the existing one instead of creating a // duplicate. Prevents the LLM from re-requesting the same gated - // action in a tool-calling loop. Uses the entities type+name - // UNIQUE constraint as the dedup key (one execution per - // action:target pair). + // action in a tool-calling loop. Only blocks when a pending + // execution exists; completed/failed ones don't block. if action == "systemctl" || action == "apt_upgrade" || action == "pct_create" { - execName := action + " on " + targetSlug - var existingID, existingStatus string + execNamePrefix := action + " on " + targetSlug + var existingID string err := pool.QueryRow(ctx, ` - SELECT e.id::text, COALESCE(ex.status,'') FROM entities e - LEFT JOIN executions ex ON ex.entity_id = e.id - WHERE e.type = 'execution' AND e.name = $1 - ORDER BY e.created_at DESC LIMIT 1`, execName).Scan(&existingID, &existingStatus) - if err == nil && existingID != "" && existingStatus != "completed" && existingStatus != "failed" { + SELECT e.id::text FROM entities e + JOIN executions ex ON ex.entity_id = e.id + WHERE e.type = 'execution' AND e.name LIKE $1 AND ex.status = 'pending_approval' + ORDER BY e.created_at DESC LIMIT 1`, execNamePrefix+"%").Scan(&existingID) + if err == nil && existingID != "" { return textResult(fmt.Sprintf("%s on %s is already queued for approval — execution %s. Wait for operator approval. Do not re-request.", action, targetSlug, existingID)), nil } @@ -304,12 +303,10 @@ func newServer(pool *db.Pool, agentID uuid.UUID) *mcp.Server { id, _ := uuid.NewV7() correlationID := uuid.New().String() - // Write execution record. If the (type,name) UNIQUE constraint - // fires (dedup race), the INSERT silently does nothing and the - // existing record wins. + execName := action + " on " + targetSlug + " (" + id.String()[:8] + ")" execSlug := "exec:" + targetSlug + ":" + id.String()[:8] - _, err := pool.Exec(ctx, `INSERT INTO entities (id, slug, type, name, attributes) VALUES ($1, $2, 'execution', $3, '{}') ON CONFLICT (type, name) DO NOTHING`, - id, execSlug, action+" on "+targetSlug) + _, err := pool.Exec(ctx, `INSERT INTO entities (id, slug, type, name, attributes) VALUES ($1, $2, 'execution', $3, '{}')`, + id, execSlug, execName) if err != nil { return textResult(fmt.Sprintf("error: failed to create execution: %v", err)), nil } diff --git a/web/src/pages/Chat.svelte b/web/src/pages/Chat.svelte index 3692efb..16d68d8 100644 --- a/web/src/pages/Chat.svelte +++ b/web/src/pages/Chat.svelte @@ -130,38 +130,6 @@ {/if}
- {#if pendingApprovals.length > 0} -
- {#each pendingApprovals as a (a.executionId)} -
- - - {a.action} on {a.target} - - {#if approving === a.executionId} - - {:else} - - - {/if} -
- {/each} - {#if pendingApprovals.length > 1} -
- -
- {/if} -
- {/if}
{#if $messages.length === 0} @@ -215,6 +183,37 @@
{/if} + {#if pendingApprovals.length > 0} +
+ {#each pendingApprovals as a (a.executionId)} +
+ + + {a.action} on {a.target} + + {#if approving === a.executionId} + + {:else} + + + {/if} +
+ {/each} + {#if pendingApprovals.length > 1} + + {/if} +
+ {/if} +