From 554ecc4c851fe48171cf6b88af729d74a7ba4a0d Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Wed, 4 Feb 2026 18:25:13 +0530 Subject: [PATCH] feat(agents): Enhance agent update confirmation and status handling --- backend/app/api/agents.py | 5 ++++- backend/app/services/agent_provisioning.py | 5 +++-- frontend/src/app/agents/[agentId]/page.tsx | 21 ++++++++++++++------ frontend/src/app/agents/page.tsx | 21 ++++++++++++++------ frontend/src/components/atoms/StatusPill.tsx | 1 + templates/BOOT.md | 1 + 6 files changed, 39 insertions(+), 15 deletions(-) diff --git a/backend/app/api/agents.py b/backend/app/api/agents.py index 30cec3e5..42cb179f 100644 --- a/backend/app/api/agents.py +++ b/backend/app/api/agents.py @@ -108,7 +108,7 @@ async def _ensure_gateway_session( def _with_computed_status(agent: Agent) -> Agent: now = datetime.utcnow() - if agent.status == "deleting": + if agent.status in {"deleting", "updating"}: return agent if agent.last_seen_at is None: agent.status = "provisioning" @@ -283,6 +283,7 @@ async def update_agent( agent.provision_confirm_token_hash = hash_agent_token(provision_token) agent.provision_requested_at = datetime.utcnow() agent.provision_action = "update" + agent.status = "updating" session.add(agent) session.commit() session.refresh(agent) @@ -560,6 +561,8 @@ def confirm_provision_agent( agent.provision_confirm_token_hash = None agent.provision_requested_at = None agent.provision_action = None + if action == "update": + agent.status = "online" agent.updated_at = datetime.utcnow() session.add(agent) record_activity( diff --git a/backend/app/services/agent_provisioning.py b/backend/app/services/agent_provisioning.py index be703118..aab2ed47 100644 --- a/backend/app/services/agent_provisioning.py +++ b/backend/app/services/agent_provisioning.py @@ -210,9 +210,10 @@ def build_update_message( "```\n" "Note: if any agents.list entry defines heartbeat, only those agents " "run heartbeats.\n" - "7) After the update completes, confirm by calling:\n" + "7) After the update completes (and only after files are written), confirm by calling:\n" f" POST {context['base_url']}/api/v1/agents/{context['agent_id']}/provision/confirm\n" - f" Body: {{\"token\": \"{confirm_token}\", \"action\": \"update\"}}\n\n" + f" Body: {{\"token\": \"{confirm_token}\", \"action\": \"update\"}}\n" + " Mission Control will send the hello message only after this confirmation.\n\n" "Files:" + file_blocks ) diff --git a/frontend/src/app/agents/[agentId]/page.tsx b/frontend/src/app/agents/[agentId]/page.tsx index 409532c3..8eb52f75 100644 --- a/frontend/src/app/agents/[agentId]/page.tsx +++ b/frontend/src/app/agents/[agentId]/page.tsx @@ -48,9 +48,18 @@ type ActivityEvent = { created_at: string; }; -const formatTimestamp = (value: string) => { - const date = new Date(value); - if (Number.isNaN(date.getTime())) return "—"; +const parseTimestamp = (value?: string | null) => { + if (!value) return null; + const hasTz = /[zZ]|[+-]\d\d:\d\d$/.test(value); + const normalized = hasTz ? value : `${value}Z`; + const date = new Date(normalized); + if (Number.isNaN(date.getTime())) return null; + return date; +}; + +const formatTimestamp = (value?: string | null) => { + const date = parseTimestamp(value); + if (!date) return "—"; return date.toLocaleString(undefined, { month: "short", day: "numeric", @@ -59,9 +68,9 @@ const formatTimestamp = (value: string) => { }); }; -const formatRelative = (value: string) => { - const date = new Date(value); - if (Number.isNaN(date.getTime())) return "—"; +const formatRelative = (value?: string | null) => { + const date = parseTimestamp(value); + if (!date) return "—"; const diff = Date.now() - date.getTime(); const minutes = Math.round(diff / 60000); if (minutes < 1) return "Just now"; diff --git a/frontend/src/app/agents/page.tsx b/frontend/src/app/agents/page.tsx index 75137cfd..70f8f6e1 100644 --- a/frontend/src/app/agents/page.tsx +++ b/frontend/src/app/agents/page.tsx @@ -63,9 +63,18 @@ type GatewayStatus = { error?: string; }; -const formatTimestamp = (value: string) => { - const date = new Date(value); - if (Number.isNaN(date.getTime())) return "—"; +const parseTimestamp = (value?: string | null) => { + if (!value) return null; + const hasTz = /[zZ]|[+-]\d\d:\d\d$/.test(value); + const normalized = hasTz ? value : `${value}Z`; + const date = new Date(normalized); + if (Number.isNaN(date.getTime())) return null; + return date; +}; + +const formatTimestamp = (value?: string | null) => { + const date = parseTimestamp(value); + if (!date) return "—"; return date.toLocaleString(undefined, { month: "short", day: "numeric", @@ -74,9 +83,9 @@ const formatTimestamp = (value: string) => { }); }; -const formatRelative = (value: string) => { - const date = new Date(value); - if (Number.isNaN(date.getTime())) return "—"; +const formatRelative = (value?: string | null) => { + const date = parseTimestamp(value); + if (!date) return "—"; const diff = Date.now() - date.getTime(); const minutes = Math.round(diff / 60000); if (minutes < 1) return "Just now"; diff --git a/frontend/src/components/atoms/StatusPill.tsx b/frontend/src/components/atoms/StatusPill.tsx index 8071b673..525e535f 100644 --- a/frontend/src/components/atoms/StatusPill.tsx +++ b/frontend/src/components/atoms/StatusPill.tsx @@ -15,6 +15,7 @@ const STATUS_STYLES: Record< provisioning: "warning", offline: "outline", deleting: "danger", + updating: "accent", }; export function StatusPill({ status }: { status: string }) { diff --git a/templates/BOOT.md b/templates/BOOT.md index c0935ed6..8f17c337 100644 --- a/templates/BOOT.md +++ b/templates/BOOT.md @@ -2,6 +2,7 @@ On startup: 1) Verify API reachability (GET {{ base_url }}/api/v1/gateway/status). + - A 401 Unauthorized response is acceptable here for agents (auth-protected endpoint). 2) Connect to Mission Control once by sending a heartbeat check-in. 2a) Use task comments for updates; do not send task updates to chat/web. 3) If you send a boot message, end with NO_REPLY.