@agent-team-foundation/first-tree-hub 0.6.2 → 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-DW7aIpmE.mjs → bootstrap-CRDR6NwE.mjs} +11 -6
- package/dist/cli/index.mjs +128 -109
- package/dist/{core-RXUUKkCO.mjs → core-4nvleGlC.mjs} +625 -180
- package/dist/drizzle/0020_unified_user_token.sql +50 -44
- package/dist/drizzle/0022_session_events.sql +32 -0
- package/dist/drizzle/meta/_journal.json +7 -0
- package/dist/{feishu-BZ8pnMrQ.mjs → feishu-CJ08ntOD.mjs} +55 -7
- 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-BMOr9-X2.js +0 -308
- package/dist/web/assets/index-CTl4pHIL.css +0 -1
|
@@ -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";
|
|
@@ -11,7 +11,7 @@ import WebSocket from "ws";
|
|
|
11
11
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
12
12
|
import { execFileSync, execSync, spawn } from "node:child_process";
|
|
13
13
|
import bcrypt from "bcrypt";
|
|
14
|
-
import { and, asc, count, desc, eq, gt, inArray, isNotNull, lt, ne, or, sql } from "drizzle-orm";
|
|
14
|
+
import { and, asc, count, desc, eq, gt, inArray, isNotNull, isNull, lt, ne, or, sql } from "drizzle-orm";
|
|
15
15
|
import { drizzle } from "drizzle-orm/postgres-js";
|
|
16
16
|
import postgres from "postgres";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
@@ -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
|
|
@@ -172,7 +172,7 @@ z.object({
|
|
|
172
172
|
visibility: agentVisibilitySchema.optional(),
|
|
173
173
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
174
174
|
managerId: z.string().nullable().optional(),
|
|
175
|
-
clientId: z.string().optional()
|
|
175
|
+
clientId: z.string().min(1).max(100).nullable().optional()
|
|
176
176
|
});
|
|
177
177
|
z.object({
|
|
178
178
|
uuid: z.string(),
|
|
@@ -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() {
|
|
@@ -1277,6 +1328,10 @@ defineConfig({
|
|
|
1277
1328
|
default: "http://localhost:8000"
|
|
1278
1329
|
}
|
|
1279
1330
|
}) },
|
|
1331
|
+
client: { id: field(z.string().regex(/^client_[a-f0-9]{8}$/), {
|
|
1332
|
+
auto: "client-id",
|
|
1333
|
+
env: "FIRST_TREE_HUB_CLIENT_ID"
|
|
1334
|
+
}) },
|
|
1280
1335
|
logLevel: field(z.enum([
|
|
1281
1336
|
"debug",
|
|
1282
1337
|
"info",
|
|
@@ -1534,6 +1589,95 @@ function cleanWorkspaces(workspaceRoot, activeChatIds, ttlMs = DEFAULT_WORKSPACE
|
|
|
1534
1589
|
return removed;
|
|
1535
1590
|
}
|
|
1536
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
|
+
}
|
|
1537
1681
|
/**
|
|
1538
1682
|
* Map a payload's MCP server list to the SDK's record type. Handles all three
|
|
1539
1683
|
* transports (stdio/http/sse) defined in the M1 schema.
|
|
@@ -1721,57 +1865,84 @@ const createClaudeCodeHandler = (config) => {
|
|
|
1721
1865
|
return true;
|
|
1722
1866
|
}
|
|
1723
1867
|
async function consumeOutput(sessionCtx) {
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
sessionCtx.
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
if (
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
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
|
+
});
|
|
1740
1912
|
}
|
|
1741
|
-
|
|
1742
|
-
const errorLog = `Query result error: ${result.errors ? result.errors.join("; ") : result.subtype} (subtype=${result.subtype}, turns=${result.num_turns ?? "?"}, duration=${result.duration_ms ?? "?"}ms)`;
|
|
1743
|
-
sessionCtx.log(errorLog);
|
|
1744
|
-
sessionCtx.appendOutput(`[ERROR] ${errorLog}`);
|
|
1913
|
+
sessionCtx.setRuntimeState("idle");
|
|
1745
1914
|
}
|
|
1746
|
-
sessionCtx.setRuntimeState("idle");
|
|
1747
1915
|
}
|
|
1748
|
-
|
|
1749
|
-
sessionCtx.setRuntimeState("idle");
|
|
1750
|
-
return;
|
|
1751
|
-
} catch (err) {
|
|
1752
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1753
|
-
sessionCtx.log(`Query error: ${errMsg}`);
|
|
1754
|
-
if (err instanceof Error) {
|
|
1755
|
-
if (err.cause) sessionCtx.log(` cause: ${err.cause instanceof Error ? err.cause.message : String(err.cause)}`);
|
|
1756
|
-
if ("exitCode" in err) sessionCtx.log(` exitCode: ${err.exitCode}`);
|
|
1757
|
-
if ("stderr" in err) sessionCtx.log(` stderr: ${err.stderr}`);
|
|
1758
|
-
if ("code" in err) sessionCtx.log(` code: ${err.code}`);
|
|
1759
|
-
if (err.stack) sessionCtx.log(` stack: ${err.stack.split("\n").slice(1, 4).join(" | ")}`);
|
|
1760
|
-
}
|
|
1761
|
-
if (retryCount >= MAX_RETRIES || !claudeSessionId) {
|
|
1762
|
-
sessionCtx.log("Exhausted retries, session will be suspended");
|
|
1763
|
-
sessionCtx.setRuntimeState("error");
|
|
1764
|
-
return;
|
|
1765
|
-
}
|
|
1766
|
-
retryCount++;
|
|
1767
|
-
sessionCtx.log(`Attempting auto-resume (retry ${retryCount}/${MAX_RETRIES})`);
|
|
1768
|
-
try {
|
|
1769
|
-
respawnQuery(claudeSessionId, sessionCtx);
|
|
1770
|
-
} catch (resumeErr) {
|
|
1771
|
-
sessionCtx.log(`Auto-resume failed: ${resumeErr instanceof Error ? resumeErr.message : String(resumeErr)}`);
|
|
1916
|
+
sessionCtx.setRuntimeState("idle");
|
|
1772
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
|
+
}
|
|
1773
1942
|
}
|
|
1774
1943
|
}
|
|
1944
|
+
} finally {
|
|
1945
|
+
toolCallProcessor.flush();
|
|
1775
1946
|
}
|
|
1776
1947
|
}
|
|
1777
1948
|
const contextTreePath = config.contextTreePath ?? null;
|
|
@@ -2701,8 +2872,11 @@ var SessionManager = class {
|
|
|
2701
2872
|
setRuntimeState: (state) => {
|
|
2702
2873
|
this.setSessionRuntimeState(chatId, state);
|
|
2703
2874
|
},
|
|
2704
|
-
|
|
2705
|
-
this.config.
|
|
2875
|
+
emitEvent: (event) => {
|
|
2876
|
+
this.config.onSessionEvent?.(chatId, event);
|
|
2877
|
+
},
|
|
2878
|
+
reportSessionCompletion: () => {
|
|
2879
|
+
this.config.onSessionCompletion?.(chatId);
|
|
2706
2880
|
}
|
|
2707
2881
|
};
|
|
2708
2882
|
}
|
|
@@ -2846,7 +3020,8 @@ var AgentSlot = class {
|
|
|
2846
3020
|
agentConfigCache: this.agentConfigCache,
|
|
2847
3021
|
onStateChange: (chatId, state) => this.reportSessionState(chatId, state),
|
|
2848
3022
|
onRuntimeStateChange: (state) => this.reportRuntimeState(state),
|
|
2849
|
-
|
|
3023
|
+
onSessionEvent: (chatId, event) => this.reportSessionEvent(chatId, event),
|
|
3024
|
+
onSessionCompletion: (chatId) => this.reportSessionCompletion(chatId)
|
|
2850
3025
|
});
|
|
2851
3026
|
const onCommand = (cmd) => {
|
|
2852
3027
|
if (cmd.agentId === this.config.agentId && this.sessionManager) this.sessionManager.handleCommand(cmd.chatId, cmd.type).catch((err) => {
|
|
@@ -2880,8 +3055,11 @@ var AgentSlot = class {
|
|
|
2880
3055
|
reportRuntimeState(state) {
|
|
2881
3056
|
this.clientConnection.reportRuntimeState(this.config.agentId, state);
|
|
2882
3057
|
}
|
|
2883
|
-
|
|
2884
|
-
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);
|
|
2885
3063
|
}
|
|
2886
3064
|
fullStateSync() {
|
|
2887
3065
|
if (!this.sessionManager) return;
|
|
@@ -3036,10 +3214,11 @@ var ClientRuntime = class {
|
|
|
3036
3214
|
agentNames = /* @__PURE__ */ new Set();
|
|
3037
3215
|
watcher = null;
|
|
3038
3216
|
debounceTimer = null;
|
|
3039
|
-
constructor(serverUrl) {
|
|
3217
|
+
constructor(serverUrl, clientId) {
|
|
3040
3218
|
this.serverUrl = serverUrl;
|
|
3041
3219
|
this.connection = new ClientConnection({
|
|
3042
3220
|
serverUrl,
|
|
3221
|
+
clientId,
|
|
3043
3222
|
getAccessToken: () => ensureFreshAccessToken()
|
|
3044
3223
|
});
|
|
3045
3224
|
registerBuiltinHandlers();
|
|
@@ -3655,7 +3834,7 @@ async function onboardCheck(args) {
|
|
|
3655
3834
|
key: "connect",
|
|
3656
3835
|
label: "Signed in",
|
|
3657
3836
|
status: "missing_required",
|
|
3658
|
-
hint: "Run `first-tree-hub connect <server-url>` first"
|
|
3837
|
+
hint: "Run `first-tree-hub client connect <server-url>` first"
|
|
3659
3838
|
});
|
|
3660
3839
|
try {
|
|
3661
3840
|
const serverUrl = resolveServerUrl(args.server);
|
|
@@ -3713,11 +3892,17 @@ async function onboardCheck(args) {
|
|
|
3713
3892
|
status: "missing_required",
|
|
3714
3893
|
hint: "Provide via --type"
|
|
3715
3894
|
});
|
|
3716
|
-
if (args.type && args.type !== "human"
|
|
3895
|
+
if (args.type && args.type !== "human") if (args.clientId) items.push({
|
|
3717
3896
|
key: "client",
|
|
3718
3897
|
label: "Target client",
|
|
3719
|
-
status: "
|
|
3720
|
-
|
|
3898
|
+
status: "ok",
|
|
3899
|
+
value: args.clientId
|
|
3900
|
+
});
|
|
3901
|
+
else items.push({
|
|
3902
|
+
key: "client",
|
|
3903
|
+
label: "Target client",
|
|
3904
|
+
status: "ok",
|
|
3905
|
+
value: "(unbound — claimed on first WS connect)"
|
|
3721
3906
|
});
|
|
3722
3907
|
return items;
|
|
3723
3908
|
}
|
|
@@ -3788,7 +3973,7 @@ async function onboardCreate(args) {
|
|
|
3788
3973
|
}
|
|
3789
3974
|
const runtimeAgent = args.type === "human" ? args.assistant : args.id;
|
|
3790
3975
|
if (args.feishuBotAppId && args.feishuBotAppSecret) {
|
|
3791
|
-
const { bindFeishuBot } = await import("./feishu-
|
|
3976
|
+
const { bindFeishuBot } = await import("./feishu-CJ08ntOD.mjs").then((n) => n.r);
|
|
3792
3977
|
const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
|
|
3793
3978
|
if (!targetAgentUuid) process.stderr.write(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
|
|
3794
3979
|
else {
|
|
@@ -3929,7 +4114,7 @@ function setNestedByDot(obj, dotPath, value) {
|
|
|
3929
4114
|
if (lastKey !== void 0) current[lastKey] = value;
|
|
3930
4115
|
}
|
|
3931
4116
|
//#endregion
|
|
3932
|
-
//#region ../server/dist/app-
|
|
4117
|
+
//#region ../server/dist/app-nJ9jSQtv.mjs
|
|
3933
4118
|
var __defProp = Object.defineProperty;
|
|
3934
4119
|
var __exportAll = (all, no_symbols) => {
|
|
3935
4120
|
let target = {};
|
|
@@ -4852,18 +5037,20 @@ function defaultVisibility(type) {
|
|
|
4852
5037
|
/**
|
|
4853
5038
|
* Resolve + validate the client that will own the new agent.
|
|
4854
5039
|
*
|
|
4855
|
-
* Rule (unified-user-token
|
|
4856
|
-
* -
|
|
4857
|
-
*
|
|
4858
|
-
* -
|
|
4859
|
-
*
|
|
5040
|
+
* Rule (unified-user-token, post-first-bind relaxation):
|
|
5041
|
+
* - Human agents represent the member themselves and have no runtime; a
|
|
5042
|
+
* missing `clientId` is required and the column stays NULL.
|
|
5043
|
+
* - Non-human agents MAY omit `clientId` at creation; the row stays NULL
|
|
5044
|
+
* and is claimed on the first WS bind (see `api/agent/ws-client.ts`).
|
|
5045
|
+
* - When a non-human agent IS created with a `clientId`, the pinned client
|
|
5046
|
+
* must already be owned by the manager's user (Rule R-RUN).
|
|
4860
5047
|
*/
|
|
4861
5048
|
async function resolveAgentClient(db, data) {
|
|
4862
5049
|
if (data.type === "human") {
|
|
4863
5050
|
if (data.clientId) throw new BadRequestError("Human agents cannot be pinned to a client");
|
|
4864
5051
|
return null;
|
|
4865
5052
|
}
|
|
4866
|
-
if (!data.clientId)
|
|
5053
|
+
if (!data.clientId) return null;
|
|
4867
5054
|
const [manager] = await db.select({ userId: members.userId }).from(members).where(eq(members.id, data.managerId)).limit(1);
|
|
4868
5055
|
if (!manager) throw new BadRequestError(`Manager "${data.managerId}" not found`);
|
|
4869
5056
|
const [client] = await db.select({
|
|
@@ -4871,7 +5058,7 @@ async function resolveAgentClient(db, data) {
|
|
|
4871
5058
|
userId: clients.userId
|
|
4872
5059
|
}).from(clients).where(eq(clients.id, data.clientId)).limit(1);
|
|
4873
5060
|
if (!client) throw new BadRequestError(`Client "${data.clientId}" not found`);
|
|
4874
|
-
if (!client.userId) throw new BadRequestError(`Client "${data.clientId}" has not been claimed by a user yet. Have the operator run \`first-tree-hub connect\` on that machine before pinning an agent to it.`);
|
|
5061
|
+
if (!client.userId) throw new BadRequestError(`Client "${data.clientId}" has not been claimed by a user yet. Have the operator run \`first-tree-hub client connect\` on that machine before pinning an agent to it.`);
|
|
4875
5062
|
if (client.userId !== manager.userId) throw new ForbiddenError(`Client "${data.clientId}" is not owned by the manager's user — pick a client belonging to that user.`);
|
|
4876
5063
|
return client.id;
|
|
4877
5064
|
}
|
|
@@ -4973,8 +5160,11 @@ async function listAgentsForMember(db, scope, limit, cursor, type) {
|
|
|
4973
5160
|
};
|
|
4974
5161
|
}
|
|
4975
5162
|
async function updateAgent(db, uuid, data) {
|
|
4976
|
-
if (data.clientId !== void 0) throw new BadRequestError("clientId is immutable in this milestone — delete and re-create the agent on the target client to move it");
|
|
4977
5163
|
const agent = await getAgent(db, uuid);
|
|
5164
|
+
if (data.clientId !== void 0) {
|
|
5165
|
+
if (data.clientId === null) throw new BadRequestError("clientId cannot be cleared — once bound, an agent stays bound to its client");
|
|
5166
|
+
if (agent.clientId !== null && agent.clientId !== data.clientId) throw new BadRequestError("clientId is immutable once set — delete and re-create the agent on the target client to move it");
|
|
5167
|
+
}
|
|
4978
5168
|
const updates = { updatedAt: /* @__PURE__ */ new Date() };
|
|
4979
5169
|
if (data.type !== void 0) updates.type = data.type;
|
|
4980
5170
|
if (data.displayName !== void 0) updates.displayName = data.displayName;
|
|
@@ -4991,6 +5181,14 @@ async function updateAgent(db, uuid, data) {
|
|
|
4991
5181
|
if (manager.organizationId !== agent.organizationId) throw new BadRequestError("Manager must belong to the same organization as the agent");
|
|
4992
5182
|
updates.managerId = data.managerId;
|
|
4993
5183
|
}
|
|
5184
|
+
if (data.clientId !== void 0 && data.clientId !== null && agent.clientId === null) {
|
|
5185
|
+
const resolvedClientId = await resolveAgentClient(db, {
|
|
5186
|
+
clientId: data.clientId,
|
|
5187
|
+
managerId: updates.managerId ?? agent.managerId,
|
|
5188
|
+
type: agent.type
|
|
5189
|
+
});
|
|
5190
|
+
if (resolvedClientId !== null) updates.clientId = resolvedClientId;
|
|
5191
|
+
}
|
|
4994
5192
|
const [updated] = await db.update(agents).set(updates).where(eq(agents.uuid, agent.uuid)).returning();
|
|
4995
5193
|
if (!updated) throw new Error("Unexpected: UPDATE RETURNING produced no row");
|
|
4996
5194
|
return updated;
|
|
@@ -5354,14 +5552,20 @@ async function unbindAgent(db, agentId) {
|
|
|
5354
5552
|
...runtimeFieldsReset(now)
|
|
5355
5553
|
}).where(eq(agentPresence.agentId, agentId));
|
|
5356
5554
|
}
|
|
5357
|
-
/** Set runtime state directly from client-reported value.
|
|
5358
|
-
|
|
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) {
|
|
5359
5562
|
const now = /* @__PURE__ */ new Date();
|
|
5360
5563
|
await db.update(agentPresence).set({
|
|
5361
5564
|
runtimeState,
|
|
5362
5565
|
runtimeUpdatedAt: now,
|
|
5363
5566
|
lastSeenAt: now
|
|
5364
5567
|
}).where(eq(agentPresence.agentId, agentId));
|
|
5568
|
+
if (options?.notifier && options.organizationId) options.notifier.notifyRuntimeStateChange(agentId, runtimeState, options.organizationId).catch(() => {});
|
|
5365
5569
|
}
|
|
5366
5570
|
/** Touch agent last_seen_at on heartbeat (per-agent liveness). */
|
|
5367
5571
|
async function touchAgent(db, agentId) {
|
|
@@ -5487,9 +5691,7 @@ async function getClient(db, clientId) {
|
|
|
5487
5691
|
return row ?? null;
|
|
5488
5692
|
}
|
|
5489
5693
|
async function listClients(db, userId) {
|
|
5490
|
-
const
|
|
5491
|
-
if (userId) conditions.push(eq(clients.userId, userId));
|
|
5492
|
-
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));
|
|
5493
5695
|
const counts = await db.select({
|
|
5494
5696
|
clientId: agents.clientId,
|
|
5495
5697
|
count: sql`count(*)::int`
|
|
@@ -5754,13 +5956,16 @@ async function listMessages(db, chatId, limit, cursor) {
|
|
|
5754
5956
|
const INBOX_CHANNEL = "inbox_notifications";
|
|
5755
5957
|
const CONFIG_CHANNEL = "config_changes";
|
|
5756
5958
|
const SESSION_STATE_CHANNEL = "session_state_changes";
|
|
5959
|
+
const RUNTIME_STATE_CHANNEL = "runtime_state_changes";
|
|
5757
5960
|
function createNotifier(listenClient) {
|
|
5758
5961
|
const subscriptions = /* @__PURE__ */ new Map();
|
|
5759
5962
|
const configChangeHandlers = [];
|
|
5760
5963
|
const sessionStateChangeHandlers = [];
|
|
5964
|
+
const runtimeStateChangeHandlers = [];
|
|
5761
5965
|
let unlistenInboxFn = null;
|
|
5762
5966
|
let unlistenConfigFn = null;
|
|
5763
5967
|
let unlistenSessionStateFn = null;
|
|
5968
|
+
let unlistenRuntimeStateFn = null;
|
|
5764
5969
|
function handleNotification(payload) {
|
|
5765
5970
|
const sepIdx = payload.indexOf(":");
|
|
5766
5971
|
if (sepIdx === -1) return;
|
|
@@ -5801,9 +6006,14 @@ function createNotifier(listenClient) {
|
|
|
5801
6006
|
await listenClient`SELECT pg_notify(${CONFIG_CHANNEL}, ${configType})`;
|
|
5802
6007
|
} catch {}
|
|
5803
6008
|
},
|
|
5804
|
-
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) {
|
|
5805
6015
|
try {
|
|
5806
|
-
await listenClient`SELECT pg_notify(${
|
|
6016
|
+
await listenClient`SELECT pg_notify(${RUNTIME_STATE_CHANNEL}, ${`${agentId}:${state}:${organizationId}`})`;
|
|
5807
6017
|
} catch {}
|
|
5808
6018
|
},
|
|
5809
6019
|
onConfigChange(handler) {
|
|
@@ -5812,6 +6022,9 @@ function createNotifier(listenClient) {
|
|
|
5812
6022
|
onSessionStateChange(handler) {
|
|
5813
6023
|
sessionStateChangeHandlers.push(handler);
|
|
5814
6024
|
},
|
|
6025
|
+
onRuntimeStateChange(handler) {
|
|
6026
|
+
runtimeStateChangeHandlers.push(handler);
|
|
6027
|
+
},
|
|
5815
6028
|
async start() {
|
|
5816
6029
|
unlistenInboxFn = (await listenClient.listen(INBOX_CHANNEL, (payload) => {
|
|
5817
6030
|
if (payload) handleNotification(payload);
|
|
@@ -5823,14 +6036,33 @@ function createNotifier(listenClient) {
|
|
|
5823
6036
|
if (payload) {
|
|
5824
6037
|
const firstSep = payload.indexOf(":");
|
|
5825
6038
|
const secondSep = payload.indexOf(":", firstSep + 1);
|
|
5826
|
-
|
|
6039
|
+
const thirdSep = payload.indexOf(":", secondSep + 1);
|
|
6040
|
+
if (firstSep > 0 && secondSep > firstSep && thirdSep > secondSep) {
|
|
5827
6041
|
const agentId = payload.slice(0, firstSep);
|
|
5828
6042
|
const chatId = payload.slice(firstSep + 1, secondSep);
|
|
5829
|
-
const state = payload.slice(secondSep + 1);
|
|
6043
|
+
const state = payload.slice(secondSep + 1, thirdSep);
|
|
6044
|
+
const organizationId = payload.slice(thirdSep + 1);
|
|
5830
6045
|
for (const handler of sessionStateChangeHandlers) handler({
|
|
5831
6046
|
agentId,
|
|
5832
6047
|
chatId,
|
|
5833
|
-
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
|
|
5834
6066
|
});
|
|
5835
6067
|
}
|
|
5836
6068
|
}
|
|
@@ -5849,6 +6081,10 @@ function createNotifier(listenClient) {
|
|
|
5849
6081
|
await unlistenSessionStateFn();
|
|
5850
6082
|
unlistenSessionStateFn = null;
|
|
5851
6083
|
}
|
|
6084
|
+
if (unlistenRuntimeStateFn) {
|
|
6085
|
+
await unlistenRuntimeStateFn();
|
|
6086
|
+
unlistenRuntimeStateFn = null;
|
|
6087
|
+
}
|
|
5852
6088
|
}
|
|
5853
6089
|
};
|
|
5854
6090
|
}
|
|
@@ -5982,7 +6218,7 @@ async function adminAgentRoutes(app) {
|
|
|
5982
6218
|
};
|
|
5983
6219
|
if (health === "disconnected") return reply.status(200).send({
|
|
5984
6220
|
status: "offline",
|
|
5985
|
-
message: "Agent is not connected. Start the client with: first-tree-hub connect <server-url>",
|
|
6221
|
+
message: "Agent is not connected. Start the client with: first-tree-hub client connect <server-url>",
|
|
5986
6222
|
connection
|
|
5987
6223
|
});
|
|
5988
6224
|
if (health === "stale") return reply.status(200).send({
|
|
@@ -6280,8 +6516,8 @@ const agentChatSessions = pgTable("agent_chat_sessions", {
|
|
|
6280
6516
|
state: text("state").notNull(),
|
|
6281
6517
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
6282
6518
|
}, (table) => [primaryKey({ columns: [table.agentId, table.chatId] })]);
|
|
6283
|
-
/** Upsert a session state
|
|
6284
|
-
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) {
|
|
6285
6521
|
const now = /* @__PURE__ */ new Date();
|
|
6286
6522
|
await db.transaction(async (tx) => {
|
|
6287
6523
|
await tx.insert(agentChatSessions).values({
|
|
@@ -6308,7 +6544,7 @@ async function upsertSessionState(db, agentId, chatId, state, notifier) {
|
|
|
6308
6544
|
lastSeenAt: now
|
|
6309
6545
|
}).where(eq(agentPresence.agentId, agentId));
|
|
6310
6546
|
});
|
|
6311
|
-
if (notifier) notifier.notifySessionStateChange(agentId, chatId, state).catch(() => {});
|
|
6547
|
+
if (notifier) notifier.notifySessionStateChange(agentId, chatId, state, organizationId).catch(() => {});
|
|
6312
6548
|
}
|
|
6313
6549
|
async function resetActivity(db, agentId) {
|
|
6314
6550
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -6357,7 +6593,8 @@ async function listAgentsWithRuntime(db, scope) {
|
|
|
6357
6593
|
runtimeState: agentPresence.runtimeState,
|
|
6358
6594
|
activeSessions: agentPresence.activeSessions,
|
|
6359
6595
|
totalSessions: agentPresence.totalSessions,
|
|
6360
|
-
runtimeUpdatedAt: agentPresence.runtimeUpdatedAt
|
|
6596
|
+
runtimeUpdatedAt: agentPresence.runtimeUpdatedAt,
|
|
6597
|
+
type: agents.type
|
|
6361
6598
|
}).from(agentPresence).innerJoin(agents, eq(agentPresence.agentId, agents.uuid)).where(and(isNotNull(agentPresence.runtimeState), agentVisibilityCondition(scope)));
|
|
6362
6599
|
}
|
|
6363
6600
|
/**
|
|
@@ -6446,7 +6683,8 @@ async function adminActivityRoutes(app) {
|
|
|
6446
6683
|
runtimeState: a.runtimeState,
|
|
6447
6684
|
activeSessions: a.activeSessions,
|
|
6448
6685
|
totalSessions: a.totalSessions,
|
|
6449
|
-
runtimeUpdatedAt: a.runtimeUpdatedAt?.toISOString() ?? null
|
|
6686
|
+
runtimeUpdatedAt: a.runtimeUpdatedAt?.toISOString() ?? null,
|
|
6687
|
+
type: "type" in a ? a.type : null
|
|
6450
6688
|
}))
|
|
6451
6689
|
};
|
|
6452
6690
|
});
|
|
@@ -6475,6 +6713,16 @@ const notifications = pgTable("notifications", {
|
|
|
6475
6713
|
index("idx_notifications_agent").on(table.agentId),
|
|
6476
6714
|
index("idx_notifications_org_read").on(table.organizationId, table.read)
|
|
6477
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
|
+
}
|
|
6478
6726
|
/** Runtime system configuration (key-value JSONB). Dynamically modifiable via Admin API; controls inbox timeout, retry count, etc. */
|
|
6479
6727
|
const systemConfigs = pgTable("system_configs", {
|
|
6480
6728
|
key: text("key").primaryKey(),
|
|
@@ -6507,15 +6755,6 @@ async function updateConfigs(db, updates) {
|
|
|
6507
6755
|
});
|
|
6508
6756
|
return getAllConfigs(db);
|
|
6509
6757
|
}
|
|
6510
|
-
/**
|
|
6511
|
-
* Push channels: Admin WS, Webhook, Feishu.
|
|
6512
|
-
* Set at app startup via `setAdminWsBroadcast` and reloaded from system_configs.
|
|
6513
|
-
*/
|
|
6514
|
-
let adminWsBroadcast = null;
|
|
6515
|
-
/** Register the admin WS broadcast function (called once at app startup). */
|
|
6516
|
-
function setAdminWsBroadcast(fn) {
|
|
6517
|
-
adminWsBroadcast = fn;
|
|
6518
|
-
}
|
|
6519
6758
|
/** Create a notification, persist it, and fire-and-forget push to all channels. */
|
|
6520
6759
|
async function createNotification(db, data) {
|
|
6521
6760
|
const id = uuidv7();
|
|
@@ -6599,13 +6838,11 @@ async function notifyAgentEvent(db, agentId, type, severity, message, chatId) {
|
|
|
6599
6838
|
} catch {}
|
|
6600
6839
|
}
|
|
6601
6840
|
function pushToAdminWs(notification) {
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
6606
|
-
|
|
6607
|
-
});
|
|
6608
|
-
} catch {}
|
|
6841
|
+
broadcastToAdmins({
|
|
6842
|
+
type: "notification",
|
|
6843
|
+
organizationId: notification.organizationId,
|
|
6844
|
+
data: notification
|
|
6845
|
+
});
|
|
6609
6846
|
}
|
|
6610
6847
|
async function pushToWebhook(db, notification) {
|
|
6611
6848
|
const webhookUrl = await getConfig(db, "notification_webhook_url");
|
|
@@ -6807,46 +7044,97 @@ async function filterSessionsByParticipant(db, sessions, participantAgentId) {
|
|
|
6807
7044
|
return sessions.filter((s) => allowedChatIds.has(s.chatId));
|
|
6808
7045
|
}
|
|
6809
7046
|
/**
|
|
6810
|
-
* Session
|
|
6811
|
-
*
|
|
6812
|
-
*
|
|
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.
|
|
6813
7058
|
*/
|
|
6814
|
-
const
|
|
7059
|
+
const sessionEvents = pgTable("session_events", {
|
|
6815
7060
|
id: text("id").primaryKey(),
|
|
6816
7061
|
agentId: text("agent_id").notNull(),
|
|
6817
7062
|
chatId: text("chat_id").notNull(),
|
|
6818
|
-
|
|
6819
|
-
|
|
6820
|
-
|
|
6821
|
-
|
|
6822
|
-
|
|
6823
|
-
|
|
6824
|
-
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
|
|
6830
|
-
|
|
6831
|
-
|
|
6832
|
-
|
|
6833
|
-
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
|
-
});
|
|
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
|
+
};
|
|
6837
7081
|
}
|
|
6838
|
-
/**
|
|
6839
|
-
async function
|
|
6840
|
-
const
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
|
|
6844
|
-
|
|
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];
|
|
6845
7129
|
return {
|
|
6846
|
-
|
|
6847
|
-
|
|
7130
|
+
items,
|
|
7131
|
+
nextCursor: hasMore && last ? last.seq : null
|
|
6848
7132
|
};
|
|
6849
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
|
+
}
|
|
6850
7138
|
const sessionFilterSchema = z.object({
|
|
6851
7139
|
state: z.enum([
|
|
6852
7140
|
"active",
|
|
@@ -6892,16 +7180,22 @@ async function adminSessionRoutes(app) {
|
|
|
6892
7180
|
});
|
|
6893
7181
|
/** GET /admin/sessions/agents/:agentId/:chatId — single session detail */
|
|
6894
7182
|
app.get("/agents/:agentId/:chatId", async (request) => {
|
|
6895
|
-
|
|
7183
|
+
const scope = memberScope(request);
|
|
7184
|
+
await assertAgentVisible(app.db, scope, request.params.agentId);
|
|
7185
|
+
await assertChatAccess(app.db, scope, request.params.chatId);
|
|
6896
7186
|
return getSession(app.db, request.params.agentId, request.params.chatId);
|
|
6897
7187
|
});
|
|
6898
|
-
/** GET /admin/sessions/agents/:agentId/:chatId/
|
|
6899
|
-
app.get("/agents/:agentId/:chatId/
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
|
|
6903
|
-
|
|
6904
|
-
|
|
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
|
+
});
|
|
6905
7199
|
});
|
|
6906
7200
|
/** POST /admin/sessions/agents/:agentId/:chatId/suspend — suspend a session */
|
|
6907
7201
|
app.post("/agents/:agentId/:chatId/suspend", async (request, reply) => {
|
|
@@ -7508,30 +7802,40 @@ async function adminTaskRoutes(app) {
|
|
|
7508
7802
|
return getTaskHealth(app.db, request.params.taskId);
|
|
7509
7803
|
});
|
|
7510
7804
|
}
|
|
7511
|
-
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
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
|
+
}
|
|
7520
7814
|
function adminWsRoutes(notifier, jwtSecret) {
|
|
7521
7815
|
const adminSockets = /* @__PURE__ */ new Map();
|
|
7522
7816
|
const secret = new TextEncoder().encode(jwtSecret);
|
|
7523
|
-
|
|
7817
|
+
function broadcastOrgScoped(payload) {
|
|
7524
7818
|
const orgId = payload.organizationId;
|
|
7525
|
-
|
|
7526
|
-
|
|
7527
|
-
|
|
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);
|
|
7528
7834
|
notifier.onSessionStateChange((payload) => {
|
|
7529
|
-
|
|
7835
|
+
broadcastOrgScoped({
|
|
7530
7836
|
type: "session:state",
|
|
7531
7837
|
...payload
|
|
7532
7838
|
});
|
|
7533
|
-
const orgId = payload.organizationId;
|
|
7534
|
-
for (const [ws, meta] of adminSockets) if (ws.readyState === 1 && (!orgId || meta.organizationId === orgId)) ws.send(data);
|
|
7535
7839
|
});
|
|
7536
7840
|
return async (app) => {
|
|
7537
7841
|
app.get("/admin", { websocket: true }, async (socket, request) => {
|
|
@@ -7545,9 +7849,10 @@ function adminWsRoutes(notifier, jwtSecret) {
|
|
|
7545
7849
|
return;
|
|
7546
7850
|
}
|
|
7547
7851
|
let organizationId;
|
|
7852
|
+
let memberId;
|
|
7548
7853
|
try {
|
|
7549
7854
|
const { payload } = await jwtVerify(token, secret);
|
|
7550
|
-
if (payload.type !== "access" || !payload.sub ||
|
|
7855
|
+
if (payload.type !== "access" || !payload.sub || typeof payload.organizationId !== "string" || typeof payload.memberId !== "string") {
|
|
7551
7856
|
socket.send(JSON.stringify({
|
|
7552
7857
|
type: "error",
|
|
7553
7858
|
message: "Invalid token type"
|
|
@@ -7556,6 +7861,7 @@ function adminWsRoutes(notifier, jwtSecret) {
|
|
|
7556
7861
|
return;
|
|
7557
7862
|
}
|
|
7558
7863
|
organizationId = payload.organizationId;
|
|
7864
|
+
memberId = payload.memberId;
|
|
7559
7865
|
} catch {
|
|
7560
7866
|
socket.send(JSON.stringify({
|
|
7561
7867
|
type: "error",
|
|
@@ -7564,7 +7870,12 @@ function adminWsRoutes(notifier, jwtSecret) {
|
|
|
7564
7870
|
socket.close(4001, "Auth failed");
|
|
7565
7871
|
return;
|
|
7566
7872
|
}
|
|
7567
|
-
|
|
7873
|
+
const visibleAgentIds = await loadVisibleAgentIds(app.db, organizationId, memberId);
|
|
7874
|
+
adminSockets.set(socket, {
|
|
7875
|
+
organizationId,
|
|
7876
|
+
memberId,
|
|
7877
|
+
visibleAgentIds
|
|
7878
|
+
});
|
|
7568
7879
|
socket.send(JSON.stringify({ type: "admin:connected" }));
|
|
7569
7880
|
socket.on("close", () => {
|
|
7570
7881
|
adminSockets.delete(socket);
|
|
@@ -8048,7 +8359,7 @@ const wsMessageSchema = z.object({
|
|
|
8048
8359
|
* Failure ⇒ server sends `auth:rejected` and closes (code 4401).
|
|
8049
8360
|
* 2. `client:register` — bind the client_id to the authenticated user.
|
|
8050
8361
|
* 3. `agent:bind` — run Rule R-RUN (no token); populate presence.
|
|
8051
|
-
* 4. `session:state` / `runtime:state` / `session:
|
|
8362
|
+
* 4. `session:state` / `runtime:state` / `session:event` / `session:completion` / `heartbeat`.
|
|
8052
8363
|
* 5. `agent:unbind` — stop multiplexing for a specific agent.
|
|
8053
8364
|
*
|
|
8054
8365
|
* When the JWT is about to expire the server sends `auth:expired` so the
|
|
@@ -8083,6 +8394,15 @@ function clientWsRoutes(notifier, instanceId) {
|
|
|
8083
8394
|
let clientId = null;
|
|
8084
8395
|
let authExpiryTimer = null;
|
|
8085
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
|
+
}
|
|
8086
8406
|
const authTimeout = setTimeout(() => {
|
|
8087
8407
|
if (!session) {
|
|
8088
8408
|
try {
|
|
@@ -8226,8 +8546,9 @@ function clientWsRoutes(notifier, instanceId) {
|
|
|
8226
8546
|
inboxId: agents.inboxId,
|
|
8227
8547
|
status: agents.status,
|
|
8228
8548
|
clientId: agents.clientId,
|
|
8229
|
-
clientUserId: clients.userId
|
|
8230
|
-
|
|
8549
|
+
clientUserId: clients.userId,
|
|
8550
|
+
managerUserId: members.userId
|
|
8551
|
+
}).from(agents).leftJoin(clients, eq(agents.clientId, clients.id)).leftJoin(members, eq(agents.managerId, members.id)).where(and(eq(agents.uuid, bindRequest.agentId))).limit(1);
|
|
8231
8552
|
if (!agent) {
|
|
8232
8553
|
sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.UNKNOWN_AGENT);
|
|
8233
8554
|
return;
|
|
@@ -8240,11 +8561,22 @@ function clientWsRoutes(notifier, instanceId) {
|
|
|
8240
8561
|
sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.AGENT_SUSPENDED);
|
|
8241
8562
|
return;
|
|
8242
8563
|
}
|
|
8243
|
-
if (
|
|
8564
|
+
if (agent.clientId === null) {
|
|
8565
|
+
if (!agent.managerUserId || agent.managerUserId !== session.userId) {
|
|
8566
|
+
sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.NOT_OWNED);
|
|
8567
|
+
return;
|
|
8568
|
+
}
|
|
8569
|
+
if ((await app.db.update(agents).set({
|
|
8570
|
+
clientId,
|
|
8571
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
8572
|
+
}).where(and(eq(agents.uuid, agent.id), isNull(agents.clientId))).returning({ uuid: agents.uuid })).length === 0) {
|
|
8573
|
+
sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.WRONG_CLIENT);
|
|
8574
|
+
return;
|
|
8575
|
+
}
|
|
8576
|
+
} else if (agent.clientId !== clientId) {
|
|
8244
8577
|
sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.WRONG_CLIENT);
|
|
8245
8578
|
return;
|
|
8246
|
-
}
|
|
8247
|
-
if (!agent.clientUserId || agent.clientUserId !== session.userId) {
|
|
8579
|
+
} else if (!agent.clientUserId || agent.clientUserId !== session.userId) {
|
|
8248
8580
|
sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.NOT_OWNED);
|
|
8249
8581
|
return;
|
|
8250
8582
|
}
|
|
@@ -8295,7 +8627,8 @@ function clientWsRoutes(notifier, instanceId) {
|
|
|
8295
8627
|
return;
|
|
8296
8628
|
}
|
|
8297
8629
|
const payload = sessionStateMessageSchema.parse(msg);
|
|
8298
|
-
|
|
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);
|
|
8299
8632
|
} else if (type === "runtime:state") {
|
|
8300
8633
|
const agentId = parsed.data.agentId;
|
|
8301
8634
|
if (!agentId || !boundAgents.has(agentId)) {
|
|
@@ -8306,10 +8639,33 @@ function clientWsRoutes(notifier, instanceId) {
|
|
|
8306
8639
|
return;
|
|
8307
8640
|
}
|
|
8308
8641
|
const payload = runtimeStateMessageSchema.parse(msg);
|
|
8309
|
-
await setRuntimeState(app.db, agentId, payload.runtimeState
|
|
8642
|
+
await setRuntimeState(app.db, agentId, payload.runtimeState, {
|
|
8643
|
+
organizationId: session.organizationId,
|
|
8644
|
+
notifier
|
|
8645
|
+
});
|
|
8310
8646
|
if (payload.runtimeState === "error" && shouldNotify(agentId, "agent_error")) notifyAgentEvent(app.db, agentId, "agent_error", "high", `Agent ${agentId} entered error state`).catch(() => {});
|
|
8311
8647
|
else if (payload.runtimeState === "blocked" && shouldNotify(agentId, "agent_blocked")) notifyAgentEvent(app.db, agentId, "agent_blocked", "medium", `Agent ${agentId} is blocked`).catch(() => {});
|
|
8312
|
-
} else if (type === "session:
|
|
8648
|
+
} else if (type === "session:event") {
|
|
8649
|
+
const agentId = parsed.data.agentId;
|
|
8650
|
+
if (!agentId || !boundAgents.has(agentId)) {
|
|
8651
|
+
socket.send(JSON.stringify({
|
|
8652
|
+
type: "error",
|
|
8653
|
+
message: "Agent not bound"
|
|
8654
|
+
}));
|
|
8655
|
+
return;
|
|
8656
|
+
}
|
|
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") {
|
|
8313
8669
|
const agentId = parsed.data.agentId;
|
|
8314
8670
|
if (!agentId || !boundAgents.has(agentId)) {
|
|
8315
8671
|
socket.send(JSON.stringify({
|
|
@@ -8318,8 +8674,7 @@ function clientWsRoutes(notifier, instanceId) {
|
|
|
8318
8674
|
}));
|
|
8319
8675
|
return;
|
|
8320
8676
|
}
|
|
8321
|
-
const payload =
|
|
8322
|
-
appendOutput(app.db, agentId, payload.chatId, payload.content).catch(() => {});
|
|
8677
|
+
const payload = sessionCompletionMessageSchema.parse(msg);
|
|
8323
8678
|
if (shouldNotify(agentId, `session_completed:${payload.chatId}`)) notifyAgentEvent(app.db, agentId, "session_completed", "low", `Agent ${agentId} completed a task`, payload.chatId).catch(() => {});
|
|
8324
8679
|
} else if (type === "heartbeat") {
|
|
8325
8680
|
if (clientId) {
|
|
@@ -8601,7 +8956,7 @@ async function meRoutes(app) {
|
|
|
8601
8956
|
return {
|
|
8602
8957
|
token,
|
|
8603
8958
|
expiresIn,
|
|
8604
|
-
command: `first-tree-hub connect ${`${request.headers["x-forwarded-proto"] ?? request.protocol}://${request.headers["x-forwarded-host"] ?? request.headers.host ?? request.hostname}`} --token ${token}`
|
|
8959
|
+
command: `first-tree-hub client connect ${`${request.headers["x-forwarded-proto"] ?? request.protocol}://${request.headers["x-forwarded-host"] ?? request.headers.host ?? request.hostname}`} --token ${token}`
|
|
8605
8960
|
};
|
|
8606
8961
|
});
|
|
8607
8962
|
}
|
|
@@ -9231,7 +9586,7 @@ var schema_exports = /* @__PURE__ */ __exportAll({
|
|
|
9231
9586
|
notifications: () => notifications,
|
|
9232
9587
|
organizations: () => organizations,
|
|
9233
9588
|
serverInstances: () => serverInstances,
|
|
9234
|
-
|
|
9589
|
+
sessionEvents: () => sessionEvents,
|
|
9235
9590
|
systemConfigs: () => systemConfigs,
|
|
9236
9591
|
taskChats: () => taskChats,
|
|
9237
9592
|
tasks: () => tasks,
|
|
@@ -10319,6 +10674,90 @@ async function nackEntry(db, entryId) {
|
|
|
10319
10674
|
WHERE id = ${entryId}
|
|
10320
10675
|
`);
|
|
10321
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
|
+
}
|
|
10322
10761
|
async function buildApp(config) {
|
|
10323
10762
|
const app = Fastify({ logger: config.logger ?? true });
|
|
10324
10763
|
const db = connectDatabase(config.database.url);
|
|
@@ -10408,7 +10847,7 @@ async function buildApp(config) {
|
|
|
10408
10847
|
await api.register(async (adminApp) => {
|
|
10409
10848
|
adminApp.addHook("onRequest", memberAuth);
|
|
10410
10849
|
await adminApp.register(adminClientRoutes);
|
|
10411
|
-
}, { prefix: "/
|
|
10850
|
+
}, { prefix: "/clients" });
|
|
10412
10851
|
await api.register(async (adminApp) => {
|
|
10413
10852
|
adminApp.addHook("onRequest", memberAuth);
|
|
10414
10853
|
await adminApp.register(adminActivityRoutes);
|
|
@@ -10477,6 +10916,10 @@ async function buildApp(config) {
|
|
|
10477
10916
|
const contextTreeDir = join(DEFAULT_DATA_DIR$1, "context-tree");
|
|
10478
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;
|
|
10479
10918
|
const backgroundTasks = createBackgroundTasks(app, config.instanceId, adapterManager, kaelRuntime);
|
|
10919
|
+
const pulseAggregator = createPulseAggregator({
|
|
10920
|
+
notifier,
|
|
10921
|
+
broadcast: broadcastToAdmins
|
|
10922
|
+
});
|
|
10480
10923
|
notifier.onConfigChange((configType) => {
|
|
10481
10924
|
if (configType === "adapter_configs") {
|
|
10482
10925
|
adapterManager.reload().catch((err) => app.log.error(err, "Adapter hot-reload failed (PG NOTIFY)"));
|
|
@@ -10487,10 +10930,12 @@ async function buildApp(config) {
|
|
|
10487
10930
|
await ensureDefaultOrganization(db);
|
|
10488
10931
|
await notifier.start();
|
|
10489
10932
|
backgroundTasks.start();
|
|
10933
|
+
pulseAggregator.start();
|
|
10490
10934
|
await adapterManager.reload();
|
|
10491
10935
|
await kaelRuntime?.reload();
|
|
10492
10936
|
});
|
|
10493
10937
|
app.addHook("onClose", async () => {
|
|
10938
|
+
pulseAggregator.stop();
|
|
10494
10939
|
backgroundTasks.stop();
|
|
10495
10940
|
adapterManager.shutdown();
|
|
10496
10941
|
kaelRuntime?.shutdown();
|