Compare commits

...

1 Commits

Author SHA1 Message Date
yushen
68c3d312bd feat(runtime): add owner tracking, filtering, and delete functionality
Add owner_id to agent_runtime table to track who registered each runtime.
Backend: new delete endpoint with role-based permissions (owner/admin can
delete any, members only their own), list filtering by owner (?owner=me),
and agent dependency check before deletion.
Frontend: Mine/All filter toggle in runtime list, owner display in list
items and detail view, delete button with AlertDialog confirmation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 13:24:11 +08:00
16 changed files with 361 additions and 23 deletions

View File

@@ -1 +1,2 @@
export { runtimeKeys, runtimeListOptions } from "./queries";
export { useDeleteRuntime } from "./mutations";

View File

@@ -0,0 +1,13 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { api } from "@/shared/api";
import { runtimeKeys } from "./queries";
export function useDeleteRuntime(wsId: string) {
const qc = useQueryClient();
return useMutation({
mutationFn: (runtimeId: string) => api.deleteRuntime(runtimeId),
onSettled: () => {
qc.invalidateQueries({ queryKey: runtimeKeys.all(wsId) });
},
});
}

View File

@@ -4,11 +4,12 @@ import { api } from "@/shared/api";
export const runtimeKeys = {
all: (wsId: string) => ["runtimes", wsId] as const,
list: (wsId: string) => [...runtimeKeys.all(wsId), "list"] as const,
listMine: (wsId: string) => [...runtimeKeys.all(wsId), "list", "mine"] as const,
};
export function runtimeListOptions(wsId: string) {
export function runtimeListOptions(wsId: string, owner?: "me") {
return queryOptions({
queryKey: runtimeKeys.list(wsId),
queryFn: () => api.listRuntimes({ workspace_id: wsId }),
queryKey: owner === "me" ? runtimeKeys.listMine(wsId) : runtimeKeys.list(wsId),
queryFn: () => api.listRuntimes({ workspace_id: wsId, owner }),
});
}

View File

@@ -1,4 +1,25 @@
"use client";
import { useState } from "react";
import { Trash2 } from "lucide-react";
import { toast } from "sonner";
import { useQuery } from "@tanstack/react-query";
import type { AgentRuntime } from "@/shared/types";
import { useAuthStore } from "@/features/auth";
import { useWorkspaceId } from "@core/hooks";
import { memberListOptions } from "@core/workspace/queries";
import { useDeleteRuntime } from "@core/runtimes/mutations";
import { Button } from "@/components/ui/button";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { formatLastSeen } from "../utils";
import { RuntimeModeIcon, StatusBadge, InfoField } from "./shared";
import { PingSection } from "./ping-section";
@@ -20,6 +41,41 @@ export function RuntimeDetail({ runtime }: { runtime: AgentRuntime }) {
const cliVersion =
runtime.runtime_mode === "local" ? getCliVersion(runtime.metadata) : null;
const user = useAuthStore((s) => s.user);
const wsId = useWorkspaceId();
const { data: members = [] } = useQuery(memberListOptions(wsId));
const deleteMutation = useDeleteRuntime(wsId);
const [deleteOpen, setDeleteOpen] = useState(false);
// Resolve owner info
const ownerMember = runtime.owner_id
? members.find((m) => m.user_id === runtime.owner_id)
: null;
const ownerName = ownerMember?.name ?? null;
// Permission check for delete
const currentMember = user
? members.find((m) => m.user_id === user.id)
: null;
const isAdmin = currentMember
? currentMember.role === "owner" || currentMember.role === "admin"
: false;
const isRuntimeOwner = user && runtime.owner_id === user.id;
const canDelete = isAdmin || isRuntimeOwner;
const handleDelete = () => {
deleteMutation.mutate(runtime.id, {
onSuccess: () => {
toast.success("Runtime deleted");
setDeleteOpen(false);
},
onError: (e) => {
toast.error(e instanceof Error ? e.message : "Failed to delete runtime");
},
});
};
return (
<div className="flex h-full flex-col">
{/* Header */}
@@ -36,7 +92,19 @@ export function RuntimeDetail({ runtime }: { runtime: AgentRuntime }) {
<h2 className="text-sm font-semibold truncate">{runtime.name}</h2>
</div>
</div>
<StatusBadge status={runtime.status} />
<div className="flex items-center gap-2">
<StatusBadge status={runtime.status} />
{canDelete && (
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-muted-foreground hover:text-destructive"
onClick={() => setDeleteOpen(true)}
>
<Trash2 className="h-4 w-4" />
</Button>
)}
</div>
</div>
{/* Content */}
@@ -50,6 +118,7 @@ export function RuntimeDetail({ runtime }: { runtime: AgentRuntime }) {
label="Last Seen"
value={formatLastSeen(runtime.last_seen_at)}
/>
{ownerName && <InfoField label="Owner" value={ownerName} />}
{runtime.device_info && (
<InfoField label="Device" value={runtime.device_info} />
)}
@@ -114,6 +183,28 @@ export function RuntimeDetail({ runtime }: { runtime: AgentRuntime }) {
/>
</div>
</div>
{/* Delete confirmation */}
<AlertDialog open={deleteOpen} onOpenChange={(v) => { if (!v) setDeleteOpen(false); }}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Runtime</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete &ldquo;{runtime.name}&rdquo;? This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
variant="destructive"
onClick={handleDelete}
disabled={deleteMutation.isPending}
>
{deleteMutation.isPending ? "Deleting..." : "Delete"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}

View File

@@ -1,14 +1,21 @@
import { Server } from "lucide-react";
import { useQuery } from "@tanstack/react-query";
import type { AgentRuntime } from "@/shared/types";
import { useWorkspaceId } from "@core/hooks";
import { memberListOptions } from "@core/workspace/queries";
import { RuntimeModeIcon } from "./shared";
type RuntimeFilter = "mine" | "all";
function RuntimeListItem({
runtime,
isSelected,
ownerName,
onClick,
}: {
runtime: AgentRuntime;
isSelected: boolean;
ownerName: string | null;
onClick: () => void;
}) {
return (
@@ -29,6 +36,7 @@ function RuntimeListItem({
<div className="truncate text-sm font-medium">{runtime.name}</div>
<div className="mt-0.5 truncate text-xs text-muted-foreground">
{runtime.provider} &middot; {runtime.runtime_mode}
{ownerName && <> &middot; {ownerName}</>}
</div>
</div>
<div
@@ -44,11 +52,23 @@ export function RuntimeList({
runtimes,
selectedId,
onSelect,
filter,
onFilterChange,
}: {
runtimes: AgentRuntime[];
selectedId: string;
onSelect: (id: string) => void;
filter: RuntimeFilter;
onFilterChange: (filter: RuntimeFilter) => void;
}) {
const wsId = useWorkspaceId();
const { data: members = [] } = useQuery(memberListOptions(wsId));
const getOwnerName = (ownerId: string | null) => {
if (!ownerId) return null;
return members.find((m) => m.user_id === ownerId)?.name ?? null;
};
return (
<div className="overflow-y-auto h-full border-r">
<div className="flex h-12 items-center justify-between border-b px-4">
@@ -58,11 +78,36 @@ export function RuntimeList({
{runtimes.length} online
</span>
</div>
{/* Filter toggle */}
<div className="flex border-b px-4 py-2 gap-1">
<button
onClick={() => onFilterChange("mine")}
className={`rounded-md px-2.5 py-1 text-xs font-medium transition-colors ${
filter === "mine"
? "bg-accent text-accent-foreground"
: "text-muted-foreground hover:text-foreground"
}`}
>
Mine
</button>
<button
onClick={() => onFilterChange("all")}
className={`rounded-md px-2.5 py-1 text-xs font-medium transition-colors ${
filter === "all"
? "bg-accent text-accent-foreground"
: "text-muted-foreground hover:text-foreground"
}`}
>
All
</button>
</div>
{runtimes.length === 0 ? (
<div className="flex flex-col items-center justify-center px-4 py-12">
<Server className="h-8 w-8 text-muted-foreground/40" />
<p className="mt-3 text-sm text-muted-foreground">
No runtimes registered
{filter === "mine" ? "No runtimes owned by you" : "No runtimes registered"}
</p>
<p className="mt-1 text-xs text-muted-foreground text-center">
Run{" "}
@@ -79,6 +124,7 @@ export function RuntimeList({
key={runtime.id}
runtime={runtime}
isSelected={runtime.id === selectedId}
ownerName={filter === "all" ? getOwnerName(runtime.owner_id) : null}
onClick={() => onSelect(runtime.id)}
/>
))}

View File

@@ -17,20 +17,25 @@ import { useWSEvent } from "@/features/realtime";
import { RuntimeList } from "./runtime-list";
import { RuntimeDetail } from "./runtime-detail";
type RuntimeFilter = "mine" | "all";
export default function RuntimesPage() {
const isLoading = useAuthStore((s) => s.isLoading);
const wsId = useWorkspaceId();
const qc = useQueryClient();
const { data: runtimes = [], isLoading: fetching } = useQuery(runtimeListOptions(wsId));
const [filter, setFilter] = useState<RuntimeFilter>("mine");
const [selectedId, setSelectedId] = useState("");
const ownerParam = filter === "mine" ? "me" as const : undefined;
const { data: runtimes = [], isLoading: fetching } = useQuery(runtimeListOptions(wsId, ownerParam));
const { defaultLayout, onLayoutChanged } = useDefaultLayout({
id: "multica_runtimes_layout",
});
// Re-fetch on daemon register/deregister events.
const handleDaemonEvent = useCallback(() => {
qc.invalidateQueries({ queryKey: runtimeKeys.list(wsId) });
qc.invalidateQueries({ queryKey: runtimeKeys.all(wsId) });
}, [qc, wsId]);
useWSEvent("daemon:register", handleDaemonEvent);
@@ -95,6 +100,8 @@ export default function RuntimesPage() {
runtimes={runtimes}
selectedId={effectiveSelectedId}
onSelect={setSelectedId}
filter={filter}
onFilterChange={setFilter}
/>
</ResizablePanel>

View File

@@ -338,13 +338,18 @@ export class ApiClient {
return this.fetch(`/api/agents/${id}/restore`, { method: "POST" });
}
async listRuntimes(params?: { workspace_id?: string }): Promise<AgentRuntime[]> {
async listRuntimes(params?: { workspace_id?: string; owner?: "me" }): Promise<AgentRuntime[]> {
const search = new URLSearchParams();
const wsId = params?.workspace_id ?? this.workspaceId;
if (wsId) search.set("workspace_id", wsId);
if (params?.owner) search.set("owner", params.owner);
return this.fetch(`/api/runtimes?${search}`);
}
async deleteRuntime(runtimeId: string): Promise<void> {
await this.fetch(`/api/runtimes/${runtimeId}`, { method: "DELETE" });
}
async getRuntimeUsage(runtimeId: string, params?: { days?: number }): Promise<RuntimeUsage[]> {
const search = new URLSearchParams();
if (params?.days) search.set("days", String(params.days));

View File

@@ -14,6 +14,7 @@ export interface RuntimeDevice {
status: "online" | "offline";
device_info: string;
metadata: Record<string, unknown>;
owner_id: string | null;
last_seen_at: string | null;
created_at: string;
updated_at: string;

View File

@@ -232,12 +232,15 @@ func NewRouter(pool *pgxpool.Pool, hub *realtime.Hub, bus *events.Bus) chi.Route
// Runtimes
r.Route("/api/runtimes", func(r chi.Router) {
r.Get("/", h.ListAgentRuntimes)
r.Get("/{runtimeId}/usage", h.GetRuntimeUsage)
r.Get("/{runtimeId}/activity", h.GetRuntimeTaskActivity)
r.Post("/{runtimeId}/ping", h.InitiatePing)
r.Get("/{runtimeId}/ping/{pingId}", h.GetPing)
r.Post("/{runtimeId}/update", h.InitiateUpdate)
r.Get("/{runtimeId}/update/{updateId}", h.GetUpdate)
r.Route("/{runtimeId}", func(r chi.Router) {
r.Get("/usage", h.GetRuntimeUsage)
r.Get("/activity", h.GetRuntimeTaskActivity)
r.Post("/ping", h.InitiatePing)
r.Get("/ping/{pingId}", h.GetPing)
r.Post("/update", h.InitiateUpdate)
r.Get("/update/{updateId}", h.GetUpdate)
r.Delete("/", h.DeleteAgentRuntime)
})
})
// Inbox

View File

@@ -57,7 +57,8 @@ func (h *Handler) DaemonRegister(w http.ResponseWriter, r *http.Request) {
}
// Verify the caller is a member of the target workspace.
if _, ok := h.requireWorkspaceMember(w, r, req.WorkspaceID, "workspace not found"); !ok {
member, ok := h.requireWorkspaceMember(w, r, req.WorkspaceID, "workspace not found")
if !ok {
return
}
@@ -104,6 +105,7 @@ func (h *Handler) DaemonRegister(w http.ResponseWriter, r *http.Request) {
Status: status,
DeviceInfo: deviceInfo,
Metadata: metadata,
OwnerID: member.UserID,
})
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to register runtime: "+err.Error())

View File

@@ -2,6 +2,7 @@ package handler
import (
"encoding/json"
"log/slog"
"net/http"
"strconv"
"time"
@@ -9,6 +10,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/jackc/pgx/v5/pgtype"
db "github.com/multica-ai/multica/server/pkg/db/generated"
"github.com/multica-ai/multica/server/pkg/protocol"
)
type AgentRuntimeResponse struct {
@@ -21,6 +23,7 @@ type AgentRuntimeResponse struct {
Status string `json:"status"`
DeviceInfo string `json:"device_info"`
Metadata any `json:"metadata"`
OwnerID *string `json:"owner_id"`
LastSeenAt *string `json:"last_seen_at"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
@@ -45,6 +48,7 @@ func runtimeToResponse(rt db.AgentRuntime) AgentRuntimeResponse {
Status: rt.Status,
DeviceInfo: rt.DeviceInfo,
Metadata: metadata,
OwnerID: uuidToPtr(rt.OwnerID),
LastSeenAt: timestampToPtr(rt.LastSeenAt),
CreatedAt: timestampToString(rt.CreatedAt),
UpdatedAt: timestampToString(rt.UpdatedAt),
@@ -285,7 +289,22 @@ func parseSinceParam(r *http.Request, defaultDays int) pgtype.Timestamptz {
func (h *Handler) ListAgentRuntimes(w http.ResponseWriter, r *http.Request) {
workspaceID := resolveWorkspaceID(r)
runtimes, err := h.Queries.ListAgentRuntimes(r.Context(), parseUUID(workspaceID))
var runtimes []db.AgentRuntime
var err error
if ownerFilter := r.URL.Query().Get("owner"); ownerFilter == "me" {
userID, ok := requireUserID(w, r)
if !ok {
return
}
runtimes, err = h.Queries.ListAgentRuntimesByOwner(r.Context(), db.ListAgentRuntimesByOwnerParams{
WorkspaceID: parseUUID(workspaceID),
OwnerID: parseUUID(userID),
})
} else {
runtimes, err = h.Queries.ListAgentRuntimes(r.Context(), parseUUID(workspaceID))
}
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to list runtimes")
return
@@ -298,3 +317,54 @@ func (h *Handler) ListAgentRuntimes(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, resp)
}
// DeleteAgentRuntime deletes a runtime after permission and dependency checks.
func (h *Handler) DeleteAgentRuntime(w http.ResponseWriter, r *http.Request) {
runtimeID := chi.URLParam(r, "runtimeId")
rt, err := h.Queries.GetAgentRuntime(r.Context(), parseUUID(runtimeID))
if err != nil {
writeError(w, http.StatusNotFound, "runtime not found")
return
}
wsID := uuidToString(rt.WorkspaceID)
member, ok := h.requireWorkspaceMember(w, r, wsID, "runtime not found")
if !ok {
return
}
// Permission: owner/admin can delete any runtime; members can only delete their own.
userID := uuidToString(member.UserID)
isAdmin := roleAllowed(member.Role, "owner", "admin")
isOwner := rt.OwnerID.Valid && uuidToString(rt.OwnerID) == userID
if !isAdmin && !isOwner {
writeError(w, http.StatusForbidden, "you can only delete your own runtimes")
return
}
// Check if any agents are bound to this runtime (ON DELETE RESTRICT).
agentCount, err := h.Queries.CountAgentsByRuntime(r.Context(), rt.ID)
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to check runtime dependencies")
return
}
if agentCount > 0 {
writeError(w, http.StatusConflict, "cannot delete runtime: it has agents bound to it. Reassign or remove the agents first.")
return
}
if err := h.Queries.DeleteAgentRuntime(r.Context(), rt.ID); err != nil {
writeError(w, http.StatusInternalServerError, "failed to delete runtime")
return
}
slog.Info("runtime deleted", "runtime_id", runtimeID, "deleted_by", userID)
// Notify frontend to refresh runtime list.
h.publish(protocol.EventDaemonRegister, wsID, "member", userID, map[string]any{
"action": "delete",
})
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}

View File

@@ -0,0 +1 @@
ALTER TABLE agent_runtime DROP COLUMN IF EXISTS owner_id;

View File

@@ -0,0 +1,9 @@
ALTER TABLE agent_runtime ADD COLUMN owner_id UUID REFERENCES "user"(id);
-- Backfill: set existing runtimes' owner to the workspace owner
UPDATE agent_runtime ar
SET owner_id = (
SELECT m.user_id FROM member m
WHERE m.workspace_id = ar.workspace_id AND m.role = 'owner'
LIMIT 1
);

View File

@@ -52,6 +52,7 @@ type AgentRuntime struct {
LastSeenAt pgtype.Timestamptz `json:"last_seen_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
OwnerID pgtype.UUID `json:"owner_id"`
}
type AgentSkill struct {

View File

@@ -11,6 +11,26 @@ import (
"github.com/jackc/pgx/v5/pgtype"
)
const countAgentsByRuntime = `-- name: CountAgentsByRuntime :one
SELECT count(*) FROM agent WHERE runtime_id = $1
`
func (q *Queries) CountAgentsByRuntime(ctx context.Context, runtimeID pgtype.UUID) (int64, error) {
row := q.db.QueryRow(ctx, countAgentsByRuntime, runtimeID)
var count int64
err := row.Scan(&count)
return count, err
}
const deleteAgentRuntime = `-- name: DeleteAgentRuntime :exec
DELETE FROM agent_runtime WHERE id = $1
`
func (q *Queries) DeleteAgentRuntime(ctx context.Context, id pgtype.UUID) error {
_, err := q.db.Exec(ctx, deleteAgentRuntime, id)
return err
}
const failTasksForOfflineRuntimes = `-- name: FailTasksForOfflineRuntimes :many
UPDATE agent_task_queue
SET status = 'failed', completed_at = now(), error = 'runtime went offline'
@@ -50,7 +70,7 @@ func (q *Queries) FailTasksForOfflineRuntimes(ctx context.Context) ([]FailTasksF
}
const getAgentRuntime = `-- name: GetAgentRuntime :one
SELECT id, workspace_id, daemon_id, name, runtime_mode, provider, status, device_info, metadata, last_seen_at, created_at, updated_at FROM agent_runtime
SELECT id, workspace_id, daemon_id, name, runtime_mode, provider, status, device_info, metadata, last_seen_at, created_at, updated_at, owner_id FROM agent_runtime
WHERE id = $1
`
@@ -70,12 +90,13 @@ func (q *Queries) GetAgentRuntime(ctx context.Context, id pgtype.UUID) (AgentRun
&i.LastSeenAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.OwnerID,
)
return i, err
}
const getAgentRuntimeForWorkspace = `-- name: GetAgentRuntimeForWorkspace :one
SELECT id, workspace_id, daemon_id, name, runtime_mode, provider, status, device_info, metadata, last_seen_at, created_at, updated_at FROM agent_runtime
SELECT id, workspace_id, daemon_id, name, runtime_mode, provider, status, device_info, metadata, last_seen_at, created_at, updated_at, owner_id FROM agent_runtime
WHERE id = $1 AND workspace_id = $2
`
@@ -100,12 +121,13 @@ func (q *Queries) GetAgentRuntimeForWorkspace(ctx context.Context, arg GetAgentR
&i.LastSeenAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.OwnerID,
)
return i, err
}
const listAgentRuntimes = `-- name: ListAgentRuntimes :many
SELECT id, workspace_id, daemon_id, name, runtime_mode, provider, status, device_info, metadata, last_seen_at, created_at, updated_at FROM agent_runtime
SELECT id, workspace_id, daemon_id, name, runtime_mode, provider, status, device_info, metadata, last_seen_at, created_at, updated_at, owner_id FROM agent_runtime
WHERE workspace_id = $1
ORDER BY created_at ASC
`
@@ -132,6 +154,52 @@ func (q *Queries) ListAgentRuntimes(ctx context.Context, workspaceID pgtype.UUID
&i.LastSeenAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.OwnerID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listAgentRuntimesByOwner = `-- name: ListAgentRuntimesByOwner :many
SELECT id, workspace_id, daemon_id, name, runtime_mode, provider, status, device_info, metadata, last_seen_at, created_at, updated_at, owner_id FROM agent_runtime
WHERE workspace_id = $1 AND owner_id = $2
ORDER BY created_at ASC
`
type ListAgentRuntimesByOwnerParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
OwnerID pgtype.UUID `json:"owner_id"`
}
func (q *Queries) ListAgentRuntimesByOwner(ctx context.Context, arg ListAgentRuntimesByOwnerParams) ([]AgentRuntime, error) {
rows, err := q.db.Query(ctx, listAgentRuntimesByOwner, arg.WorkspaceID, arg.OwnerID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []AgentRuntime{}
for rows.Next() {
var i AgentRuntime
if err := rows.Scan(
&i.ID,
&i.WorkspaceID,
&i.DaemonID,
&i.Name,
&i.RuntimeMode,
&i.Provider,
&i.Status,
&i.DeviceInfo,
&i.Metadata,
&i.LastSeenAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.OwnerID,
); err != nil {
return nil, err
}
@@ -191,7 +259,7 @@ const updateAgentRuntimeHeartbeat = `-- name: UpdateAgentRuntimeHeartbeat :one
UPDATE agent_runtime
SET status = 'online', last_seen_at = now(), updated_at = now()
WHERE id = $1
RETURNING id, workspace_id, daemon_id, name, runtime_mode, provider, status, device_info, metadata, last_seen_at, created_at, updated_at
RETURNING id, workspace_id, daemon_id, name, runtime_mode, provider, status, device_info, metadata, last_seen_at, created_at, updated_at, owner_id
`
func (q *Queries) UpdateAgentRuntimeHeartbeat(ctx context.Context, id pgtype.UUID) (AgentRuntime, error) {
@@ -210,6 +278,7 @@ func (q *Queries) UpdateAgentRuntimeHeartbeat(ctx context.Context, id pgtype.UUI
&i.LastSeenAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.OwnerID,
)
return i, err
}
@@ -224,8 +293,9 @@ INSERT INTO agent_runtime (
status,
device_info,
metadata,
owner_id,
last_seen_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, now())
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, now())
ON CONFLICT (workspace_id, daemon_id, provider)
DO UPDATE SET
name = EXCLUDED.name,
@@ -233,9 +303,10 @@ DO UPDATE SET
status = EXCLUDED.status,
device_info = EXCLUDED.device_info,
metadata = EXCLUDED.metadata,
owner_id = COALESCE(EXCLUDED.owner_id, agent_runtime.owner_id),
last_seen_at = now(),
updated_at = now()
RETURNING id, workspace_id, daemon_id, name, runtime_mode, provider, status, device_info, metadata, last_seen_at, created_at, updated_at
RETURNING id, workspace_id, daemon_id, name, runtime_mode, provider, status, device_info, metadata, last_seen_at, created_at, updated_at, owner_id
`
type UpsertAgentRuntimeParams struct {
@@ -247,6 +318,7 @@ type UpsertAgentRuntimeParams struct {
Status string `json:"status"`
DeviceInfo string `json:"device_info"`
Metadata []byte `json:"metadata"`
OwnerID pgtype.UUID `json:"owner_id"`
}
func (q *Queries) UpsertAgentRuntime(ctx context.Context, arg UpsertAgentRuntimeParams) (AgentRuntime, error) {
@@ -259,6 +331,7 @@ func (q *Queries) UpsertAgentRuntime(ctx context.Context, arg UpsertAgentRuntime
arg.Status,
arg.DeviceInfo,
arg.Metadata,
arg.OwnerID,
)
var i AgentRuntime
err := row.Scan(
@@ -274,6 +347,7 @@ func (q *Queries) UpsertAgentRuntime(ctx context.Context, arg UpsertAgentRuntime
&i.LastSeenAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.OwnerID,
)
return i, err
}

View File

@@ -21,8 +21,9 @@ INSERT INTO agent_runtime (
status,
device_info,
metadata,
owner_id,
last_seen_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, now())
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, now())
ON CONFLICT (workspace_id, daemon_id, provider)
DO UPDATE SET
name = EXCLUDED.name,
@@ -30,6 +31,7 @@ DO UPDATE SET
status = EXCLUDED.status,
device_info = EXCLUDED.device_info,
metadata = EXCLUDED.metadata,
owner_id = COALESCE(EXCLUDED.owner_id, agent_runtime.owner_id),
last_seen_at = now(),
updated_at = now()
RETURNING *;
@@ -62,3 +64,14 @@ WHERE status IN ('dispatched', 'running')
SELECT id FROM agent_runtime WHERE status = 'offline'
)
RETURNING id, agent_id, issue_id;
-- name: ListAgentRuntimesByOwner :many
SELECT * FROM agent_runtime
WHERE workspace_id = $1 AND owner_id = $2
ORDER BY created_at ASC;
-- name: DeleteAgentRuntime :exec
DELETE FROM agent_runtime WHERE id = $1;
-- name: CountAgentsByRuntime :one
SELECT count(*) FROM agent WHERE runtime_id = $1;