@agent-team-foundation/first-tree-hub 0.6.3 → 0.7.0
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/{bootstrap-DNL1cEwv.mjs → bootstrap-CRDR6NwE.mjs} +1 -1
- package/dist/cli/index.mjs +35 -11
- package/dist/{core-B10jgThe.mjs → core-4nvleGlC.mjs} +567 -158
- package/dist/drizzle/0022_session_events.sql +32 -0
- package/dist/drizzle/meta/_journal.json +7 -0
- package/dist/{feishu-BoMJHlOv.mjs → feishu-CJ08ntOD.mjs} +54 -6
- package/dist/index.mjs +3 -3
- package/dist/web/assets/index-30C-bada.js +333 -0
- package/dist/web/assets/index-COlVuDVR.css +1 -0
- package/dist/web/index.html +9 -2
- package/package.json +1 -1
- package/dist/web/assets/index-CTl4pHIL.css +0 -1
- package/dist/web/assets/index-CnLpaSBg.js +0 -308
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as setConfigValue, S as serverConfigSchema, _ as loadAgents, d as DEFAULT_HOME_DIR$1, f as agentConfigSchema, g as initConfig, i as loadCredentials, l as DEFAULT_CONFIG_DIR, m as collectMissingPrompts, n as ensureFreshAccessToken, o as resolveServerUrl, p as clientConfigSchema, s as saveAgentConfig, u as DEFAULT_DATA_DIR$1, x as resolveConfigReadonly } from "./bootstrap-
|
|
2
|
-
import { $ as
|
|
1
|
+
import { C as setConfigValue, S as serverConfigSchema, _ as loadAgents, d as DEFAULT_HOME_DIR$1, f as agentConfigSchema, g as initConfig, i as loadCredentials, l as DEFAULT_CONFIG_DIR, m as collectMissingPrompts, n as ensureFreshAccessToken, o as resolveServerUrl, p as clientConfigSchema, s as saveAgentConfig, u as DEFAULT_DATA_DIR$1, x as resolveConfigReadonly } from "./bootstrap-CRDR6NwE.mjs";
|
|
2
|
+
import { $ as updateAgentSchema, A as createOrganizationSchema, B as paginationQuerySchema, C as clientRegisterSchema, D as createAgentSchema, E as createAdapterMappingSchema, F as isRedactedEnvValue, G as sendToAgentSchema, H as runtimeStateMessageSchema, I as linkTaskChatSchema, J as sessionEventSchema$1, K as sessionCompletionMessageSchema, L as loginSchema, M as delegateFeishuUserSchema, N as dryRunAgentRuntimeConfigSchema, O as createChatSchema, P as inboxPollQuerySchema, Q as updateAgentRuntimeConfigSchema, R as messageSourceSchema$1, S as agentTypeSchema$1, T as createAdapterConfigSchema, U as selfServiceFeishuBotSchema, V as refreshTokenSchema, W as sendMessageSchema, X as taskListQuerySchema, Y as sessionStateMessageSchema, Z as updateAdapterConfigSchema, _ as addParticipantSchema, a as AGENT_SELECTOR_HEADER$1, b as agentBindRequestSchema, c as AGENT_TYPES, d as SYSTEM_CONFIG_DEFAULTS, et as updateMemberSchema, f as TASK_CREATOR_TYPES, g as WS_AUTH_FRAME_TIMEOUT_MS, h as TASK_TERMINAL_STATUSES, i as AGENT_BIND_REJECT_REASONS, it as wsAuthFrameSchema, j as createTaskSchema, k as createMemberSchema, l as AGENT_VISIBILITY, m as TASK_STATUSES, nt as updateSystemConfigSchema, o as AGENT_SOURCES, p as TASK_HEALTH_SIGNALS, q as sessionEventMessageSchema, rt as updateTaskStatusSchema, s as AGENT_STATUSES, tt as updateOrganizationSchema, u as DEFAULT_AGENT_RUNTIME_CONFIG_PAYLOAD, v as adminCreateTaskSchema, w as connectTokenExchangeSchema, x as agentRuntimeConfigPayloadSchema$1, y as adminUpdateTaskSchema, z as notificationQuerySchema } from "./feishu-CJ08ntOD.mjs";
|
|
3
3
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { ZodError, z } from "zod";
|
|
@@ -22,7 +22,7 @@ import rateLimit from "@fastify/rate-limit";
|
|
|
22
22
|
import fastifyStatic from "@fastify/static";
|
|
23
23
|
import websocket from "@fastify/websocket";
|
|
24
24
|
import Fastify from "fastify";
|
|
25
|
-
import { bigserial, boolean, index, integer, jsonb, pgTable, primaryKey, serial, text, timestamp, unique } from "drizzle-orm/pg-core";
|
|
25
|
+
import { bigserial, boolean, index, integer, jsonb, pgTable, primaryKey, serial, text, timestamp, unique, uniqueIndex } from "drizzle-orm/pg-core";
|
|
26
26
|
import { SignJWT, jwtVerify } from "jose";
|
|
27
27
|
import { Client, EventDispatcher, LoggerLevel, WSClient } from "@larksuiteoapi/node-sdk";
|
|
28
28
|
//#region ../client/dist/index.mjs
|
|
@@ -559,17 +559,60 @@ z.object({
|
|
|
559
559
|
createdAt: z.string(),
|
|
560
560
|
updatedAt: z.string()
|
|
561
561
|
});
|
|
562
|
+
const pulseBucketSchema = z.object({
|
|
563
|
+
workingCount: z.number().int().nonnegative(),
|
|
564
|
+
errorMask: z.boolean()
|
|
565
|
+
});
|
|
566
|
+
z.object({
|
|
567
|
+
type: z.literal("pulse:tick"),
|
|
568
|
+
organizationId: z.string(),
|
|
569
|
+
agents: z.record(z.string(), z.array(pulseBucketSchema).length(32))
|
|
570
|
+
});
|
|
571
|
+
const sessionEventKind = z.enum(["tool_call", "error"]);
|
|
572
|
+
const toolCallEventPayload = z.object({
|
|
573
|
+
toolUseId: z.string(),
|
|
574
|
+
name: z.string(),
|
|
575
|
+
args: z.unknown(),
|
|
576
|
+
status: z.enum([
|
|
577
|
+
"pending",
|
|
578
|
+
"ok",
|
|
579
|
+
"error"
|
|
580
|
+
]),
|
|
581
|
+
durationMs: z.number().int().nonnegative().optional(),
|
|
582
|
+
resultPreview: z.string().max(400).optional()
|
|
583
|
+
});
|
|
584
|
+
const errorEventPayload = z.object({
|
|
585
|
+
source: z.enum([
|
|
586
|
+
"sdk",
|
|
587
|
+
"runtime",
|
|
588
|
+
"tool"
|
|
589
|
+
]),
|
|
590
|
+
message: z.string().max(2e3)
|
|
591
|
+
});
|
|
592
|
+
const sessionEventSchema = z.discriminatedUnion("kind", [z.object({
|
|
593
|
+
kind: z.literal("tool_call"),
|
|
594
|
+
payload: toolCallEventPayload
|
|
595
|
+
}), z.object({
|
|
596
|
+
kind: z.literal("error"),
|
|
597
|
+
payload: errorEventPayload
|
|
598
|
+
})]);
|
|
562
599
|
z.object({
|
|
563
600
|
id: z.string(),
|
|
564
601
|
agentId: z.string(),
|
|
565
602
|
chatId: z.string(),
|
|
566
|
-
|
|
567
|
-
|
|
603
|
+
seq: z.number().int().positive(),
|
|
604
|
+
kind: sessionEventKind,
|
|
605
|
+
payload: z.union([toolCallEventPayload, errorEventPayload]),
|
|
606
|
+
createdAt: z.string()
|
|
568
607
|
});
|
|
569
608
|
z.object({
|
|
570
609
|
agentId: z.string(),
|
|
571
610
|
chatId: z.string(),
|
|
572
|
-
|
|
611
|
+
event: sessionEventSchema
|
|
612
|
+
});
|
|
613
|
+
z.object({
|
|
614
|
+
agentId: z.string(),
|
|
615
|
+
chatId: z.string()
|
|
573
616
|
});
|
|
574
617
|
const orgStatsSchema = z.object({
|
|
575
618
|
organizationId: z.string(),
|
|
@@ -941,13 +984,21 @@ var ClientConnection = class extends EventEmitter {
|
|
|
941
984
|
runtimeState
|
|
942
985
|
}));
|
|
943
986
|
}
|
|
944
|
-
|
|
987
|
+
reportSessionEvent(agentId, chatId, event) {
|
|
945
988
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
|
946
989
|
this.ws.send(JSON.stringify({
|
|
947
|
-
type: "session:
|
|
990
|
+
type: "session:event",
|
|
948
991
|
agentId,
|
|
949
992
|
chatId,
|
|
950
|
-
|
|
993
|
+
event
|
|
994
|
+
}));
|
|
995
|
+
}
|
|
996
|
+
reportSessionCompletion(agentId, chatId) {
|
|
997
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
|
998
|
+
this.ws.send(JSON.stringify({
|
|
999
|
+
type: "session:completion",
|
|
1000
|
+
agentId,
|
|
1001
|
+
chatId
|
|
951
1002
|
}));
|
|
952
1003
|
}
|
|
953
1004
|
async disconnect() {
|
|
@@ -1538,6 +1589,95 @@ function cleanWorkspaces(workspaceRoot, activeChatIds, ttlMs = DEFAULT_WORKSPACE
|
|
|
1538
1589
|
return removed;
|
|
1539
1590
|
}
|
|
1540
1591
|
const MAX_RETRIES = 2;
|
|
1592
|
+
const TOOL_RESULT_PREVIEW_LIMIT = 400;
|
|
1593
|
+
function extractContentBlocks(message) {
|
|
1594
|
+
if (!message || typeof message !== "object") return [];
|
|
1595
|
+
const inner = message.message;
|
|
1596
|
+
if (!inner || typeof inner !== "object") return [];
|
|
1597
|
+
const content = inner.content;
|
|
1598
|
+
return Array.isArray(content) ? content : [];
|
|
1599
|
+
}
|
|
1600
|
+
function isToolUseBlock(block) {
|
|
1601
|
+
if (!block || typeof block !== "object") return false;
|
|
1602
|
+
const b = block;
|
|
1603
|
+
return b.type === "tool_use" && typeof b.id === "string" && typeof b.name === "string";
|
|
1604
|
+
}
|
|
1605
|
+
function isToolResultBlock(block) {
|
|
1606
|
+
if (!block || typeof block !== "object") return false;
|
|
1607
|
+
const b = block;
|
|
1608
|
+
return b.type === "tool_result" && typeof b.tool_use_id === "string";
|
|
1609
|
+
}
|
|
1610
|
+
function isResultMessage(message) {
|
|
1611
|
+
if (!message || typeof message !== "object") return false;
|
|
1612
|
+
const m = message;
|
|
1613
|
+
return m.type === "result" && typeof m.subtype === "string";
|
|
1614
|
+
}
|
|
1615
|
+
function extractToolResultText(content) {
|
|
1616
|
+
if (typeof content === "string") return content;
|
|
1617
|
+
if (!Array.isArray(content)) return "";
|
|
1618
|
+
const parts = [];
|
|
1619
|
+
for (const part of content) {
|
|
1620
|
+
if (!part || typeof part !== "object") continue;
|
|
1621
|
+
const p = part;
|
|
1622
|
+
if (p.type === "text" && typeof p.text === "string") parts.push(p.text);
|
|
1623
|
+
}
|
|
1624
|
+
return parts.join("\n");
|
|
1625
|
+
}
|
|
1626
|
+
function createToolCallProcessor(emit) {
|
|
1627
|
+
const pending = /* @__PURE__ */ new Map();
|
|
1628
|
+
function pairResult(block) {
|
|
1629
|
+
const entry = pending.get(block.tool_use_id);
|
|
1630
|
+
if (!entry) return;
|
|
1631
|
+
const status = block.is_error === true ? "error" : "ok";
|
|
1632
|
+
const durationMs = Date.now() - entry.startedAt;
|
|
1633
|
+
const previewRaw = extractToolResultText(block.content);
|
|
1634
|
+
const resultPreview = previewRaw.length > 0 ? previewRaw.slice(0, TOOL_RESULT_PREVIEW_LIMIT) : void 0;
|
|
1635
|
+
emit({
|
|
1636
|
+
kind: "tool_call",
|
|
1637
|
+
payload: {
|
|
1638
|
+
toolUseId: entry.toolUseId,
|
|
1639
|
+
name: entry.name,
|
|
1640
|
+
args: entry.args,
|
|
1641
|
+
status,
|
|
1642
|
+
durationMs,
|
|
1643
|
+
...resultPreview !== void 0 ? { resultPreview } : {}
|
|
1644
|
+
}
|
|
1645
|
+
});
|
|
1646
|
+
pending.delete(block.tool_use_id);
|
|
1647
|
+
}
|
|
1648
|
+
return {
|
|
1649
|
+
onMessage(message) {
|
|
1650
|
+
if (!message || typeof message !== "object") return;
|
|
1651
|
+
const type = message.type;
|
|
1652
|
+
if (type === "assistant") for (const block of extractContentBlocks(message)) {
|
|
1653
|
+
if (!isToolUseBlock(block)) continue;
|
|
1654
|
+
pending.set(block.id, {
|
|
1655
|
+
toolUseId: block.id,
|
|
1656
|
+
name: block.name,
|
|
1657
|
+
args: block.input,
|
|
1658
|
+
startedAt: Date.now()
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
else if (type === "user") {
|
|
1662
|
+
for (const block of extractContentBlocks(message)) if (isToolResultBlock(block)) pairResult(block);
|
|
1663
|
+
}
|
|
1664
|
+
},
|
|
1665
|
+
flush() {
|
|
1666
|
+
if (pending.size === 0) return;
|
|
1667
|
+
for (const entry of pending.values()) emit({
|
|
1668
|
+
kind: "tool_call",
|
|
1669
|
+
payload: {
|
|
1670
|
+
toolUseId: entry.toolUseId,
|
|
1671
|
+
name: entry.name,
|
|
1672
|
+
args: entry.args,
|
|
1673
|
+
status: "pending",
|
|
1674
|
+
durationMs: Date.now() - entry.startedAt
|
|
1675
|
+
}
|
|
1676
|
+
});
|
|
1677
|
+
pending.clear();
|
|
1678
|
+
}
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1541
1681
|
/**
|
|
1542
1682
|
* Map a payload's MCP server list to the SDK's record type. Handles all three
|
|
1543
1683
|
* transports (stdio/http/sse) defined in the M1 schema.
|
|
@@ -1725,57 +1865,84 @@ const createClaudeCodeHandler = (config) => {
|
|
|
1725
1865
|
return true;
|
|
1726
1866
|
}
|
|
1727
1867
|
async function consumeOutput(sessionCtx) {
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
sessionCtx.
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
if (
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1868
|
+
const toolCallProcessor = createToolCallProcessor((event) => sessionCtx.emitEvent(event));
|
|
1869
|
+
try {
|
|
1870
|
+
while (true) {
|
|
1871
|
+
if (!currentQuery) return;
|
|
1872
|
+
try {
|
|
1873
|
+
sessionCtx.setRuntimeState("working");
|
|
1874
|
+
for await (const message of currentQuery) {
|
|
1875
|
+
sessionCtx.touch();
|
|
1876
|
+
toolCallProcessor.onMessage(message);
|
|
1877
|
+
if (isResultMessage(message)) {
|
|
1878
|
+
if (message.subtype === "success") {
|
|
1879
|
+
retryCount = 0;
|
|
1880
|
+
if (message.result && sessionCtx.chatId) {
|
|
1881
|
+
const resultText = message.result;
|
|
1882
|
+
sessionCtx.sdk.sendMessage(sessionCtx.chatId, {
|
|
1883
|
+
format: "text",
|
|
1884
|
+
content: resultText
|
|
1885
|
+
}).then(() => {
|
|
1886
|
+
sessionCtx.log("Result forwarded to chat");
|
|
1887
|
+
sessionCtx.reportSessionCompletion();
|
|
1888
|
+
}).catch((err) => {
|
|
1889
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1890
|
+
sessionCtx.log(`Failed to forward result: ${reason}`);
|
|
1891
|
+
const message = `Result forward failed: ${reason}\n---\n${resultText.slice(0, 1500)}`.slice(0, 2e3);
|
|
1892
|
+
sessionCtx.emitEvent({
|
|
1893
|
+
kind: "error",
|
|
1894
|
+
payload: {
|
|
1895
|
+
source: "runtime",
|
|
1896
|
+
message
|
|
1897
|
+
}
|
|
1898
|
+
});
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
} else {
|
|
1902
|
+
const errors = message.errors ? message.errors.join("; ") : message.subtype;
|
|
1903
|
+
const errorLog = `Query result error: ${errors} (subtype=${message.subtype}, turns=${message.num_turns ?? "?"}, duration=${message.duration_ms ?? "?"}ms)`;
|
|
1904
|
+
sessionCtx.log(errorLog);
|
|
1905
|
+
sessionCtx.emitEvent({
|
|
1906
|
+
kind: "error",
|
|
1907
|
+
payload: {
|
|
1908
|
+
source: "sdk",
|
|
1909
|
+
message: errors
|
|
1910
|
+
}
|
|
1911
|
+
});
|
|
1744
1912
|
}
|
|
1745
|
-
|
|
1746
|
-
const errorLog = `Query result error: ${result.errors ? result.errors.join("; ") : result.subtype} (subtype=${result.subtype}, turns=${result.num_turns ?? "?"}, duration=${result.duration_ms ?? "?"}ms)`;
|
|
1747
|
-
sessionCtx.log(errorLog);
|
|
1748
|
-
sessionCtx.appendOutput(`[ERROR] ${errorLog}`);
|
|
1913
|
+
sessionCtx.setRuntimeState("idle");
|
|
1749
1914
|
}
|
|
1750
|
-
sessionCtx.setRuntimeState("idle");
|
|
1751
1915
|
}
|
|
1752
|
-
|
|
1753
|
-
sessionCtx.setRuntimeState("idle");
|
|
1754
|
-
return;
|
|
1755
|
-
} catch (err) {
|
|
1756
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1757
|
-
sessionCtx.log(`Query error: ${errMsg}`);
|
|
1758
|
-
if (err instanceof Error) {
|
|
1759
|
-
if (err.cause) sessionCtx.log(` cause: ${err.cause instanceof Error ? err.cause.message : String(err.cause)}`);
|
|
1760
|
-
if ("exitCode" in err) sessionCtx.log(` exitCode: ${err.exitCode}`);
|
|
1761
|
-
if ("stderr" in err) sessionCtx.log(` stderr: ${err.stderr}`);
|
|
1762
|
-
if ("code" in err) sessionCtx.log(` code: ${err.code}`);
|
|
1763
|
-
if (err.stack) sessionCtx.log(` stack: ${err.stack.split("\n").slice(1, 4).join(" | ")}`);
|
|
1764
|
-
}
|
|
1765
|
-
if (retryCount >= MAX_RETRIES || !claudeSessionId) {
|
|
1766
|
-
sessionCtx.log("Exhausted retries, session will be suspended");
|
|
1767
|
-
sessionCtx.setRuntimeState("error");
|
|
1768
|
-
return;
|
|
1769
|
-
}
|
|
1770
|
-
retryCount++;
|
|
1771
|
-
sessionCtx.log(`Attempting auto-resume (retry ${retryCount}/${MAX_RETRIES})`);
|
|
1772
|
-
try {
|
|
1773
|
-
respawnQuery(claudeSessionId, sessionCtx);
|
|
1774
|
-
} catch (resumeErr) {
|
|
1775
|
-
sessionCtx.log(`Auto-resume failed: ${resumeErr instanceof Error ? resumeErr.message : String(resumeErr)}`);
|
|
1916
|
+
sessionCtx.setRuntimeState("idle");
|
|
1776
1917
|
return;
|
|
1918
|
+
} catch (err) {
|
|
1919
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1920
|
+
sessionCtx.log(`Query error: ${errMsg}`);
|
|
1921
|
+
if (err instanceof Error) {
|
|
1922
|
+
if (err.cause) sessionCtx.log(` cause: ${err.cause instanceof Error ? err.cause.message : String(err.cause)}`);
|
|
1923
|
+
if ("exitCode" in err) sessionCtx.log(` exitCode: ${err.exitCode}`);
|
|
1924
|
+
if ("stderr" in err) sessionCtx.log(` stderr: ${err.stderr}`);
|
|
1925
|
+
if ("code" in err) sessionCtx.log(` code: ${err.code}`);
|
|
1926
|
+
if (err.stack) sessionCtx.log(` stack: ${err.stack.split("\n").slice(1, 4).join(" | ")}`);
|
|
1927
|
+
}
|
|
1928
|
+
if (retryCount >= MAX_RETRIES || !claudeSessionId) {
|
|
1929
|
+
sessionCtx.log("Exhausted retries, session will be suspended");
|
|
1930
|
+
sessionCtx.setRuntimeState("error");
|
|
1931
|
+
return;
|
|
1932
|
+
}
|
|
1933
|
+
toolCallProcessor.flush();
|
|
1934
|
+
retryCount++;
|
|
1935
|
+
sessionCtx.log(`Attempting auto-resume (retry ${retryCount}/${MAX_RETRIES})`);
|
|
1936
|
+
try {
|
|
1937
|
+
respawnQuery(claudeSessionId, sessionCtx);
|
|
1938
|
+
} catch (resumeErr) {
|
|
1939
|
+
sessionCtx.log(`Auto-resume failed: ${resumeErr instanceof Error ? resumeErr.message : String(resumeErr)}`);
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1777
1942
|
}
|
|
1778
1943
|
}
|
|
1944
|
+
} finally {
|
|
1945
|
+
toolCallProcessor.flush();
|
|
1779
1946
|
}
|
|
1780
1947
|
}
|
|
1781
1948
|
const contextTreePath = config.contextTreePath ?? null;
|
|
@@ -2705,8 +2872,11 @@ var SessionManager = class {
|
|
|
2705
2872
|
setRuntimeState: (state) => {
|
|
2706
2873
|
this.setSessionRuntimeState(chatId, state);
|
|
2707
2874
|
},
|
|
2708
|
-
|
|
2709
|
-
this.config.
|
|
2875
|
+
emitEvent: (event) => {
|
|
2876
|
+
this.config.onSessionEvent?.(chatId, event);
|
|
2877
|
+
},
|
|
2878
|
+
reportSessionCompletion: () => {
|
|
2879
|
+
this.config.onSessionCompletion?.(chatId);
|
|
2710
2880
|
}
|
|
2711
2881
|
};
|
|
2712
2882
|
}
|
|
@@ -2850,7 +3020,8 @@ var AgentSlot = class {
|
|
|
2850
3020
|
agentConfigCache: this.agentConfigCache,
|
|
2851
3021
|
onStateChange: (chatId, state) => this.reportSessionState(chatId, state),
|
|
2852
3022
|
onRuntimeStateChange: (state) => this.reportRuntimeState(state),
|
|
2853
|
-
|
|
3023
|
+
onSessionEvent: (chatId, event) => this.reportSessionEvent(chatId, event),
|
|
3024
|
+
onSessionCompletion: (chatId) => this.reportSessionCompletion(chatId)
|
|
2854
3025
|
});
|
|
2855
3026
|
const onCommand = (cmd) => {
|
|
2856
3027
|
if (cmd.agentId === this.config.agentId && this.sessionManager) this.sessionManager.handleCommand(cmd.chatId, cmd.type).catch((err) => {
|
|
@@ -2884,8 +3055,11 @@ var AgentSlot = class {
|
|
|
2884
3055
|
reportRuntimeState(state) {
|
|
2885
3056
|
this.clientConnection.reportRuntimeState(this.config.agentId, state);
|
|
2886
3057
|
}
|
|
2887
|
-
|
|
2888
|
-
this.clientConnection.
|
|
3058
|
+
reportSessionEvent(chatId, event) {
|
|
3059
|
+
this.clientConnection.reportSessionEvent(this.config.agentId, chatId, event);
|
|
3060
|
+
}
|
|
3061
|
+
reportSessionCompletion(chatId) {
|
|
3062
|
+
this.clientConnection.reportSessionCompletion(this.config.agentId, chatId);
|
|
2889
3063
|
}
|
|
2890
3064
|
fullStateSync() {
|
|
2891
3065
|
if (!this.sessionManager) return;
|
|
@@ -3799,7 +3973,7 @@ async function onboardCreate(args) {
|
|
|
3799
3973
|
}
|
|
3800
3974
|
const runtimeAgent = args.type === "human" ? args.assistant : args.id;
|
|
3801
3975
|
if (args.feishuBotAppId && args.feishuBotAppSecret) {
|
|
3802
|
-
const { bindFeishuBot } = await import("./feishu-
|
|
3976
|
+
const { bindFeishuBot } = await import("./feishu-CJ08ntOD.mjs").then((n) => n.r);
|
|
3803
3977
|
const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
|
|
3804
3978
|
if (!targetAgentUuid) process.stderr.write(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
|
|
3805
3979
|
else {
|
|
@@ -3940,7 +4114,7 @@ function setNestedByDot(obj, dotPath, value) {
|
|
|
3940
4114
|
if (lastKey !== void 0) current[lastKey] = value;
|
|
3941
4115
|
}
|
|
3942
4116
|
//#endregion
|
|
3943
|
-
//#region ../server/dist/app-
|
|
4117
|
+
//#region ../server/dist/app-nJ9jSQtv.mjs
|
|
3944
4118
|
var __defProp = Object.defineProperty;
|
|
3945
4119
|
var __exportAll = (all, no_symbols) => {
|
|
3946
4120
|
let target = {};
|
|
@@ -5378,14 +5552,20 @@ async function unbindAgent(db, agentId) {
|
|
|
5378
5552
|
...runtimeFieldsReset(now)
|
|
5379
5553
|
}).where(eq(agentPresence.agentId, agentId));
|
|
5380
5554
|
}
|
|
5381
|
-
/** Set runtime state directly from client-reported value.
|
|
5382
|
-
|
|
5555
|
+
/** Set runtime state directly from client-reported value.
|
|
5556
|
+
*
|
|
5557
|
+
* When an org-scoped notifier is provided, emit a PG NOTIFY on the
|
|
5558
|
+
* `runtime_state_changes` channel so the pulse aggregator (and any future
|
|
5559
|
+
* admin-side consumers) can observe the transition. Fire-and-forget to match
|
|
5560
|
+
* notifier semantics elsewhere in this module. */
|
|
5561
|
+
async function setRuntimeState(db, agentId, runtimeState, options) {
|
|
5383
5562
|
const now = /* @__PURE__ */ new Date();
|
|
5384
5563
|
await db.update(agentPresence).set({
|
|
5385
5564
|
runtimeState,
|
|
5386
5565
|
runtimeUpdatedAt: now,
|
|
5387
5566
|
lastSeenAt: now
|
|
5388
5567
|
}).where(eq(agentPresence.agentId, agentId));
|
|
5568
|
+
if (options?.notifier && options.organizationId) options.notifier.notifyRuntimeStateChange(agentId, runtimeState, options.organizationId).catch(() => {});
|
|
5389
5569
|
}
|
|
5390
5570
|
/** Touch agent last_seen_at on heartbeat (per-agent liveness). */
|
|
5391
5571
|
async function touchAgent(db, agentId) {
|
|
@@ -5511,9 +5691,7 @@ async function getClient(db, clientId) {
|
|
|
5511
5691
|
return row ?? null;
|
|
5512
5692
|
}
|
|
5513
5693
|
async function listClients(db, userId) {
|
|
5514
|
-
const
|
|
5515
|
-
if (userId) conditions.push(eq(clients.userId, userId));
|
|
5516
|
-
const rows = await db.select().from(clients).where(conditions.length === 1 ? conditions[0] : and(...conditions));
|
|
5694
|
+
const rows = await db.select().from(clients).where(eq(clients.userId, userId));
|
|
5517
5695
|
const counts = await db.select({
|
|
5518
5696
|
clientId: agents.clientId,
|
|
5519
5697
|
count: sql`count(*)::int`
|
|
@@ -5778,13 +5956,16 @@ async function listMessages(db, chatId, limit, cursor) {
|
|
|
5778
5956
|
const INBOX_CHANNEL = "inbox_notifications";
|
|
5779
5957
|
const CONFIG_CHANNEL = "config_changes";
|
|
5780
5958
|
const SESSION_STATE_CHANNEL = "session_state_changes";
|
|
5959
|
+
const RUNTIME_STATE_CHANNEL = "runtime_state_changes";
|
|
5781
5960
|
function createNotifier(listenClient) {
|
|
5782
5961
|
const subscriptions = /* @__PURE__ */ new Map();
|
|
5783
5962
|
const configChangeHandlers = [];
|
|
5784
5963
|
const sessionStateChangeHandlers = [];
|
|
5964
|
+
const runtimeStateChangeHandlers = [];
|
|
5785
5965
|
let unlistenInboxFn = null;
|
|
5786
5966
|
let unlistenConfigFn = null;
|
|
5787
5967
|
let unlistenSessionStateFn = null;
|
|
5968
|
+
let unlistenRuntimeStateFn = null;
|
|
5788
5969
|
function handleNotification(payload) {
|
|
5789
5970
|
const sepIdx = payload.indexOf(":");
|
|
5790
5971
|
if (sepIdx === -1) return;
|
|
@@ -5825,9 +6006,14 @@ function createNotifier(listenClient) {
|
|
|
5825
6006
|
await listenClient`SELECT pg_notify(${CONFIG_CHANNEL}, ${configType})`;
|
|
5826
6007
|
} catch {}
|
|
5827
6008
|
},
|
|
5828
|
-
async notifySessionStateChange(agentId, chatId, state) {
|
|
6009
|
+
async notifySessionStateChange(agentId, chatId, state, organizationId) {
|
|
6010
|
+
try {
|
|
6011
|
+
await listenClient`SELECT pg_notify(${SESSION_STATE_CHANNEL}, ${`${agentId}:${chatId}:${state}:${organizationId}`})`;
|
|
6012
|
+
} catch {}
|
|
6013
|
+
},
|
|
6014
|
+
async notifyRuntimeStateChange(agentId, state, organizationId) {
|
|
5829
6015
|
try {
|
|
5830
|
-
await listenClient`SELECT pg_notify(${
|
|
6016
|
+
await listenClient`SELECT pg_notify(${RUNTIME_STATE_CHANNEL}, ${`${agentId}:${state}:${organizationId}`})`;
|
|
5831
6017
|
} catch {}
|
|
5832
6018
|
},
|
|
5833
6019
|
onConfigChange(handler) {
|
|
@@ -5836,6 +6022,9 @@ function createNotifier(listenClient) {
|
|
|
5836
6022
|
onSessionStateChange(handler) {
|
|
5837
6023
|
sessionStateChangeHandlers.push(handler);
|
|
5838
6024
|
},
|
|
6025
|
+
onRuntimeStateChange(handler) {
|
|
6026
|
+
runtimeStateChangeHandlers.push(handler);
|
|
6027
|
+
},
|
|
5839
6028
|
async start() {
|
|
5840
6029
|
unlistenInboxFn = (await listenClient.listen(INBOX_CHANNEL, (payload) => {
|
|
5841
6030
|
if (payload) handleNotification(payload);
|
|
@@ -5847,14 +6036,33 @@ function createNotifier(listenClient) {
|
|
|
5847
6036
|
if (payload) {
|
|
5848
6037
|
const firstSep = payload.indexOf(":");
|
|
5849
6038
|
const secondSep = payload.indexOf(":", firstSep + 1);
|
|
5850
|
-
|
|
6039
|
+
const thirdSep = payload.indexOf(":", secondSep + 1);
|
|
6040
|
+
if (firstSep > 0 && secondSep > firstSep && thirdSep > secondSep) {
|
|
5851
6041
|
const agentId = payload.slice(0, firstSep);
|
|
5852
6042
|
const chatId = payload.slice(firstSep + 1, secondSep);
|
|
5853
|
-
const state = payload.slice(secondSep + 1);
|
|
6043
|
+
const state = payload.slice(secondSep + 1, thirdSep);
|
|
6044
|
+
const organizationId = payload.slice(thirdSep + 1);
|
|
5854
6045
|
for (const handler of sessionStateChangeHandlers) handler({
|
|
5855
6046
|
agentId,
|
|
5856
6047
|
chatId,
|
|
5857
|
-
state
|
|
6048
|
+
state,
|
|
6049
|
+
organizationId
|
|
6050
|
+
});
|
|
6051
|
+
}
|
|
6052
|
+
}
|
|
6053
|
+
})).unlisten;
|
|
6054
|
+
unlistenRuntimeStateFn = (await listenClient.listen(RUNTIME_STATE_CHANNEL, (payload) => {
|
|
6055
|
+
if (payload) {
|
|
6056
|
+
const firstSep = payload.indexOf(":");
|
|
6057
|
+
const secondSep = payload.indexOf(":", firstSep + 1);
|
|
6058
|
+
if (firstSep > 0 && secondSep > firstSep) {
|
|
6059
|
+
const agentId = payload.slice(0, firstSep);
|
|
6060
|
+
const state = payload.slice(firstSep + 1, secondSep);
|
|
6061
|
+
const organizationId = payload.slice(secondSep + 1);
|
|
6062
|
+
for (const handler of runtimeStateChangeHandlers) handler({
|
|
6063
|
+
agentId,
|
|
6064
|
+
state,
|
|
6065
|
+
organizationId
|
|
5858
6066
|
});
|
|
5859
6067
|
}
|
|
5860
6068
|
}
|
|
@@ -5873,6 +6081,10 @@ function createNotifier(listenClient) {
|
|
|
5873
6081
|
await unlistenSessionStateFn();
|
|
5874
6082
|
unlistenSessionStateFn = null;
|
|
5875
6083
|
}
|
|
6084
|
+
if (unlistenRuntimeStateFn) {
|
|
6085
|
+
await unlistenRuntimeStateFn();
|
|
6086
|
+
unlistenRuntimeStateFn = null;
|
|
6087
|
+
}
|
|
5876
6088
|
}
|
|
5877
6089
|
};
|
|
5878
6090
|
}
|
|
@@ -6304,8 +6516,8 @@ const agentChatSessions = pgTable("agent_chat_sessions", {
|
|
|
6304
6516
|
state: text("state").notNull(),
|
|
6305
6517
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
6306
6518
|
}, (table) => [primaryKey({ columns: [table.agentId, table.chatId] })]);
|
|
6307
|
-
/** Upsert a session state
|
|
6308
|
-
async function upsertSessionState(db, agentId, chatId, state, notifier) {
|
|
6519
|
+
/** Upsert a session state, refresh materialized aggregates on agent_presence, and emit org-scoped NOTIFY. */
|
|
6520
|
+
async function upsertSessionState(db, agentId, chatId, state, organizationId, notifier) {
|
|
6309
6521
|
const now = /* @__PURE__ */ new Date();
|
|
6310
6522
|
await db.transaction(async (tx) => {
|
|
6311
6523
|
await tx.insert(agentChatSessions).values({
|
|
@@ -6332,7 +6544,7 @@ async function upsertSessionState(db, agentId, chatId, state, notifier) {
|
|
|
6332
6544
|
lastSeenAt: now
|
|
6333
6545
|
}).where(eq(agentPresence.agentId, agentId));
|
|
6334
6546
|
});
|
|
6335
|
-
if (notifier) notifier.notifySessionStateChange(agentId, chatId, state).catch(() => {});
|
|
6547
|
+
if (notifier) notifier.notifySessionStateChange(agentId, chatId, state, organizationId).catch(() => {});
|
|
6336
6548
|
}
|
|
6337
6549
|
async function resetActivity(db, agentId) {
|
|
6338
6550
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -6381,7 +6593,8 @@ async function listAgentsWithRuntime(db, scope) {
|
|
|
6381
6593
|
runtimeState: agentPresence.runtimeState,
|
|
6382
6594
|
activeSessions: agentPresence.activeSessions,
|
|
6383
6595
|
totalSessions: agentPresence.totalSessions,
|
|
6384
|
-
runtimeUpdatedAt: agentPresence.runtimeUpdatedAt
|
|
6596
|
+
runtimeUpdatedAt: agentPresence.runtimeUpdatedAt,
|
|
6597
|
+
type: agents.type
|
|
6385
6598
|
}).from(agentPresence).innerJoin(agents, eq(agentPresence.agentId, agents.uuid)).where(and(isNotNull(agentPresence.runtimeState), agentVisibilityCondition(scope)));
|
|
6386
6599
|
}
|
|
6387
6600
|
/**
|
|
@@ -6470,7 +6683,8 @@ async function adminActivityRoutes(app) {
|
|
|
6470
6683
|
runtimeState: a.runtimeState,
|
|
6471
6684
|
activeSessions: a.activeSessions,
|
|
6472
6685
|
totalSessions: a.totalSessions,
|
|
6473
|
-
runtimeUpdatedAt: a.runtimeUpdatedAt?.toISOString() ?? null
|
|
6686
|
+
runtimeUpdatedAt: a.runtimeUpdatedAt?.toISOString() ?? null,
|
|
6687
|
+
type: "type" in a ? a.type : null
|
|
6474
6688
|
}))
|
|
6475
6689
|
};
|
|
6476
6690
|
});
|
|
@@ -6499,6 +6713,16 @@ const notifications = pgTable("notifications", {
|
|
|
6499
6713
|
index("idx_notifications_agent").on(table.agentId),
|
|
6500
6714
|
index("idx_notifications_org_read").on(table.organizationId, table.read)
|
|
6501
6715
|
]);
|
|
6716
|
+
let registered = null;
|
|
6717
|
+
function registerAdminBroadcaster(fn) {
|
|
6718
|
+
registered = fn;
|
|
6719
|
+
}
|
|
6720
|
+
function broadcastToAdmins(payload) {
|
|
6721
|
+
if (!registered) return;
|
|
6722
|
+
try {
|
|
6723
|
+
registered(payload);
|
|
6724
|
+
} catch {}
|
|
6725
|
+
}
|
|
6502
6726
|
/** Runtime system configuration (key-value JSONB). Dynamically modifiable via Admin API; controls inbox timeout, retry count, etc. */
|
|
6503
6727
|
const systemConfigs = pgTable("system_configs", {
|
|
6504
6728
|
key: text("key").primaryKey(),
|
|
@@ -6531,15 +6755,6 @@ async function updateConfigs(db, updates) {
|
|
|
6531
6755
|
});
|
|
6532
6756
|
return getAllConfigs(db);
|
|
6533
6757
|
}
|
|
6534
|
-
/**
|
|
6535
|
-
* Push channels: Admin WS, Webhook, Feishu.
|
|
6536
|
-
* Set at app startup via `setAdminWsBroadcast` and reloaded from system_configs.
|
|
6537
|
-
*/
|
|
6538
|
-
let adminWsBroadcast = null;
|
|
6539
|
-
/** Register the admin WS broadcast function (called once at app startup). */
|
|
6540
|
-
function setAdminWsBroadcast(fn) {
|
|
6541
|
-
adminWsBroadcast = fn;
|
|
6542
|
-
}
|
|
6543
6758
|
/** Create a notification, persist it, and fire-and-forget push to all channels. */
|
|
6544
6759
|
async function createNotification(db, data) {
|
|
6545
6760
|
const id = uuidv7();
|
|
@@ -6623,13 +6838,11 @@ async function notifyAgentEvent(db, agentId, type, severity, message, chatId) {
|
|
|
6623
6838
|
} catch {}
|
|
6624
6839
|
}
|
|
6625
6840
|
function pushToAdminWs(notification) {
|
|
6626
|
-
|
|
6627
|
-
|
|
6628
|
-
|
|
6629
|
-
|
|
6630
|
-
|
|
6631
|
-
});
|
|
6632
|
-
} catch {}
|
|
6841
|
+
broadcastToAdmins({
|
|
6842
|
+
type: "notification",
|
|
6843
|
+
organizationId: notification.organizationId,
|
|
6844
|
+
data: notification
|
|
6845
|
+
});
|
|
6633
6846
|
}
|
|
6634
6847
|
async function pushToWebhook(db, notification) {
|
|
6635
6848
|
const webhookUrl = await getConfig(db, "notification_webhook_url");
|
|
@@ -6831,46 +7044,97 @@ async function filterSessionsByParticipant(db, sessions, participantAgentId) {
|
|
|
6831
7044
|
return sessions.filter((s) => allowedChatIds.has(s.chatId));
|
|
6832
7045
|
}
|
|
6833
7046
|
/**
|
|
6834
|
-
* Session
|
|
6835
|
-
*
|
|
6836
|
-
*
|
|
7047
|
+
* Session events — structured event stream per (agent, chat) session.
|
|
7048
|
+
* `kind` is 'tool_call' | 'error'; the payload shape is enforced by the
|
|
7049
|
+
* service layer via Zod (no FK / CHECK on this table per project rule).
|
|
7050
|
+
*
|
|
7051
|
+
* `seq` is monotonic per (agent_id, chat_id). The single-writer invariant
|
|
7052
|
+
* in the client-side session-manager guarantees ordering; the service wraps
|
|
7053
|
+
* the insert in a MAX(seq)+1 retry loop to recover from restart-overlap
|
|
7054
|
+
* windows.
|
|
7055
|
+
*
|
|
7056
|
+
* Cleanup: rows are dropped when the session is evicted or terminated —
|
|
7057
|
+
* see sessionEventService.clearEvents.
|
|
6837
7058
|
*/
|
|
6838
|
-
const
|
|
7059
|
+
const sessionEvents = pgTable("session_events", {
|
|
6839
7060
|
id: text("id").primaryKey(),
|
|
6840
7061
|
agentId: text("agent_id").notNull(),
|
|
6841
7062
|
chatId: text("chat_id").notNull(),
|
|
6842
|
-
|
|
6843
|
-
|
|
6844
|
-
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
|
|
6850
|
-
|
|
6851
|
-
|
|
6852
|
-
|
|
6853
|
-
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
|
|
6858
|
-
|
|
6859
|
-
|
|
6860
|
-
});
|
|
7063
|
+
seq: integer("seq").notNull(),
|
|
7064
|
+
kind: text("kind").notNull(),
|
|
7065
|
+
payload: jsonb("payload").notNull(),
|
|
7066
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
7067
|
+
}, (table) => [uniqueIndex("uq_session_events_chat_seq").on(table.agentId, table.chatId, table.seq), index("idx_session_events_chat_created").on(table.agentId, table.chatId, table.createdAt.desc())]);
|
|
7068
|
+
const DEFAULT_LIMIT = 200;
|
|
7069
|
+
const MAX_LIMIT = 1e3;
|
|
7070
|
+
const MAX_SEQ_RETRIES = 3;
|
|
7071
|
+
function rowToEvent(row) {
|
|
7072
|
+
return {
|
|
7073
|
+
id: row.id,
|
|
7074
|
+
agentId: row.agentId,
|
|
7075
|
+
chatId: row.chatId,
|
|
7076
|
+
seq: row.seq,
|
|
7077
|
+
kind: row.kind,
|
|
7078
|
+
payload: row.payload,
|
|
7079
|
+
createdAt: row.createdAt.toISOString()
|
|
7080
|
+
};
|
|
6861
7081
|
}
|
|
6862
|
-
/**
|
|
6863
|
-
async function
|
|
6864
|
-
const
|
|
6865
|
-
|
|
6866
|
-
|
|
6867
|
-
|
|
6868
|
-
|
|
7082
|
+
/** Append one event; throws after MAX_SEQ_RETRIES on persistent seq contention. */
|
|
7083
|
+
async function appendEvent(db, agentId, chatId, event) {
|
|
7084
|
+
const validated = sessionEventSchema$1.parse(event);
|
|
7085
|
+
for (let attempt = 0; attempt < MAX_SEQ_RETRIES; attempt++) {
|
|
7086
|
+
const id = uuidv7();
|
|
7087
|
+
const payloadJson = JSON.stringify(validated.payload);
|
|
7088
|
+
const row = (await db.execute(sql`
|
|
7089
|
+
INSERT INTO session_events (id, agent_id, chat_id, seq, kind, payload)
|
|
7090
|
+
SELECT ${id}, ${agentId}, ${chatId},
|
|
7091
|
+
COALESCE(MAX(seq), 0) + 1, ${validated.kind}, ${payloadJson}::jsonb
|
|
7092
|
+
FROM session_events
|
|
7093
|
+
WHERE agent_id = ${agentId} AND chat_id = ${chatId}
|
|
7094
|
+
ON CONFLICT (agent_id, chat_id, seq) DO NOTHING
|
|
7095
|
+
RETURNING id, agent_id, chat_id, seq, kind, payload, created_at
|
|
7096
|
+
`))[0];
|
|
7097
|
+
if (row) return rowToEvent({
|
|
7098
|
+
id: row.id,
|
|
7099
|
+
agentId: row.agent_id,
|
|
7100
|
+
chatId: row.chat_id,
|
|
7101
|
+
seq: row.seq,
|
|
7102
|
+
kind: row.kind,
|
|
7103
|
+
payload: row.payload,
|
|
7104
|
+
createdAt: row.created_at instanceof Date ? row.created_at : new Date(row.created_at)
|
|
7105
|
+
});
|
|
7106
|
+
}
|
|
7107
|
+
throw new Error(`session_events seq contention on ${agentId}/${chatId}`);
|
|
7108
|
+
}
|
|
7109
|
+
/**
|
|
7110
|
+
* List events for a session in `seq asc` order with cursor pagination.
|
|
7111
|
+
* `cursor` is the last seen `seq`; pass it as-is on the next page.
|
|
7112
|
+
*/
|
|
7113
|
+
async function listEvents(db, agentId, chatId, options) {
|
|
7114
|
+
const limit = Math.min(Math.max(options?.limit ?? DEFAULT_LIMIT, 1), MAX_LIMIT);
|
|
7115
|
+
const conditions = [eq(sessionEvents.agentId, agentId), eq(sessionEvents.chatId, chatId)];
|
|
7116
|
+
if (options?.cursor !== void 0) conditions.push(gt(sessionEvents.seq, options.cursor));
|
|
7117
|
+
const rows = await db.select({
|
|
7118
|
+
id: sessionEvents.id,
|
|
7119
|
+
agentId: sessionEvents.agentId,
|
|
7120
|
+
chatId: sessionEvents.chatId,
|
|
7121
|
+
seq: sessionEvents.seq,
|
|
7122
|
+
kind: sessionEvents.kind,
|
|
7123
|
+
payload: sessionEvents.payload,
|
|
7124
|
+
createdAt: sessionEvents.createdAt
|
|
7125
|
+
}).from(sessionEvents).where(and(...conditions)).orderBy(asc(sessionEvents.seq)).limit(limit + 1);
|
|
7126
|
+
const hasMore = rows.length > limit;
|
|
7127
|
+
const items = (hasMore ? rows.slice(0, limit) : rows).map(rowToEvent);
|
|
7128
|
+
const last = items[items.length - 1];
|
|
6869
7129
|
return {
|
|
6870
|
-
|
|
6871
|
-
|
|
7130
|
+
items,
|
|
7131
|
+
nextCursor: hasMore && last ? last.seq : null
|
|
6872
7132
|
};
|
|
6873
7133
|
}
|
|
7134
|
+
/** Delete all events for a session — called on eviction / termination. */
|
|
7135
|
+
async function clearEvents(db, agentId, chatId) {
|
|
7136
|
+
await db.delete(sessionEvents).where(and(eq(sessionEvents.agentId, agentId), eq(sessionEvents.chatId, chatId)));
|
|
7137
|
+
}
|
|
6874
7138
|
const sessionFilterSchema = z.object({
|
|
6875
7139
|
state: z.enum([
|
|
6876
7140
|
"active",
|
|
@@ -6916,16 +7180,22 @@ async function adminSessionRoutes(app) {
|
|
|
6916
7180
|
});
|
|
6917
7181
|
/** GET /admin/sessions/agents/:agentId/:chatId — single session detail */
|
|
6918
7182
|
app.get("/agents/:agentId/:chatId", async (request) => {
|
|
6919
|
-
|
|
7183
|
+
const scope = memberScope(request);
|
|
7184
|
+
await assertAgentVisible(app.db, scope, request.params.agentId);
|
|
7185
|
+
await assertChatAccess(app.db, scope, request.params.chatId);
|
|
6920
7186
|
return getSession(app.db, request.params.agentId, request.params.chatId);
|
|
6921
7187
|
});
|
|
6922
|
-
/** GET /admin/sessions/agents/:agentId/:chatId/
|
|
6923
|
-
app.get("/agents/:agentId/:chatId/
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
|
|
7188
|
+
/** GET /admin/sessions/agents/:agentId/:chatId/events — session event stream, paged by `seq` */
|
|
7189
|
+
app.get("/agents/:agentId/:chatId/events", async (request) => {
|
|
7190
|
+
const scope = memberScope(request);
|
|
7191
|
+
await assertAgentVisible(app.db, scope, request.params.agentId);
|
|
7192
|
+
await assertChatAccess(app.db, scope, request.params.chatId);
|
|
7193
|
+
const limit = request.query.limit !== void 0 ? Number.parseInt(request.query.limit, 10) : void 0;
|
|
7194
|
+
const cursor = request.query.cursor !== void 0 ? Number.parseInt(request.query.cursor, 10) : void 0;
|
|
7195
|
+
return listEvents(app.db, request.params.agentId, request.params.chatId, {
|
|
7196
|
+
limit: Number.isFinite(limit) ? limit : void 0,
|
|
7197
|
+
cursor: Number.isFinite(cursor) ? cursor : void 0
|
|
7198
|
+
});
|
|
6929
7199
|
});
|
|
6930
7200
|
/** POST /admin/sessions/agents/:agentId/:chatId/suspend — suspend a session */
|
|
6931
7201
|
app.post("/agents/:agentId/:chatId/suspend", async (request, reply) => {
|
|
@@ -7532,30 +7802,40 @@ async function adminTaskRoutes(app) {
|
|
|
7532
7802
|
return getTaskHealth(app.db, request.params.taskId);
|
|
7533
7803
|
});
|
|
7534
7804
|
}
|
|
7535
|
-
|
|
7536
|
-
|
|
7537
|
-
|
|
7538
|
-
|
|
7539
|
-
|
|
7540
|
-
|
|
7541
|
-
|
|
7542
|
-
|
|
7543
|
-
|
|
7805
|
+
async function loadVisibleAgentIds(db, organizationId, memberId) {
|
|
7806
|
+
const rows = await db.select({ id: agents.uuid }).from(agents).where(and(eq(agents.organizationId, organizationId), ne(agents.status, AGENT_STATUSES.DELETED), or(eq(agents.visibility, AGENT_VISIBILITY.ORGANIZATION), eq(agents.managerId, memberId))));
|
|
7807
|
+
return new Set(rows.map((r) => r.id));
|
|
7808
|
+
}
|
|
7809
|
+
function filterPulseAgents(agentsMap, visible) {
|
|
7810
|
+
const out = {};
|
|
7811
|
+
for (const [agentId, buckets] of Object.entries(agentsMap)) if (visible.has(agentId)) out[agentId] = buckets;
|
|
7812
|
+
return out;
|
|
7813
|
+
}
|
|
7544
7814
|
function adminWsRoutes(notifier, jwtSecret) {
|
|
7545
7815
|
const adminSockets = /* @__PURE__ */ new Map();
|
|
7546
7816
|
const secret = new TextEncoder().encode(jwtSecret);
|
|
7547
|
-
|
|
7817
|
+
function broadcastOrgScoped(payload) {
|
|
7548
7818
|
const orgId = payload.organizationId;
|
|
7549
|
-
|
|
7550
|
-
|
|
7551
|
-
|
|
7819
|
+
if (typeof orgId !== "string" || orgId.length === 0) return;
|
|
7820
|
+
const isPulseTick = payload.type === "pulse:tick" && typeof payload.agents === "object" && payload.agents !== null;
|
|
7821
|
+
const sharedData = isPulseTick ? null : JSON.stringify(payload);
|
|
7822
|
+
for (const [ws, meta] of adminSockets) {
|
|
7823
|
+
if (ws.readyState !== 1 || meta.organizationId !== orgId) continue;
|
|
7824
|
+
if (isPulseTick) {
|
|
7825
|
+
const filtered = filterPulseAgents(payload.agents, meta.visibleAgentIds);
|
|
7826
|
+
ws.send(JSON.stringify({
|
|
7827
|
+
...payload,
|
|
7828
|
+
agents: filtered
|
|
7829
|
+
}));
|
|
7830
|
+
} else ws.send(sharedData);
|
|
7831
|
+
}
|
|
7832
|
+
}
|
|
7833
|
+
registerAdminBroadcaster(broadcastOrgScoped);
|
|
7552
7834
|
notifier.onSessionStateChange((payload) => {
|
|
7553
|
-
|
|
7835
|
+
broadcastOrgScoped({
|
|
7554
7836
|
type: "session:state",
|
|
7555
7837
|
...payload
|
|
7556
7838
|
});
|
|
7557
|
-
const orgId = payload.organizationId;
|
|
7558
|
-
for (const [ws, meta] of adminSockets) if (ws.readyState === 1 && (!orgId || meta.organizationId === orgId)) ws.send(data);
|
|
7559
7839
|
});
|
|
7560
7840
|
return async (app) => {
|
|
7561
7841
|
app.get("/admin", { websocket: true }, async (socket, request) => {
|
|
@@ -7569,9 +7849,10 @@ function adminWsRoutes(notifier, jwtSecret) {
|
|
|
7569
7849
|
return;
|
|
7570
7850
|
}
|
|
7571
7851
|
let organizationId;
|
|
7852
|
+
let memberId;
|
|
7572
7853
|
try {
|
|
7573
7854
|
const { payload } = await jwtVerify(token, secret);
|
|
7574
|
-
if (payload.type !== "access" || !payload.sub ||
|
|
7855
|
+
if (payload.type !== "access" || !payload.sub || typeof payload.organizationId !== "string" || typeof payload.memberId !== "string") {
|
|
7575
7856
|
socket.send(JSON.stringify({
|
|
7576
7857
|
type: "error",
|
|
7577
7858
|
message: "Invalid token type"
|
|
@@ -7580,6 +7861,7 @@ function adminWsRoutes(notifier, jwtSecret) {
|
|
|
7580
7861
|
return;
|
|
7581
7862
|
}
|
|
7582
7863
|
organizationId = payload.organizationId;
|
|
7864
|
+
memberId = payload.memberId;
|
|
7583
7865
|
} catch {
|
|
7584
7866
|
socket.send(JSON.stringify({
|
|
7585
7867
|
type: "error",
|
|
@@ -7588,7 +7870,12 @@ function adminWsRoutes(notifier, jwtSecret) {
|
|
|
7588
7870
|
socket.close(4001, "Auth failed");
|
|
7589
7871
|
return;
|
|
7590
7872
|
}
|
|
7591
|
-
|
|
7873
|
+
const visibleAgentIds = await loadVisibleAgentIds(app.db, organizationId, memberId);
|
|
7874
|
+
adminSockets.set(socket, {
|
|
7875
|
+
organizationId,
|
|
7876
|
+
memberId,
|
|
7877
|
+
visibleAgentIds
|
|
7878
|
+
});
|
|
7592
7879
|
socket.send(JSON.stringify({ type: "admin:connected" }));
|
|
7593
7880
|
socket.on("close", () => {
|
|
7594
7881
|
adminSockets.delete(socket);
|
|
@@ -8072,7 +8359,7 @@ const wsMessageSchema = z.object({
|
|
|
8072
8359
|
* Failure ⇒ server sends `auth:rejected` and closes (code 4401).
|
|
8073
8360
|
* 2. `client:register` — bind the client_id to the authenticated user.
|
|
8074
8361
|
* 3. `agent:bind` — run Rule R-RUN (no token); populate presence.
|
|
8075
|
-
* 4. `session:state` / `runtime:state` / `session:
|
|
8362
|
+
* 4. `session:state` / `runtime:state` / `session:event` / `session:completion` / `heartbeat`.
|
|
8076
8363
|
* 5. `agent:unbind` — stop multiplexing for a specific agent.
|
|
8077
8364
|
*
|
|
8078
8365
|
* When the JWT is about to expire the server sends `auth:expired` so the
|
|
@@ -8107,6 +8394,15 @@ function clientWsRoutes(notifier, instanceId) {
|
|
|
8107
8394
|
let clientId = null;
|
|
8108
8395
|
let authExpiryTimer = null;
|
|
8109
8396
|
const boundAgents = /* @__PURE__ */ new Map();
|
|
8397
|
+
const sessionOpQueues = /* @__PURE__ */ new Map();
|
|
8398
|
+
function chainSessionOp(agentId, chatId, op) {
|
|
8399
|
+
const key = `${agentId}:${chatId}`;
|
|
8400
|
+
const next = (sessionOpQueues.get(key) ?? Promise.resolve()).then(op, op);
|
|
8401
|
+
sessionOpQueues.set(key, next.finally(() => {
|
|
8402
|
+
if (sessionOpQueues.get(key) === next) sessionOpQueues.delete(key);
|
|
8403
|
+
}));
|
|
8404
|
+
return next;
|
|
8405
|
+
}
|
|
8110
8406
|
const authTimeout = setTimeout(() => {
|
|
8111
8407
|
if (!session) {
|
|
8112
8408
|
try {
|
|
@@ -8331,7 +8627,8 @@ function clientWsRoutes(notifier, instanceId) {
|
|
|
8331
8627
|
return;
|
|
8332
8628
|
}
|
|
8333
8629
|
const payload = sessionStateMessageSchema.parse(msg);
|
|
8334
|
-
|
|
8630
|
+
if (payload.state === "evicted") chainSessionOp(agentId, payload.chatId, () => clearEvents(app.db, agentId, payload.chatId).catch(() => {}));
|
|
8631
|
+
await upsertSessionState(app.db, agentId, payload.chatId, payload.state, session.organizationId, notifier);
|
|
8335
8632
|
} else if (type === "runtime:state") {
|
|
8336
8633
|
const agentId = parsed.data.agentId;
|
|
8337
8634
|
if (!agentId || !boundAgents.has(agentId)) {
|
|
@@ -8342,10 +8639,13 @@ function clientWsRoutes(notifier, instanceId) {
|
|
|
8342
8639
|
return;
|
|
8343
8640
|
}
|
|
8344
8641
|
const payload = runtimeStateMessageSchema.parse(msg);
|
|
8345
|
-
await setRuntimeState(app.db, agentId, payload.runtimeState
|
|
8642
|
+
await setRuntimeState(app.db, agentId, payload.runtimeState, {
|
|
8643
|
+
organizationId: session.organizationId,
|
|
8644
|
+
notifier
|
|
8645
|
+
});
|
|
8346
8646
|
if (payload.runtimeState === "error" && shouldNotify(agentId, "agent_error")) notifyAgentEvent(app.db, agentId, "agent_error", "high", `Agent ${agentId} entered error state`).catch(() => {});
|
|
8347
8647
|
else if (payload.runtimeState === "blocked" && shouldNotify(agentId, "agent_blocked")) notifyAgentEvent(app.db, agentId, "agent_blocked", "medium", `Agent ${agentId} is blocked`).catch(() => {});
|
|
8348
|
-
} else if (type === "session:
|
|
8648
|
+
} else if (type === "session:event") {
|
|
8349
8649
|
const agentId = parsed.data.agentId;
|
|
8350
8650
|
if (!agentId || !boundAgents.has(agentId)) {
|
|
8351
8651
|
socket.send(JSON.stringify({
|
|
@@ -8354,8 +8654,27 @@ function clientWsRoutes(notifier, instanceId) {
|
|
|
8354
8654
|
}));
|
|
8355
8655
|
return;
|
|
8356
8656
|
}
|
|
8357
|
-
const payload =
|
|
8358
|
-
|
|
8657
|
+
const payload = sessionEventMessageSchema.parse(msg);
|
|
8658
|
+
chainSessionOp(agentId, payload.chatId, async () => {
|
|
8659
|
+
try {
|
|
8660
|
+
await appendEvent(app.db, agentId, payload.chatId, payload.event);
|
|
8661
|
+
} catch (err) {
|
|
8662
|
+
socket.send(JSON.stringify({
|
|
8663
|
+
type: "error",
|
|
8664
|
+
message: `Failed to persist session event: ${err instanceof Error ? err.message : String(err)}`
|
|
8665
|
+
}));
|
|
8666
|
+
}
|
|
8667
|
+
});
|
|
8668
|
+
} else if (type === "session:completion") {
|
|
8669
|
+
const agentId = parsed.data.agentId;
|
|
8670
|
+
if (!agentId || !boundAgents.has(agentId)) {
|
|
8671
|
+
socket.send(JSON.stringify({
|
|
8672
|
+
type: "error",
|
|
8673
|
+
message: "Agent not bound"
|
|
8674
|
+
}));
|
|
8675
|
+
return;
|
|
8676
|
+
}
|
|
8677
|
+
const payload = sessionCompletionMessageSchema.parse(msg);
|
|
8359
8678
|
if (shouldNotify(agentId, `session_completed:${payload.chatId}`)) notifyAgentEvent(app.db, agentId, "session_completed", "low", `Agent ${agentId} completed a task`, payload.chatId).catch(() => {});
|
|
8360
8679
|
} else if (type === "heartbeat") {
|
|
8361
8680
|
if (clientId) {
|
|
@@ -9267,7 +9586,7 @@ var schema_exports = /* @__PURE__ */ __exportAll({
|
|
|
9267
9586
|
notifications: () => notifications,
|
|
9268
9587
|
organizations: () => organizations,
|
|
9269
9588
|
serverInstances: () => serverInstances,
|
|
9270
|
-
|
|
9589
|
+
sessionEvents: () => sessionEvents,
|
|
9271
9590
|
systemConfigs: () => systemConfigs,
|
|
9272
9591
|
taskChats: () => taskChats,
|
|
9273
9592
|
tasks: () => tasks,
|
|
@@ -10355,6 +10674,90 @@ async function nackEntry(db, entryId) {
|
|
|
10355
10674
|
WHERE id = ${entryId}
|
|
10356
10675
|
`);
|
|
10357
10676
|
}
|
|
10677
|
+
const DEFAULT_INTERVAL_MS = 5e3;
|
|
10678
|
+
const DEFAULT_BUCKET_COUNT = 32;
|
|
10679
|
+
function createPulseAggregator(options) {
|
|
10680
|
+
const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
10681
|
+
const bucketCount = options.bucketCount ?? DEFAULT_BUCKET_COUNT;
|
|
10682
|
+
const state = /* @__PURE__ */ new Map();
|
|
10683
|
+
let currentIdx = 0;
|
|
10684
|
+
let running = false;
|
|
10685
|
+
let intervalHandle = null;
|
|
10686
|
+
function emptyBucket() {
|
|
10687
|
+
return {
|
|
10688
|
+
workingCount: 0,
|
|
10689
|
+
errorMask: false
|
|
10690
|
+
};
|
|
10691
|
+
}
|
|
10692
|
+
function initBuckets() {
|
|
10693
|
+
return Array.from({ length: bucketCount }, emptyBucket);
|
|
10694
|
+
}
|
|
10695
|
+
function ensureAgent(organizationId, agentId) {
|
|
10696
|
+
let orgMap = state.get(organizationId);
|
|
10697
|
+
if (!orgMap) {
|
|
10698
|
+
orgMap = /* @__PURE__ */ new Map();
|
|
10699
|
+
state.set(organizationId, orgMap);
|
|
10700
|
+
}
|
|
10701
|
+
let buckets = orgMap.get(agentId);
|
|
10702
|
+
if (!buckets) {
|
|
10703
|
+
buckets = initBuckets();
|
|
10704
|
+
orgMap.set(agentId, buckets);
|
|
10705
|
+
}
|
|
10706
|
+
return buckets;
|
|
10707
|
+
}
|
|
10708
|
+
const ingest = (payload) => {
|
|
10709
|
+
if (!running) return;
|
|
10710
|
+
if (!payload.organizationId || !payload.agentId) return;
|
|
10711
|
+
const bucket = ensureAgent(payload.organizationId, payload.agentId)[currentIdx];
|
|
10712
|
+
if (!bucket) return;
|
|
10713
|
+
if (payload.state === "working") bucket.workingCount += 1;
|
|
10714
|
+
else if (payload.state === "error") bucket.errorMask = true;
|
|
10715
|
+
};
|
|
10716
|
+
function snapshotBuckets(buckets) {
|
|
10717
|
+
const out = [];
|
|
10718
|
+
for (let i = 0; i < bucketCount; i++) {
|
|
10719
|
+
const src = buckets[(currentIdx + 1 + i) % bucketCount] ?? emptyBucket();
|
|
10720
|
+
out.push({
|
|
10721
|
+
workingCount: src.workingCount,
|
|
10722
|
+
errorMask: src.errorMask
|
|
10723
|
+
});
|
|
10724
|
+
}
|
|
10725
|
+
return out;
|
|
10726
|
+
}
|
|
10727
|
+
function broadcastTick() {
|
|
10728
|
+
for (const [organizationId, orgMap] of state) {
|
|
10729
|
+
const agents = {};
|
|
10730
|
+
for (const [agentId, buckets] of orgMap) agents[agentId] = snapshotBuckets(buckets);
|
|
10731
|
+
options.broadcast({
|
|
10732
|
+
type: "pulse:tick",
|
|
10733
|
+
organizationId,
|
|
10734
|
+
agents
|
|
10735
|
+
});
|
|
10736
|
+
}
|
|
10737
|
+
}
|
|
10738
|
+
function advance() {
|
|
10739
|
+
broadcastTick();
|
|
10740
|
+
currentIdx = (currentIdx + 1) % bucketCount;
|
|
10741
|
+
for (const orgMap of state.values()) for (const buckets of orgMap.values()) buckets[currentIdx] = emptyBucket();
|
|
10742
|
+
}
|
|
10743
|
+
return {
|
|
10744
|
+
start() {
|
|
10745
|
+
if (running) return;
|
|
10746
|
+
running = true;
|
|
10747
|
+
options.notifier.onRuntimeStateChange(ingest);
|
|
10748
|
+
intervalHandle = setInterval(advance, intervalMs);
|
|
10749
|
+
},
|
|
10750
|
+
stop() {
|
|
10751
|
+
if (!running) return;
|
|
10752
|
+
running = false;
|
|
10753
|
+
if (intervalHandle) {
|
|
10754
|
+
clearInterval(intervalHandle);
|
|
10755
|
+
intervalHandle = null;
|
|
10756
|
+
}
|
|
10757
|
+
},
|
|
10758
|
+
ingest
|
|
10759
|
+
};
|
|
10760
|
+
}
|
|
10358
10761
|
async function buildApp(config) {
|
|
10359
10762
|
const app = Fastify({ logger: config.logger ?? true });
|
|
10360
10763
|
const db = connectDatabase(config.database.url);
|
|
@@ -10444,7 +10847,7 @@ async function buildApp(config) {
|
|
|
10444
10847
|
await api.register(async (adminApp) => {
|
|
10445
10848
|
adminApp.addHook("onRequest", memberAuth);
|
|
10446
10849
|
await adminApp.register(adminClientRoutes);
|
|
10447
|
-
}, { prefix: "/
|
|
10850
|
+
}, { prefix: "/clients" });
|
|
10448
10851
|
await api.register(async (adminApp) => {
|
|
10449
10852
|
adminApp.addHook("onRequest", memberAuth);
|
|
10450
10853
|
await adminApp.register(adminActivityRoutes);
|
|
@@ -10513,6 +10916,10 @@ async function buildApp(config) {
|
|
|
10513
10916
|
const contextTreeDir = join(DEFAULT_DATA_DIR$1, "context-tree");
|
|
10514
10917
|
const kaelRuntime = config.kael?.endpoint ? createKaelRuntime(db, config.secrets.encryptionKey, config.kael.endpoint, config.kael.apiKey, config.kael.hubPublicUrl, app.log, contextTreeDir) : void 0;
|
|
10515
10918
|
const backgroundTasks = createBackgroundTasks(app, config.instanceId, adapterManager, kaelRuntime);
|
|
10919
|
+
const pulseAggregator = createPulseAggregator({
|
|
10920
|
+
notifier,
|
|
10921
|
+
broadcast: broadcastToAdmins
|
|
10922
|
+
});
|
|
10516
10923
|
notifier.onConfigChange((configType) => {
|
|
10517
10924
|
if (configType === "adapter_configs") {
|
|
10518
10925
|
adapterManager.reload().catch((err) => app.log.error(err, "Adapter hot-reload failed (PG NOTIFY)"));
|
|
@@ -10523,10 +10930,12 @@ async function buildApp(config) {
|
|
|
10523
10930
|
await ensureDefaultOrganization(db);
|
|
10524
10931
|
await notifier.start();
|
|
10525
10932
|
backgroundTasks.start();
|
|
10933
|
+
pulseAggregator.start();
|
|
10526
10934
|
await adapterManager.reload();
|
|
10527
10935
|
await kaelRuntime?.reload();
|
|
10528
10936
|
});
|
|
10529
10937
|
app.addHook("onClose", async () => {
|
|
10938
|
+
pulseAggregator.stop();
|
|
10530
10939
|
backgroundTasks.stop();
|
|
10531
10940
|
adapterManager.shutdown();
|
|
10532
10941
|
kaelRuntime?.shutdown();
|