letagents 0.12.1 → 0.12.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mcp/agent-presence.js +39 -0
- package/dist/mcp/room-events-query.js +18 -0
- package/dist/mcp/server.js +139 -8
- package/dist/shared/agent-presence.js +26 -0
- package/package.json +1 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const IDLE_STATUS_RE = /\b(idle|available|online|polling|monitoring|watching|ready)\b/i;
|
|
2
|
+
const REVIEWING_STATUS_RE = /\b(review|reviewing|approve|approval|approving)\b/i;
|
|
3
|
+
const BLOCKED_STATUS_RE = /\b(blocked|waiting|stuck)\b/i;
|
|
4
|
+
export function classifyPresenceStatusText(statusText, fallback = "working") {
|
|
5
|
+
const normalized = statusText.trim();
|
|
6
|
+
if (!normalized) {
|
|
7
|
+
return fallback;
|
|
8
|
+
}
|
|
9
|
+
if (BLOCKED_STATUS_RE.test(normalized)) {
|
|
10
|
+
return "blocked";
|
|
11
|
+
}
|
|
12
|
+
if (IDLE_STATUS_RE.test(normalized)) {
|
|
13
|
+
return "idle";
|
|
14
|
+
}
|
|
15
|
+
if (REVIEWING_STATUS_RE.test(normalized)) {
|
|
16
|
+
return "reviewing";
|
|
17
|
+
}
|
|
18
|
+
return "working";
|
|
19
|
+
}
|
|
20
|
+
export function deriveTaskPresenceStatus(taskStatus, fallback = "working") {
|
|
21
|
+
switch (taskStatus) {
|
|
22
|
+
case "blocked":
|
|
23
|
+
return "blocked";
|
|
24
|
+
case "in_review":
|
|
25
|
+
return "reviewing";
|
|
26
|
+
case "merged":
|
|
27
|
+
case "done":
|
|
28
|
+
case "accepted":
|
|
29
|
+
return "idle";
|
|
30
|
+
case "assigned":
|
|
31
|
+
case "in_progress":
|
|
32
|
+
return "working";
|
|
33
|
+
default:
|
|
34
|
+
return fallback;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function getRoomIdentityPresenceCacheKey(roomId, actorLabel) {
|
|
38
|
+
return JSON.stringify([roomId, actorLabel]);
|
|
39
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function buildRoomEventsQueryString(input) {
|
|
2
|
+
const params = new URLSearchParams();
|
|
3
|
+
if (input.event_type)
|
|
4
|
+
params.set("event_type", input.event_type);
|
|
5
|
+
if (input.object_id)
|
|
6
|
+
params.set("object_id", input.object_id);
|
|
7
|
+
if (input.actor)
|
|
8
|
+
params.set("actor", input.actor);
|
|
9
|
+
if (input.since)
|
|
10
|
+
params.set("since", input.since);
|
|
11
|
+
if (input.until)
|
|
12
|
+
params.set("until", input.until);
|
|
13
|
+
if (input.after)
|
|
14
|
+
params.set("after", input.after);
|
|
15
|
+
if (input.limit)
|
|
16
|
+
params.set("limit", String(input.limit));
|
|
17
|
+
return params.toString();
|
|
18
|
+
}
|
package/dist/mcp/server.js
CHANGED
|
@@ -16,10 +16,13 @@ import { encodeRoomIdPath, getCanonicalRoomWebPath, looksLikeInviteCode, normali
|
|
|
16
16
|
import { buildAgentActorLabel, formatOwnerAttribution, inferAgentIdeLabel, toTitleCaseCodename, } from "../shared/agent-identity.js";
|
|
17
17
|
import { buildRoomAgentPrompt, normalizeAgentPromptKind, } from "../shared/room-agent-prompts.js";
|
|
18
18
|
import { inspectLocalCodexSession, startLocalCodexSession, stopLocalCodexSession, toPublicCodexLiveSession, } from "./codex-session.js";
|
|
19
|
+
import { classifyPresenceStatusText, deriveTaskPresenceStatus, getRoomIdentityPresenceCacheKey, } from "./agent-presence.js";
|
|
20
|
+
import { buildRoomEventsQueryString } from "./room-events-query.js";
|
|
19
21
|
let currentRoom = null;
|
|
20
22
|
let currentAgentIdentityKey = "";
|
|
21
23
|
let currentAgentIdentity = null;
|
|
22
24
|
let currentAuthenticatedAccount = undefined;
|
|
25
|
+
const roomPresenceByIdentity = new Map();
|
|
23
26
|
// ---------------------------------------------------------------------------
|
|
24
27
|
// Conversation-scoped identity (Option C: per-conversation hints)
|
|
25
28
|
// ---------------------------------------------------------------------------
|
|
@@ -734,6 +737,42 @@ function touchCurrentRoom(lastMessageId) {
|
|
|
734
737
|
}
|
|
735
738
|
touchRoomSession(currentRoom.room_id, lastMessageId);
|
|
736
739
|
}
|
|
740
|
+
function getRememberedRoomPresence(roomId, identity) {
|
|
741
|
+
if (!roomId || !identity) {
|
|
742
|
+
return { status: "idle", status_text: null };
|
|
743
|
+
}
|
|
744
|
+
return (roomPresenceByIdentity.get(getRoomIdentityPresenceCacheKey(roomId, identity.actor_label)) ?? { status: "idle", status_text: null });
|
|
745
|
+
}
|
|
746
|
+
async function syncRoomPresence(roomId, identity, presence) {
|
|
747
|
+
if (!roomId || !identity) {
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
roomPresenceByIdentity.set(getRoomIdentityPresenceCacheKey(roomId, identity.actor_label), presence);
|
|
751
|
+
try {
|
|
752
|
+
await apiCall(`/rooms/${encodeRoomIdPath(roomId)}/presence`, {
|
|
753
|
+
method: "POST",
|
|
754
|
+
body: JSON.stringify({
|
|
755
|
+
actor_label: identity.actor_label,
|
|
756
|
+
agent_key: identity.canonical_key,
|
|
757
|
+
display_name: identity.display_name,
|
|
758
|
+
owner_label: identity.owner_label,
|
|
759
|
+
ide_label: identity.ide_label,
|
|
760
|
+
status: presence.status,
|
|
761
|
+
status_text: presence.status_text,
|
|
762
|
+
}),
|
|
763
|
+
});
|
|
764
|
+
touchRoomSession(roomId);
|
|
765
|
+
}
|
|
766
|
+
catch (error) {
|
|
767
|
+
if (isMissingRouteError(error)) {
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
throw error;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
async function heartbeatRoomPresence(roomId, identity) {
|
|
774
|
+
await syncRoomPresence(roomId, identity, getRememberedRoomPresence(roomId, identity));
|
|
775
|
+
}
|
|
737
776
|
function getTargetRoomId(roomId) {
|
|
738
777
|
return roomId || currentRoom?.room_id || null;
|
|
739
778
|
}
|
|
@@ -827,6 +866,10 @@ async function joinRoomIdentifier(identifier, joinedVia) {
|
|
|
827
866
|
joined_via: joinedVia,
|
|
828
867
|
}));
|
|
829
868
|
const agentIdentity = await ensureAgentIdentity();
|
|
869
|
+
await syncRoomPresence(room.room_id, agentIdentity, {
|
|
870
|
+
status: "idle",
|
|
871
|
+
status_text: "online in room",
|
|
872
|
+
});
|
|
830
873
|
return {
|
|
831
874
|
room,
|
|
832
875
|
response: {
|
|
@@ -855,6 +898,10 @@ async function joinRoomIdentifier(identifier, joinedVia) {
|
|
|
855
898
|
joined_via: joinedVia,
|
|
856
899
|
}));
|
|
857
900
|
const agentIdentity = await ensureAgentIdentity();
|
|
901
|
+
await syncRoomPresence(room.room_id, agentIdentity, {
|
|
902
|
+
status: "idle",
|
|
903
|
+
status_text: "online in room",
|
|
904
|
+
});
|
|
858
905
|
return {
|
|
859
906
|
room,
|
|
860
907
|
response: {
|
|
@@ -883,6 +930,10 @@ async function joinRoomIdentifier(identifier, joinedVia) {
|
|
|
883
930
|
joined_via: joinedVia,
|
|
884
931
|
}));
|
|
885
932
|
const agentIdentity = await ensureAgentIdentity();
|
|
933
|
+
await syncRoomPresence(room.room_id, agentIdentity, {
|
|
934
|
+
status: "idle",
|
|
935
|
+
status_text: "online in room",
|
|
936
|
+
});
|
|
886
937
|
return {
|
|
887
938
|
room,
|
|
888
939
|
response: {
|
|
@@ -908,6 +959,10 @@ async function createInviteRoom() {
|
|
|
908
959
|
joined_via: "join_code",
|
|
909
960
|
}));
|
|
910
961
|
const agentIdentity = await ensureAgentIdentity();
|
|
962
|
+
await syncRoomPresence(room.room_id, agentIdentity, {
|
|
963
|
+
status: "idle",
|
|
964
|
+
status_text: "online in room",
|
|
965
|
+
});
|
|
911
966
|
return {
|
|
912
967
|
room,
|
|
913
968
|
response: {
|
|
@@ -1362,6 +1417,10 @@ server.tool("post_status", "Broadcast a lightweight status update to the current
|
|
|
1362
1417
|
},
|
|
1363
1418
|
});
|
|
1364
1419
|
touchCurrentRoom(typeof message.id === "string" ? message.id : undefined);
|
|
1420
|
+
await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, {
|
|
1421
|
+
status: classifyPresenceStatusText(status, getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity).status),
|
|
1422
|
+
status_text: status,
|
|
1423
|
+
});
|
|
1365
1424
|
return {
|
|
1366
1425
|
content: [
|
|
1367
1426
|
{
|
|
@@ -1420,6 +1479,7 @@ server.tool("add_task", "Add a new task to the room board. Tasks normally start
|
|
|
1420
1479
|
}),
|
|
1421
1480
|
},
|
|
1422
1481
|
});
|
|
1482
|
+
await heartbeatRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity);
|
|
1423
1483
|
return {
|
|
1424
1484
|
content: [
|
|
1425
1485
|
{
|
|
@@ -1471,10 +1531,49 @@ server.tool("get_board", "Get the current task board for the room. By default sh
|
|
|
1471
1531
|
break;
|
|
1472
1532
|
afterCursor = lastTask.id;
|
|
1473
1533
|
}
|
|
1534
|
+
await heartbeatRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, await ensureAgentIdentity());
|
|
1474
1535
|
return {
|
|
1475
1536
|
content: [{ type: "text", text: JSON.stringify({ success: true, tasks: allTasks }, null, 2) }],
|
|
1476
1537
|
};
|
|
1477
1538
|
});
|
|
1539
|
+
server.tool("get_room_events", "Get GitHub events for the room (PRs, issues, reviews, check runs, etc.). " +
|
|
1540
|
+
"Returns a paginated list of normalized GitHub events persisted from webhooks. " +
|
|
1541
|
+
"Use this to check what happened in the repo without parsing chat messages.", {
|
|
1542
|
+
event_type: z.string().optional().describe("Filter by event type: pull_request, issue, issue_comment, pull_request_review, check_run, installation, installation_repositories, repository"),
|
|
1543
|
+
object_id: z.string().optional().describe("Filter by GitHub object ID (e.g. PR number, issue number)"),
|
|
1544
|
+
actor: z.string().optional().describe("Filter by GitHub login of the actor"),
|
|
1545
|
+
since: z.string().optional().describe("ISO timestamp — only events after this time"),
|
|
1546
|
+
until: z.string().optional().describe("ISO timestamp — only events before this time"),
|
|
1547
|
+
after: z.string().optional().describe("Cursor event ID for pagination (from a previous response)"),
|
|
1548
|
+
limit: z.number().int().min(1).max(100).optional().describe("Max events to return (default 50, max 100)"),
|
|
1549
|
+
room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
|
|
1550
|
+
}, async ({ event_type, object_id, actor, since, until, after, limit, room_id }) => {
|
|
1551
|
+
const targetRoomId = getTargetRoomId(room_id);
|
|
1552
|
+
const targetProjectId = getFallbackProjectId();
|
|
1553
|
+
if (!targetRoomId && !targetProjectId) {
|
|
1554
|
+
return {
|
|
1555
|
+
content: [{ type: "text", text: JSON.stringify({ success: false, error: "Not in a room. Join one first." }) }],
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
const qs = buildRoomEventsQueryString({
|
|
1559
|
+
event_type,
|
|
1560
|
+
object_id,
|
|
1561
|
+
actor,
|
|
1562
|
+
since,
|
|
1563
|
+
until,
|
|
1564
|
+
after,
|
|
1565
|
+
limit,
|
|
1566
|
+
});
|
|
1567
|
+
const result = await roomScopedApiCall({
|
|
1568
|
+
room_id: targetRoomId,
|
|
1569
|
+
project_id: targetProjectId,
|
|
1570
|
+
room_path: (targetRoomId) => `/rooms/${encodeRoomIdPath(targetRoomId)}/events${qs ? `?${qs}` : ""}`,
|
|
1571
|
+
project_path: (targetProjectId) => `/rooms/${encodeURIComponent(targetProjectId)}/events${qs ? `?${qs}` : ""}`,
|
|
1572
|
+
});
|
|
1573
|
+
return {
|
|
1574
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, events: result.events ?? [], has_more: result.has_more ?? false }, null, 2) }],
|
|
1575
|
+
};
|
|
1576
|
+
});
|
|
1478
1577
|
server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted' " +
|
|
1479
1578
|
"status. This sets the assignee to you and moves the status to 'assigned'. " +
|
|
1480
1579
|
"Do NOT claim proposed tasks — they need to be accepted first.", {
|
|
@@ -1505,6 +1604,10 @@ server.tool("claim_task", "Claim an accepted task. The task must be in 'accepted
|
|
|
1505
1604
|
body: JSON.stringify({ status: "assigned", assignee: identity.actor_label }),
|
|
1506
1605
|
},
|
|
1507
1606
|
});
|
|
1607
|
+
await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, {
|
|
1608
|
+
status: "working",
|
|
1609
|
+
status_text: `claimed ${task_id}`,
|
|
1610
|
+
});
|
|
1508
1611
|
return {
|
|
1509
1612
|
content: [
|
|
1510
1613
|
{
|
|
@@ -1530,9 +1633,23 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
|
|
|
1530
1633
|
.optional()
|
|
1531
1634
|
.describe("New assignee for the task. Defaults to the current agent when status=assigned."),
|
|
1532
1635
|
pr_url: z.string().optional().describe("PR URL to link to the task"),
|
|
1636
|
+
workflow_artifacts: z
|
|
1637
|
+
.array(z.object({
|
|
1638
|
+
provider: z.enum(["github", "gitlab", "bitbucket", "unknown"]),
|
|
1639
|
+
kind: z.enum(["issue", "branch", "pull_request", "merge_request", "review", "check_run", "merge"]),
|
|
1640
|
+
id: z.string().nullable().optional(),
|
|
1641
|
+
number: z.number().int().nullable().optional(),
|
|
1642
|
+
title: z.string().nullable().optional(),
|
|
1643
|
+
url: z.string().nullable().optional(),
|
|
1644
|
+
ref: z.string().nullable().optional(),
|
|
1645
|
+
state: z.string().nullable().optional(),
|
|
1646
|
+
}).strict())
|
|
1647
|
+
.max(32)
|
|
1648
|
+
.optional()
|
|
1649
|
+
.describe("Persisted provider-neutral task workflow artifacts to attach to the task"),
|
|
1533
1650
|
room_id: z.string().optional().describe("Canonical room ID. Defaults to current room."),
|
|
1534
1651
|
conversation_id: z.string().optional().describe("Optional conversation ID for per-conversation identity scoping."),
|
|
1535
|
-
}, async ({ task_id, status, assignee, pr_url, room_id, conversation_id }) => {
|
|
1652
|
+
}, async ({ task_id, status, assignee, pr_url, workflow_artifacts, room_id, conversation_id }) => {
|
|
1536
1653
|
const targetRoomId = getTargetRoomId(room_id);
|
|
1537
1654
|
const targetProjectId = getFallbackProjectId();
|
|
1538
1655
|
if (!targetRoomId && !targetProjectId) {
|
|
@@ -1541,9 +1658,7 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
|
|
|
1541
1658
|
};
|
|
1542
1659
|
}
|
|
1543
1660
|
try {
|
|
1544
|
-
const identity =
|
|
1545
|
-
? (getConversationIdentity(conversation_id) ?? await ensureAgentIdentity())
|
|
1546
|
-
: null;
|
|
1661
|
+
const identity = getConversationIdentity(conversation_id) ?? await ensureAgentIdentity();
|
|
1547
1662
|
const updated = await roomScopedApiCall({
|
|
1548
1663
|
room_id: targetRoomId,
|
|
1549
1664
|
project_id: targetProjectId,
|
|
@@ -1553,11 +1668,18 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
|
|
|
1553
1668
|
method: "PATCH",
|
|
1554
1669
|
body: JSON.stringify({
|
|
1555
1670
|
status,
|
|
1556
|
-
assignee: assignee
|
|
1671
|
+
assignee: status === "assigned" && !assignee ? identity.actor_label : assignee,
|
|
1557
1672
|
pr_url,
|
|
1673
|
+
workflow_artifacts,
|
|
1558
1674
|
}),
|
|
1559
1675
|
},
|
|
1560
1676
|
});
|
|
1677
|
+
await syncRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity, {
|
|
1678
|
+
status: deriveTaskPresenceStatus(status ?? null, getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity).status),
|
|
1679
|
+
status_text: status
|
|
1680
|
+
? `${task_id} -> ${status}`
|
|
1681
|
+
: getRememberedRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity).status_text,
|
|
1682
|
+
});
|
|
1561
1683
|
return {
|
|
1562
1684
|
content: [
|
|
1563
1685
|
{
|
|
@@ -1565,7 +1687,7 @@ server.tool("update_task", "Update a task's status or assignee. Status transitio
|
|
|
1565
1687
|
text: JSON.stringify({
|
|
1566
1688
|
success: true,
|
|
1567
1689
|
task: updated,
|
|
1568
|
-
agent_identity:
|
|
1690
|
+
agent_identity: toPublicAgentIdentity(identity),
|
|
1569
1691
|
}, null, 2),
|
|
1570
1692
|
},
|
|
1571
1693
|
],
|
|
@@ -1765,11 +1887,15 @@ server.tool("send_message", "Send a message to a Let Agents Chat room.", {
|
|
|
1765
1887
|
.optional()
|
|
1766
1888
|
.describe("Deprecated override. Agent identity is resolved automatically on room entry."),
|
|
1767
1889
|
text: z.string().describe("The message text to send"),
|
|
1890
|
+
reply_to: z
|
|
1891
|
+
.string()
|
|
1892
|
+
.optional()
|
|
1893
|
+
.describe("Optional message id to quote-reply to (for example `msg_42`)."),
|
|
1768
1894
|
conversation_id: z
|
|
1769
1895
|
.string()
|
|
1770
1896
|
.optional()
|
|
1771
1897
|
.describe("Optional conversation ID for per-conversation identity scoping."),
|
|
1772
|
-
}, async ({ room_id, sender: _sender, text, conversation_id }) => {
|
|
1898
|
+
}, async ({ room_id, sender: _sender, text, reply_to, conversation_id }) => {
|
|
1773
1899
|
const targetRoomId = getTargetRoomId(room_id);
|
|
1774
1900
|
const targetProjectId = getFallbackProjectId();
|
|
1775
1901
|
if (!targetRoomId && !targetProjectId) {
|
|
@@ -1783,10 +1909,11 @@ server.tool("send_message", "Send a message to a Let Agents Chat room.", {
|
|
|
1783
1909
|
project_path: (targetProjectId) => `/projects/${encodeURIComponent(targetProjectId)}/messages`,
|
|
1784
1910
|
options: {
|
|
1785
1911
|
method: "POST",
|
|
1786
|
-
body: JSON.stringify({ sender: identity.actor_label, text }),
|
|
1912
|
+
body: JSON.stringify({ sender: identity.actor_label, text, reply_to }),
|
|
1787
1913
|
},
|
|
1788
1914
|
});
|
|
1789
1915
|
touchCurrentRoom(typeof message.id === "string" ? message.id : undefined);
|
|
1916
|
+
await heartbeatRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity);
|
|
1790
1917
|
return {
|
|
1791
1918
|
content: [
|
|
1792
1919
|
{
|
|
@@ -1831,6 +1958,7 @@ server.tool("read_messages", "Read all messages from a Let Agents Chat room.", {
|
|
|
1831
1958
|
break;
|
|
1832
1959
|
afterCursor = lastMsg.id;
|
|
1833
1960
|
}
|
|
1961
|
+
await heartbeatRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, await ensureAgentIdentity());
|
|
1834
1962
|
const output = { messages: toAgentReadableMessages(allMessages) };
|
|
1835
1963
|
if (roomIdFromResponse) {
|
|
1836
1964
|
output[targetRoomId ? "room_id" : "project_id"] = roomIdFromResponse;
|
|
@@ -1860,6 +1988,8 @@ server.tool("wait_for_messages", "Wait for new messages in a Let Agents Chat roo
|
|
|
1860
1988
|
}, async ({ room_id, after_message_id, timeout }) => {
|
|
1861
1989
|
const targetRoomId = getTargetRoomId(room_id);
|
|
1862
1990
|
const targetProjectId = getFallbackProjectId();
|
|
1991
|
+
const identity = await ensureAgentIdentity();
|
|
1992
|
+
await heartbeatRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity);
|
|
1863
1993
|
const serverTimeout = Math.min(Math.max(timeout || DEFAULT_POLL_TIMEOUT_MS, 1000), MAX_POLL_TIMEOUT_MS);
|
|
1864
1994
|
const clientTimeout = serverTimeout + 5000; // 5s buffer over server timeout
|
|
1865
1995
|
const params = new URLSearchParams();
|
|
@@ -1905,6 +2035,7 @@ server.tool("wait_for_messages", "Wait for new messages in a Let Agents Chat roo
|
|
|
1905
2035
|
if (targetRoomId) {
|
|
1906
2036
|
touchRoomSession(targetRoomId, getLastMessageId(output));
|
|
1907
2037
|
}
|
|
2038
|
+
await heartbeatRoomPresence(targetRoomId ?? currentRoom?.room_id ?? null, identity);
|
|
1908
2039
|
return {
|
|
1909
2040
|
content: [
|
|
1910
2041
|
{
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const AGENT_PRESENCE_STATUSES = [
|
|
2
|
+
"idle",
|
|
3
|
+
"working",
|
|
4
|
+
"reviewing",
|
|
5
|
+
"blocked",
|
|
6
|
+
];
|
|
7
|
+
export const AGENT_PRESENCE_FRESHNESS = [
|
|
8
|
+
"active",
|
|
9
|
+
"stale",
|
|
10
|
+
];
|
|
11
|
+
export const ACTIVE_AGENT_PRESENCE_WINDOW_MS = 90_000;
|
|
12
|
+
export function normalizeAgentPresenceStatus(value) {
|
|
13
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
14
|
+
return AGENT_PRESENCE_STATUSES.includes(normalized)
|
|
15
|
+
? normalized
|
|
16
|
+
: null;
|
|
17
|
+
}
|
|
18
|
+
export function getAgentPresenceFreshness(lastHeartbeatAt, now = Date.now()) {
|
|
19
|
+
const heartbeatTime = new Date(lastHeartbeatAt).getTime();
|
|
20
|
+
if (!Number.isFinite(heartbeatTime)) {
|
|
21
|
+
return "stale";
|
|
22
|
+
}
|
|
23
|
+
return now - heartbeatTime <= ACTIVE_AGENT_PRESENCE_WINDOW_MS
|
|
24
|
+
? "active"
|
|
25
|
+
: "stale";
|
|
26
|
+
}
|