@agent-team-foundation/first-tree-hub 0.6.3 → 0.7.1
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 +90 -13
- package/dist/{core-B10jgThe.mjs → core-C05B8FzH.mjs} +948 -167
- 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 +4 -4
- 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,11 +1,11 @@
|
|
|
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
|
|
3
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
|
|
4
|
-
import { dirname, join, resolve } from "node:path";
|
|
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
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
5
|
import { ZodError, z } from "zod";
|
|
6
6
|
import "yaml";
|
|
7
7
|
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
8
|
-
import { homedir, hostname, platform } from "node:os";
|
|
8
|
+
import { homedir, hostname, platform, userInfo } from "node:os";
|
|
9
9
|
import { EventEmitter } from "node:events";
|
|
10
10
|
import WebSocket from "ws";
|
|
11
11
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
@@ -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() {
|
|
@@ -1055,7 +1106,6 @@ var ClientConnection = class extends EventEmitter {
|
|
|
1055
1106
|
return;
|
|
1056
1107
|
}
|
|
1057
1108
|
if (type === "auth:rejected" || type === "auth:expired") {
|
|
1058
|
-
this.registered = false;
|
|
1059
1109
|
if (type === "auth:expired") this.emit("auth:expired");
|
|
1060
1110
|
this.ws?.close(4401, type);
|
|
1061
1111
|
return;
|
|
@@ -1538,6 +1588,95 @@ function cleanWorkspaces(workspaceRoot, activeChatIds, ttlMs = DEFAULT_WORKSPACE
|
|
|
1538
1588
|
return removed;
|
|
1539
1589
|
}
|
|
1540
1590
|
const MAX_RETRIES = 2;
|
|
1591
|
+
const TOOL_RESULT_PREVIEW_LIMIT = 400;
|
|
1592
|
+
function extractContentBlocks(message) {
|
|
1593
|
+
if (!message || typeof message !== "object") return [];
|
|
1594
|
+
const inner = message.message;
|
|
1595
|
+
if (!inner || typeof inner !== "object") return [];
|
|
1596
|
+
const content = inner.content;
|
|
1597
|
+
return Array.isArray(content) ? content : [];
|
|
1598
|
+
}
|
|
1599
|
+
function isToolUseBlock(block) {
|
|
1600
|
+
if (!block || typeof block !== "object") return false;
|
|
1601
|
+
const b = block;
|
|
1602
|
+
return b.type === "tool_use" && typeof b.id === "string" && typeof b.name === "string";
|
|
1603
|
+
}
|
|
1604
|
+
function isToolResultBlock(block) {
|
|
1605
|
+
if (!block || typeof block !== "object") return false;
|
|
1606
|
+
const b = block;
|
|
1607
|
+
return b.type === "tool_result" && typeof b.tool_use_id === "string";
|
|
1608
|
+
}
|
|
1609
|
+
function isResultMessage(message) {
|
|
1610
|
+
if (!message || typeof message !== "object") return false;
|
|
1611
|
+
const m = message;
|
|
1612
|
+
return m.type === "result" && typeof m.subtype === "string";
|
|
1613
|
+
}
|
|
1614
|
+
function extractToolResultText(content) {
|
|
1615
|
+
if (typeof content === "string") return content;
|
|
1616
|
+
if (!Array.isArray(content)) return "";
|
|
1617
|
+
const parts = [];
|
|
1618
|
+
for (const part of content) {
|
|
1619
|
+
if (!part || typeof part !== "object") continue;
|
|
1620
|
+
const p = part;
|
|
1621
|
+
if (p.type === "text" && typeof p.text === "string") parts.push(p.text);
|
|
1622
|
+
}
|
|
1623
|
+
return parts.join("\n");
|
|
1624
|
+
}
|
|
1625
|
+
function createToolCallProcessor(emit) {
|
|
1626
|
+
const pending = /* @__PURE__ */ new Map();
|
|
1627
|
+
function pairResult(block) {
|
|
1628
|
+
const entry = pending.get(block.tool_use_id);
|
|
1629
|
+
if (!entry) return;
|
|
1630
|
+
const status = block.is_error === true ? "error" : "ok";
|
|
1631
|
+
const durationMs = Date.now() - entry.startedAt;
|
|
1632
|
+
const previewRaw = extractToolResultText(block.content);
|
|
1633
|
+
const resultPreview = previewRaw.length > 0 ? previewRaw.slice(0, TOOL_RESULT_PREVIEW_LIMIT) : void 0;
|
|
1634
|
+
emit({
|
|
1635
|
+
kind: "tool_call",
|
|
1636
|
+
payload: {
|
|
1637
|
+
toolUseId: entry.toolUseId,
|
|
1638
|
+
name: entry.name,
|
|
1639
|
+
args: entry.args,
|
|
1640
|
+
status,
|
|
1641
|
+
durationMs,
|
|
1642
|
+
...resultPreview !== void 0 ? { resultPreview } : {}
|
|
1643
|
+
}
|
|
1644
|
+
});
|
|
1645
|
+
pending.delete(block.tool_use_id);
|
|
1646
|
+
}
|
|
1647
|
+
return {
|
|
1648
|
+
onMessage(message) {
|
|
1649
|
+
if (!message || typeof message !== "object") return;
|
|
1650
|
+
const type = message.type;
|
|
1651
|
+
if (type === "assistant") for (const block of extractContentBlocks(message)) {
|
|
1652
|
+
if (!isToolUseBlock(block)) continue;
|
|
1653
|
+
pending.set(block.id, {
|
|
1654
|
+
toolUseId: block.id,
|
|
1655
|
+
name: block.name,
|
|
1656
|
+
args: block.input,
|
|
1657
|
+
startedAt: Date.now()
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
else if (type === "user") {
|
|
1661
|
+
for (const block of extractContentBlocks(message)) if (isToolResultBlock(block)) pairResult(block);
|
|
1662
|
+
}
|
|
1663
|
+
},
|
|
1664
|
+
flush() {
|
|
1665
|
+
if (pending.size === 0) return;
|
|
1666
|
+
for (const entry of pending.values()) emit({
|
|
1667
|
+
kind: "tool_call",
|
|
1668
|
+
payload: {
|
|
1669
|
+
toolUseId: entry.toolUseId,
|
|
1670
|
+
name: entry.name,
|
|
1671
|
+
args: entry.args,
|
|
1672
|
+
status: "pending",
|
|
1673
|
+
durationMs: Date.now() - entry.startedAt
|
|
1674
|
+
}
|
|
1675
|
+
});
|
|
1676
|
+
pending.clear();
|
|
1677
|
+
}
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1541
1680
|
/**
|
|
1542
1681
|
* Map a payload's MCP server list to the SDK's record type. Handles all three
|
|
1543
1682
|
* transports (stdio/http/sse) defined in the M1 schema.
|
|
@@ -1725,57 +1864,84 @@ const createClaudeCodeHandler = (config) => {
|
|
|
1725
1864
|
return true;
|
|
1726
1865
|
}
|
|
1727
1866
|
async function consumeOutput(sessionCtx) {
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
sessionCtx.
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
if (
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1867
|
+
const toolCallProcessor = createToolCallProcessor((event) => sessionCtx.emitEvent(event));
|
|
1868
|
+
try {
|
|
1869
|
+
while (true) {
|
|
1870
|
+
if (!currentQuery) return;
|
|
1871
|
+
try {
|
|
1872
|
+
sessionCtx.setRuntimeState("working");
|
|
1873
|
+
for await (const message of currentQuery) {
|
|
1874
|
+
sessionCtx.touch();
|
|
1875
|
+
toolCallProcessor.onMessage(message);
|
|
1876
|
+
if (isResultMessage(message)) {
|
|
1877
|
+
if (message.subtype === "success") {
|
|
1878
|
+
retryCount = 0;
|
|
1879
|
+
if (message.result && sessionCtx.chatId) {
|
|
1880
|
+
const resultText = message.result;
|
|
1881
|
+
sessionCtx.sdk.sendMessage(sessionCtx.chatId, {
|
|
1882
|
+
format: "text",
|
|
1883
|
+
content: resultText
|
|
1884
|
+
}).then(() => {
|
|
1885
|
+
sessionCtx.log("Result forwarded to chat");
|
|
1886
|
+
sessionCtx.reportSessionCompletion();
|
|
1887
|
+
}).catch((err) => {
|
|
1888
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1889
|
+
sessionCtx.log(`Failed to forward result: ${reason}`);
|
|
1890
|
+
const message = `Result forward failed: ${reason}\n---\n${resultText.slice(0, 1500)}`.slice(0, 2e3);
|
|
1891
|
+
sessionCtx.emitEvent({
|
|
1892
|
+
kind: "error",
|
|
1893
|
+
payload: {
|
|
1894
|
+
source: "runtime",
|
|
1895
|
+
message
|
|
1896
|
+
}
|
|
1897
|
+
});
|
|
1898
|
+
});
|
|
1899
|
+
}
|
|
1900
|
+
} else {
|
|
1901
|
+
const errors = message.errors ? message.errors.join("; ") : message.subtype;
|
|
1902
|
+
const errorLog = `Query result error: ${errors} (subtype=${message.subtype}, turns=${message.num_turns ?? "?"}, duration=${message.duration_ms ?? "?"}ms)`;
|
|
1903
|
+
sessionCtx.log(errorLog);
|
|
1904
|
+
sessionCtx.emitEvent({
|
|
1905
|
+
kind: "error",
|
|
1906
|
+
payload: {
|
|
1907
|
+
source: "sdk",
|
|
1908
|
+
message: errors
|
|
1909
|
+
}
|
|
1910
|
+
});
|
|
1744
1911
|
}
|
|
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}`);
|
|
1912
|
+
sessionCtx.setRuntimeState("idle");
|
|
1749
1913
|
}
|
|
1750
|
-
sessionCtx.setRuntimeState("idle");
|
|
1751
1914
|
}
|
|
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)}`);
|
|
1915
|
+
sessionCtx.setRuntimeState("idle");
|
|
1776
1916
|
return;
|
|
1917
|
+
} catch (err) {
|
|
1918
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1919
|
+
sessionCtx.log(`Query error: ${errMsg}`);
|
|
1920
|
+
if (err instanceof Error) {
|
|
1921
|
+
if (err.cause) sessionCtx.log(` cause: ${err.cause instanceof Error ? err.cause.message : String(err.cause)}`);
|
|
1922
|
+
if ("exitCode" in err) sessionCtx.log(` exitCode: ${err.exitCode}`);
|
|
1923
|
+
if ("stderr" in err) sessionCtx.log(` stderr: ${err.stderr}`);
|
|
1924
|
+
if ("code" in err) sessionCtx.log(` code: ${err.code}`);
|
|
1925
|
+
if (err.stack) sessionCtx.log(` stack: ${err.stack.split("\n").slice(1, 4).join(" | ")}`);
|
|
1926
|
+
}
|
|
1927
|
+
if (retryCount >= MAX_RETRIES || !claudeSessionId) {
|
|
1928
|
+
sessionCtx.log("Exhausted retries, session will be suspended");
|
|
1929
|
+
sessionCtx.setRuntimeState("error");
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1932
|
+
toolCallProcessor.flush();
|
|
1933
|
+
retryCount++;
|
|
1934
|
+
sessionCtx.log(`Attempting auto-resume (retry ${retryCount}/${MAX_RETRIES})`);
|
|
1935
|
+
try {
|
|
1936
|
+
respawnQuery(claudeSessionId, sessionCtx);
|
|
1937
|
+
} catch (resumeErr) {
|
|
1938
|
+
sessionCtx.log(`Auto-resume failed: ${resumeErr instanceof Error ? resumeErr.message : String(resumeErr)}`);
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1777
1941
|
}
|
|
1778
1942
|
}
|
|
1943
|
+
} finally {
|
|
1944
|
+
toolCallProcessor.flush();
|
|
1779
1945
|
}
|
|
1780
1946
|
}
|
|
1781
1947
|
const contextTreePath = config.contextTreePath ?? null;
|
|
@@ -2705,8 +2871,11 @@ var SessionManager = class {
|
|
|
2705
2871
|
setRuntimeState: (state) => {
|
|
2706
2872
|
this.setSessionRuntimeState(chatId, state);
|
|
2707
2873
|
},
|
|
2708
|
-
|
|
2709
|
-
this.config.
|
|
2874
|
+
emitEvent: (event) => {
|
|
2875
|
+
this.config.onSessionEvent?.(chatId, event);
|
|
2876
|
+
},
|
|
2877
|
+
reportSessionCompletion: () => {
|
|
2878
|
+
this.config.onSessionCompletion?.(chatId);
|
|
2710
2879
|
}
|
|
2711
2880
|
};
|
|
2712
2881
|
}
|
|
@@ -2850,7 +3019,8 @@ var AgentSlot = class {
|
|
|
2850
3019
|
agentConfigCache: this.agentConfigCache,
|
|
2851
3020
|
onStateChange: (chatId, state) => this.reportSessionState(chatId, state),
|
|
2852
3021
|
onRuntimeStateChange: (state) => this.reportRuntimeState(state),
|
|
2853
|
-
|
|
3022
|
+
onSessionEvent: (chatId, event) => this.reportSessionEvent(chatId, event),
|
|
3023
|
+
onSessionCompletion: (chatId) => this.reportSessionCompletion(chatId)
|
|
2854
3024
|
});
|
|
2855
3025
|
const onCommand = (cmd) => {
|
|
2856
3026
|
if (cmd.agentId === this.config.agentId && this.sessionManager) this.sessionManager.handleCommand(cmd.chatId, cmd.type).catch((err) => {
|
|
@@ -2884,8 +3054,11 @@ var AgentSlot = class {
|
|
|
2884
3054
|
reportRuntimeState(state) {
|
|
2885
3055
|
this.clientConnection.reportRuntimeState(this.config.agentId, state);
|
|
2886
3056
|
}
|
|
2887
|
-
|
|
2888
|
-
this.clientConnection.
|
|
3057
|
+
reportSessionEvent(chatId, event) {
|
|
3058
|
+
this.clientConnection.reportSessionEvent(this.config.agentId, chatId, event);
|
|
3059
|
+
}
|
|
3060
|
+
reportSessionCompletion(chatId) {
|
|
3061
|
+
this.clientConnection.reportSessionCompletion(this.config.agentId, chatId);
|
|
2889
3062
|
}
|
|
2890
3063
|
fullStateSync() {
|
|
2891
3064
|
if (!this.sessionManager) return;
|
|
@@ -3799,7 +3972,7 @@ async function onboardCreate(args) {
|
|
|
3799
3972
|
}
|
|
3800
3973
|
const runtimeAgent = args.type === "human" ? args.assistant : args.id;
|
|
3801
3974
|
if (args.feishuBotAppId && args.feishuBotAppSecret) {
|
|
3802
|
-
const { bindFeishuBot } = await import("./feishu-
|
|
3975
|
+
const { bindFeishuBot } = await import("./feishu-CJ08ntOD.mjs").then((n) => n.r);
|
|
3803
3976
|
const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
|
|
3804
3977
|
if (!targetAgentUuid) process.stderr.write(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
|
|
3805
3978
|
else {
|
|
@@ -3940,7 +4113,7 @@ function setNestedByDot(obj, dotPath, value) {
|
|
|
3940
4113
|
if (lastKey !== void 0) current[lastKey] = value;
|
|
3941
4114
|
}
|
|
3942
4115
|
//#endregion
|
|
3943
|
-
//#region ../server/dist/app-
|
|
4116
|
+
//#region ../server/dist/app-CWKBBGod.mjs
|
|
3944
4117
|
var __defProp = Object.defineProperty;
|
|
3945
4118
|
var __exportAll = (all, no_symbols) => {
|
|
3946
4119
|
let target = {};
|
|
@@ -4475,8 +4648,17 @@ function parseId$1(raw) {
|
|
|
4475
4648
|
return id;
|
|
4476
4649
|
}
|
|
4477
4650
|
async function adminAdapterMappingRoutes(app) {
|
|
4478
|
-
app.get("/", async () => {
|
|
4479
|
-
|
|
4651
|
+
app.get("/", async (request) => {
|
|
4652
|
+
const scope = memberScope(request);
|
|
4653
|
+
return (await app.db.select({
|
|
4654
|
+
id: adapterAgentMappings.id,
|
|
4655
|
+
platform: adapterAgentMappings.platform,
|
|
4656
|
+
externalUserId: adapterAgentMappings.externalUserId,
|
|
4657
|
+
agentId: adapterAgentMappings.agentId,
|
|
4658
|
+
boundVia: adapterAgentMappings.boundVia,
|
|
4659
|
+
displayName: adapterAgentMappings.displayName,
|
|
4660
|
+
createdAt: adapterAgentMappings.createdAt
|
|
4661
|
+
}).from(adapterAgentMappings).innerJoin(agents, eq(agents.uuid, adapterAgentMappings.agentId)).where(eq(agents.organizationId, scope.organizationId)).orderBy(desc(adapterAgentMappings.createdAt))).map((r) => ({
|
|
4480
4662
|
id: r.id,
|
|
4481
4663
|
platform: r.platform,
|
|
4482
4664
|
externalUserId: r.externalUserId,
|
|
@@ -4903,8 +5085,23 @@ async function createAgent(db, data) {
|
|
|
4903
5085
|
const name = data.name ?? null;
|
|
4904
5086
|
if (name?.startsWith(RESERVED_AGENT_NAME_PREFIX)) throw new BadRequestError(`Agent name "${name}" is reserved — names starting with "${RESERVED_AGENT_NAME_PREFIX}" are Hub-internal`);
|
|
4905
5087
|
const inboxId = `inbox_${uuid}`;
|
|
4906
|
-
|
|
4907
|
-
|
|
5088
|
+
let orgId;
|
|
5089
|
+
let managerId;
|
|
5090
|
+
if (data.managerId && data.organizationId) {
|
|
5091
|
+
orgId = data.organizationId;
|
|
5092
|
+
managerId = data.managerId;
|
|
5093
|
+
} else if (data.managerId) {
|
|
5094
|
+
const [manager] = await db.select({
|
|
5095
|
+
id: members.id,
|
|
5096
|
+
organizationId: members.organizationId
|
|
5097
|
+
}).from(members).where(eq(members.id, data.managerId)).limit(1);
|
|
5098
|
+
if (!manager) throw new BadRequestError(`Manager "${data.managerId}" not found`);
|
|
5099
|
+
orgId = manager.organizationId;
|
|
5100
|
+
managerId = manager.id;
|
|
5101
|
+
} else {
|
|
5102
|
+
orgId = data.organizationId ?? await resolveDefaultOrgId(db);
|
|
5103
|
+
managerId = await resolveFallbackManagerId(db, orgId);
|
|
5104
|
+
}
|
|
4908
5105
|
const clientId = await resolveAgentClient(db, {
|
|
4909
5106
|
clientId: data.clientId,
|
|
4910
5107
|
managerId,
|
|
@@ -5378,14 +5575,20 @@ async function unbindAgent(db, agentId) {
|
|
|
5378
5575
|
...runtimeFieldsReset(now)
|
|
5379
5576
|
}).where(eq(agentPresence.agentId, agentId));
|
|
5380
5577
|
}
|
|
5381
|
-
/** Set runtime state directly from client-reported value.
|
|
5382
|
-
|
|
5578
|
+
/** Set runtime state directly from client-reported value.
|
|
5579
|
+
*
|
|
5580
|
+
* When an org-scoped notifier is provided, emit a PG NOTIFY on the
|
|
5581
|
+
* `runtime_state_changes` channel so the pulse aggregator (and any future
|
|
5582
|
+
* admin-side consumers) can observe the transition. Fire-and-forget to match
|
|
5583
|
+
* notifier semantics elsewhere in this module. */
|
|
5584
|
+
async function setRuntimeState(db, agentId, runtimeState, options) {
|
|
5383
5585
|
const now = /* @__PURE__ */ new Date();
|
|
5384
5586
|
await db.update(agentPresence).set({
|
|
5385
5587
|
runtimeState,
|
|
5386
5588
|
runtimeUpdatedAt: now,
|
|
5387
5589
|
lastSeenAt: now
|
|
5388
5590
|
}).where(eq(agentPresence.agentId, agentId));
|
|
5591
|
+
if (options?.notifier && options.organizationId) options.notifier.notifyRuntimeStateChange(agentId, runtimeState, options.organizationId).catch(() => {});
|
|
5389
5592
|
}
|
|
5390
5593
|
/** Touch agent last_seen_at on heartbeat (per-agent liveness). */
|
|
5391
5594
|
async function touchAgent(db, agentId) {
|
|
@@ -5511,9 +5714,7 @@ async function getClient(db, clientId) {
|
|
|
5511
5714
|
return row ?? null;
|
|
5512
5715
|
}
|
|
5513
5716
|
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));
|
|
5717
|
+
const rows = await db.select().from(clients).where(eq(clients.userId, userId));
|
|
5517
5718
|
const counts = await db.select({
|
|
5518
5719
|
clientId: agents.clientId,
|
|
5519
5720
|
count: sql`count(*)::int`
|
|
@@ -5778,13 +5979,16 @@ async function listMessages(db, chatId, limit, cursor) {
|
|
|
5778
5979
|
const INBOX_CHANNEL = "inbox_notifications";
|
|
5779
5980
|
const CONFIG_CHANNEL = "config_changes";
|
|
5780
5981
|
const SESSION_STATE_CHANNEL = "session_state_changes";
|
|
5982
|
+
const RUNTIME_STATE_CHANNEL = "runtime_state_changes";
|
|
5781
5983
|
function createNotifier(listenClient) {
|
|
5782
5984
|
const subscriptions = /* @__PURE__ */ new Map();
|
|
5783
5985
|
const configChangeHandlers = [];
|
|
5784
5986
|
const sessionStateChangeHandlers = [];
|
|
5987
|
+
const runtimeStateChangeHandlers = [];
|
|
5785
5988
|
let unlistenInboxFn = null;
|
|
5786
5989
|
let unlistenConfigFn = null;
|
|
5787
5990
|
let unlistenSessionStateFn = null;
|
|
5991
|
+
let unlistenRuntimeStateFn = null;
|
|
5788
5992
|
function handleNotification(payload) {
|
|
5789
5993
|
const sepIdx = payload.indexOf(":");
|
|
5790
5994
|
if (sepIdx === -1) return;
|
|
@@ -5825,9 +6029,14 @@ function createNotifier(listenClient) {
|
|
|
5825
6029
|
await listenClient`SELECT pg_notify(${CONFIG_CHANNEL}, ${configType})`;
|
|
5826
6030
|
} catch {}
|
|
5827
6031
|
},
|
|
5828
|
-
async notifySessionStateChange(agentId, chatId, state) {
|
|
6032
|
+
async notifySessionStateChange(agentId, chatId, state, organizationId) {
|
|
5829
6033
|
try {
|
|
5830
|
-
await listenClient`SELECT pg_notify(${SESSION_STATE_CHANNEL}, ${`${agentId}:${chatId}:${state}`})`;
|
|
6034
|
+
await listenClient`SELECT pg_notify(${SESSION_STATE_CHANNEL}, ${`${agentId}:${chatId}:${state}:${organizationId}`})`;
|
|
6035
|
+
} catch {}
|
|
6036
|
+
},
|
|
6037
|
+
async notifyRuntimeStateChange(agentId, state, organizationId) {
|
|
6038
|
+
try {
|
|
6039
|
+
await listenClient`SELECT pg_notify(${RUNTIME_STATE_CHANNEL}, ${`${agentId}:${state}:${organizationId}`})`;
|
|
5831
6040
|
} catch {}
|
|
5832
6041
|
},
|
|
5833
6042
|
onConfigChange(handler) {
|
|
@@ -5836,6 +6045,9 @@ function createNotifier(listenClient) {
|
|
|
5836
6045
|
onSessionStateChange(handler) {
|
|
5837
6046
|
sessionStateChangeHandlers.push(handler);
|
|
5838
6047
|
},
|
|
6048
|
+
onRuntimeStateChange(handler) {
|
|
6049
|
+
runtimeStateChangeHandlers.push(handler);
|
|
6050
|
+
},
|
|
5839
6051
|
async start() {
|
|
5840
6052
|
unlistenInboxFn = (await listenClient.listen(INBOX_CHANNEL, (payload) => {
|
|
5841
6053
|
if (payload) handleNotification(payload);
|
|
@@ -5847,14 +6059,33 @@ function createNotifier(listenClient) {
|
|
|
5847
6059
|
if (payload) {
|
|
5848
6060
|
const firstSep = payload.indexOf(":");
|
|
5849
6061
|
const secondSep = payload.indexOf(":", firstSep + 1);
|
|
5850
|
-
|
|
6062
|
+
const thirdSep = payload.indexOf(":", secondSep + 1);
|
|
6063
|
+
if (firstSep > 0 && secondSep > firstSep && thirdSep > secondSep) {
|
|
5851
6064
|
const agentId = payload.slice(0, firstSep);
|
|
5852
6065
|
const chatId = payload.slice(firstSep + 1, secondSep);
|
|
5853
|
-
const state = payload.slice(secondSep + 1);
|
|
6066
|
+
const state = payload.slice(secondSep + 1, thirdSep);
|
|
6067
|
+
const organizationId = payload.slice(thirdSep + 1);
|
|
5854
6068
|
for (const handler of sessionStateChangeHandlers) handler({
|
|
5855
6069
|
agentId,
|
|
5856
6070
|
chatId,
|
|
5857
|
-
state
|
|
6071
|
+
state,
|
|
6072
|
+
organizationId
|
|
6073
|
+
});
|
|
6074
|
+
}
|
|
6075
|
+
}
|
|
6076
|
+
})).unlisten;
|
|
6077
|
+
unlistenRuntimeStateFn = (await listenClient.listen(RUNTIME_STATE_CHANNEL, (payload) => {
|
|
6078
|
+
if (payload) {
|
|
6079
|
+
const firstSep = payload.indexOf(":");
|
|
6080
|
+
const secondSep = payload.indexOf(":", firstSep + 1);
|
|
6081
|
+
if (firstSep > 0 && secondSep > firstSep) {
|
|
6082
|
+
const agentId = payload.slice(0, firstSep);
|
|
6083
|
+
const state = payload.slice(firstSep + 1, secondSep);
|
|
6084
|
+
const organizationId = payload.slice(secondSep + 1);
|
|
6085
|
+
for (const handler of runtimeStateChangeHandlers) handler({
|
|
6086
|
+
agentId,
|
|
6087
|
+
state,
|
|
6088
|
+
organizationId
|
|
5858
6089
|
});
|
|
5859
6090
|
}
|
|
5860
6091
|
}
|
|
@@ -5873,6 +6104,10 @@ function createNotifier(listenClient) {
|
|
|
5873
6104
|
await unlistenSessionStateFn();
|
|
5874
6105
|
unlistenSessionStateFn = null;
|
|
5875
6106
|
}
|
|
6107
|
+
if (unlistenRuntimeStateFn) {
|
|
6108
|
+
await unlistenRuntimeStateFn();
|
|
6109
|
+
unlistenRuntimeStateFn = null;
|
|
6110
|
+
}
|
|
5876
6111
|
}
|
|
5877
6112
|
};
|
|
5878
6113
|
}
|
|
@@ -6304,8 +6539,8 @@ const agentChatSessions = pgTable("agent_chat_sessions", {
|
|
|
6304
6539
|
state: text("state").notNull(),
|
|
6305
6540
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
6306
6541
|
}, (table) => [primaryKey({ columns: [table.agentId, table.chatId] })]);
|
|
6307
|
-
/** Upsert a session state
|
|
6308
|
-
async function upsertSessionState(db, agentId, chatId, state, notifier) {
|
|
6542
|
+
/** Upsert a session state, refresh materialized aggregates on agent_presence, and emit org-scoped NOTIFY. */
|
|
6543
|
+
async function upsertSessionState(db, agentId, chatId, state, organizationId, notifier) {
|
|
6309
6544
|
const now = /* @__PURE__ */ new Date();
|
|
6310
6545
|
await db.transaction(async (tx) => {
|
|
6311
6546
|
await tx.insert(agentChatSessions).values({
|
|
@@ -6332,7 +6567,7 @@ async function upsertSessionState(db, agentId, chatId, state, notifier) {
|
|
|
6332
6567
|
lastSeenAt: now
|
|
6333
6568
|
}).where(eq(agentPresence.agentId, agentId));
|
|
6334
6569
|
});
|
|
6335
|
-
if (notifier) notifier.notifySessionStateChange(agentId, chatId, state).catch(() => {});
|
|
6570
|
+
if (notifier) notifier.notifySessionStateChange(agentId, chatId, state, organizationId).catch(() => {});
|
|
6336
6571
|
}
|
|
6337
6572
|
async function resetActivity(db, agentId) {
|
|
6338
6573
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -6381,7 +6616,8 @@ async function listAgentsWithRuntime(db, scope) {
|
|
|
6381
6616
|
runtimeState: agentPresence.runtimeState,
|
|
6382
6617
|
activeSessions: agentPresence.activeSessions,
|
|
6383
6618
|
totalSessions: agentPresence.totalSessions,
|
|
6384
|
-
runtimeUpdatedAt: agentPresence.runtimeUpdatedAt
|
|
6619
|
+
runtimeUpdatedAt: agentPresence.runtimeUpdatedAt,
|
|
6620
|
+
type: agents.type
|
|
6385
6621
|
}).from(agentPresence).innerJoin(agents, eq(agentPresence.agentId, agents.uuid)).where(and(isNotNull(agentPresence.runtimeState), agentVisibilityCondition(scope)));
|
|
6386
6622
|
}
|
|
6387
6623
|
/**
|
|
@@ -6470,7 +6706,8 @@ async function adminActivityRoutes(app) {
|
|
|
6470
6706
|
runtimeState: a.runtimeState,
|
|
6471
6707
|
activeSessions: a.activeSessions,
|
|
6472
6708
|
totalSessions: a.totalSessions,
|
|
6473
|
-
runtimeUpdatedAt: a.runtimeUpdatedAt?.toISOString() ?? null
|
|
6709
|
+
runtimeUpdatedAt: a.runtimeUpdatedAt?.toISOString() ?? null,
|
|
6710
|
+
type: "type" in a ? a.type : null
|
|
6474
6711
|
}))
|
|
6475
6712
|
};
|
|
6476
6713
|
});
|
|
@@ -6499,6 +6736,16 @@ const notifications = pgTable("notifications", {
|
|
|
6499
6736
|
index("idx_notifications_agent").on(table.agentId),
|
|
6500
6737
|
index("idx_notifications_org_read").on(table.organizationId, table.read)
|
|
6501
6738
|
]);
|
|
6739
|
+
let registered = null;
|
|
6740
|
+
function registerAdminBroadcaster(fn) {
|
|
6741
|
+
registered = fn;
|
|
6742
|
+
}
|
|
6743
|
+
function broadcastToAdmins(payload) {
|
|
6744
|
+
if (!registered) return;
|
|
6745
|
+
try {
|
|
6746
|
+
registered(payload);
|
|
6747
|
+
} catch {}
|
|
6748
|
+
}
|
|
6502
6749
|
/** Runtime system configuration (key-value JSONB). Dynamically modifiable via Admin API; controls inbox timeout, retry count, etc. */
|
|
6503
6750
|
const systemConfigs = pgTable("system_configs", {
|
|
6504
6751
|
key: text("key").primaryKey(),
|
|
@@ -6531,15 +6778,6 @@ async function updateConfigs(db, updates) {
|
|
|
6531
6778
|
});
|
|
6532
6779
|
return getAllConfigs(db);
|
|
6533
6780
|
}
|
|
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
6781
|
/** Create a notification, persist it, and fire-and-forget push to all channels. */
|
|
6544
6782
|
async function createNotification(db, data) {
|
|
6545
6783
|
const id = uuidv7();
|
|
@@ -6623,13 +6861,11 @@ async function notifyAgentEvent(db, agentId, type, severity, message, chatId) {
|
|
|
6623
6861
|
} catch {}
|
|
6624
6862
|
}
|
|
6625
6863
|
function pushToAdminWs(notification) {
|
|
6626
|
-
|
|
6627
|
-
|
|
6628
|
-
|
|
6629
|
-
|
|
6630
|
-
|
|
6631
|
-
});
|
|
6632
|
-
} catch {}
|
|
6864
|
+
broadcastToAdmins({
|
|
6865
|
+
type: "notification",
|
|
6866
|
+
organizationId: notification.organizationId,
|
|
6867
|
+
data: notification
|
|
6868
|
+
});
|
|
6633
6869
|
}
|
|
6634
6870
|
async function pushToWebhook(db, notification) {
|
|
6635
6871
|
const webhookUrl = await getConfig(db, "notification_webhook_url");
|
|
@@ -6831,46 +7067,97 @@ async function filterSessionsByParticipant(db, sessions, participantAgentId) {
|
|
|
6831
7067
|
return sessions.filter((s) => allowedChatIds.has(s.chatId));
|
|
6832
7068
|
}
|
|
6833
7069
|
/**
|
|
6834
|
-
* Session
|
|
6835
|
-
*
|
|
6836
|
-
*
|
|
7070
|
+
* Session events — structured event stream per (agent, chat) session.
|
|
7071
|
+
* `kind` is 'tool_call' | 'error'; the payload shape is enforced by the
|
|
7072
|
+
* service layer via Zod (no FK / CHECK on this table per project rule).
|
|
7073
|
+
*
|
|
7074
|
+
* `seq` is monotonic per (agent_id, chat_id). The single-writer invariant
|
|
7075
|
+
* in the client-side session-manager guarantees ordering; the service wraps
|
|
7076
|
+
* the insert in a MAX(seq)+1 retry loop to recover from restart-overlap
|
|
7077
|
+
* windows.
|
|
7078
|
+
*
|
|
7079
|
+
* Cleanup: rows are dropped when the session is evicted or terminated —
|
|
7080
|
+
* see sessionEventService.clearEvents.
|
|
6837
7081
|
*/
|
|
6838
|
-
const
|
|
7082
|
+
const sessionEvents = pgTable("session_events", {
|
|
6839
7083
|
id: text("id").primaryKey(),
|
|
6840
7084
|
agentId: text("agent_id").notNull(),
|
|
6841
7085
|
chatId: text("chat_id").notNull(),
|
|
6842
|
-
|
|
6843
|
-
|
|
6844
|
-
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
|
|
6850
|
-
|
|
6851
|
-
|
|
6852
|
-
|
|
6853
|
-
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
|
|
6858
|
-
|
|
6859
|
-
|
|
6860
|
-
});
|
|
7086
|
+
seq: integer("seq").notNull(),
|
|
7087
|
+
kind: text("kind").notNull(),
|
|
7088
|
+
payload: jsonb("payload").notNull(),
|
|
7089
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
7090
|
+
}, (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())]);
|
|
7091
|
+
const DEFAULT_LIMIT = 200;
|
|
7092
|
+
const MAX_LIMIT = 1e3;
|
|
7093
|
+
const MAX_SEQ_RETRIES = 3;
|
|
7094
|
+
function rowToEvent(row) {
|
|
7095
|
+
return {
|
|
7096
|
+
id: row.id,
|
|
7097
|
+
agentId: row.agentId,
|
|
7098
|
+
chatId: row.chatId,
|
|
7099
|
+
seq: row.seq,
|
|
7100
|
+
kind: row.kind,
|
|
7101
|
+
payload: row.payload,
|
|
7102
|
+
createdAt: row.createdAt.toISOString()
|
|
7103
|
+
};
|
|
6861
7104
|
}
|
|
6862
|
-
/**
|
|
6863
|
-
async function
|
|
6864
|
-
const
|
|
6865
|
-
|
|
6866
|
-
|
|
6867
|
-
|
|
6868
|
-
|
|
7105
|
+
/** Append one event; throws after MAX_SEQ_RETRIES on persistent seq contention. */
|
|
7106
|
+
async function appendEvent(db, agentId, chatId, event) {
|
|
7107
|
+
const validated = sessionEventSchema$1.parse(event);
|
|
7108
|
+
for (let attempt = 0; attempt < MAX_SEQ_RETRIES; attempt++) {
|
|
7109
|
+
const id = uuidv7();
|
|
7110
|
+
const payloadJson = JSON.stringify(validated.payload);
|
|
7111
|
+
const row = (await db.execute(sql`
|
|
7112
|
+
INSERT INTO session_events (id, agent_id, chat_id, seq, kind, payload)
|
|
7113
|
+
SELECT ${id}, ${agentId}, ${chatId},
|
|
7114
|
+
COALESCE(MAX(seq), 0) + 1, ${validated.kind}, ${payloadJson}::jsonb
|
|
7115
|
+
FROM session_events
|
|
7116
|
+
WHERE agent_id = ${agentId} AND chat_id = ${chatId}
|
|
7117
|
+
ON CONFLICT (agent_id, chat_id, seq) DO NOTHING
|
|
7118
|
+
RETURNING id, agent_id, chat_id, seq, kind, payload, created_at
|
|
7119
|
+
`))[0];
|
|
7120
|
+
if (row) return rowToEvent({
|
|
7121
|
+
id: row.id,
|
|
7122
|
+
agentId: row.agent_id,
|
|
7123
|
+
chatId: row.chat_id,
|
|
7124
|
+
seq: row.seq,
|
|
7125
|
+
kind: row.kind,
|
|
7126
|
+
payload: row.payload,
|
|
7127
|
+
createdAt: row.created_at instanceof Date ? row.created_at : new Date(row.created_at)
|
|
7128
|
+
});
|
|
7129
|
+
}
|
|
7130
|
+
throw new Error(`session_events seq contention on ${agentId}/${chatId}`);
|
|
7131
|
+
}
|
|
7132
|
+
/**
|
|
7133
|
+
* List events for a session in `seq asc` order with cursor pagination.
|
|
7134
|
+
* `cursor` is the last seen `seq`; pass it as-is on the next page.
|
|
7135
|
+
*/
|
|
7136
|
+
async function listEvents(db, agentId, chatId, options) {
|
|
7137
|
+
const limit = Math.min(Math.max(options?.limit ?? DEFAULT_LIMIT, 1), MAX_LIMIT);
|
|
7138
|
+
const conditions = [eq(sessionEvents.agentId, agentId), eq(sessionEvents.chatId, chatId)];
|
|
7139
|
+
if (options?.cursor !== void 0) conditions.push(gt(sessionEvents.seq, options.cursor));
|
|
7140
|
+
const rows = await db.select({
|
|
7141
|
+
id: sessionEvents.id,
|
|
7142
|
+
agentId: sessionEvents.agentId,
|
|
7143
|
+
chatId: sessionEvents.chatId,
|
|
7144
|
+
seq: sessionEvents.seq,
|
|
7145
|
+
kind: sessionEvents.kind,
|
|
7146
|
+
payload: sessionEvents.payload,
|
|
7147
|
+
createdAt: sessionEvents.createdAt
|
|
7148
|
+
}).from(sessionEvents).where(and(...conditions)).orderBy(asc(sessionEvents.seq)).limit(limit + 1);
|
|
7149
|
+
const hasMore = rows.length > limit;
|
|
7150
|
+
const items = (hasMore ? rows.slice(0, limit) : rows).map(rowToEvent);
|
|
7151
|
+
const last = items[items.length - 1];
|
|
6869
7152
|
return {
|
|
6870
|
-
|
|
6871
|
-
|
|
7153
|
+
items,
|
|
7154
|
+
nextCursor: hasMore && last ? last.seq : null
|
|
6872
7155
|
};
|
|
6873
7156
|
}
|
|
7157
|
+
/** Delete all events for a session — called on eviction / termination. */
|
|
7158
|
+
async function clearEvents(db, agentId, chatId) {
|
|
7159
|
+
await db.delete(sessionEvents).where(and(eq(sessionEvents.agentId, agentId), eq(sessionEvents.chatId, chatId)));
|
|
7160
|
+
}
|
|
6874
7161
|
const sessionFilterSchema = z.object({
|
|
6875
7162
|
state: z.enum([
|
|
6876
7163
|
"active",
|
|
@@ -6916,16 +7203,22 @@ async function adminSessionRoutes(app) {
|
|
|
6916
7203
|
});
|
|
6917
7204
|
/** GET /admin/sessions/agents/:agentId/:chatId — single session detail */
|
|
6918
7205
|
app.get("/agents/:agentId/:chatId", async (request) => {
|
|
6919
|
-
|
|
7206
|
+
const scope = memberScope(request);
|
|
7207
|
+
await assertAgentVisible(app.db, scope, request.params.agentId);
|
|
7208
|
+
await assertChatAccess(app.db, scope, request.params.chatId);
|
|
6920
7209
|
return getSession(app.db, request.params.agentId, request.params.chatId);
|
|
6921
7210
|
});
|
|
6922
|
-
/** GET /admin/sessions/agents/:agentId/:chatId/
|
|
6923
|
-
app.get("/agents/:agentId/:chatId/
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
|
|
7211
|
+
/** GET /admin/sessions/agents/:agentId/:chatId/events — session event stream, paged by `seq` */
|
|
7212
|
+
app.get("/agents/:agentId/:chatId/events", async (request) => {
|
|
7213
|
+
const scope = memberScope(request);
|
|
7214
|
+
await assertAgentVisible(app.db, scope, request.params.agentId);
|
|
7215
|
+
await assertChatAccess(app.db, scope, request.params.chatId);
|
|
7216
|
+
const limit = request.query.limit !== void 0 ? Number.parseInt(request.query.limit, 10) : void 0;
|
|
7217
|
+
const cursor = request.query.cursor !== void 0 ? Number.parseInt(request.query.cursor, 10) : void 0;
|
|
7218
|
+
return listEvents(app.db, request.params.agentId, request.params.chatId, {
|
|
7219
|
+
limit: Number.isFinite(limit) ? limit : void 0,
|
|
7220
|
+
cursor: Number.isFinite(cursor) ? cursor : void 0
|
|
7221
|
+
});
|
|
6929
7222
|
});
|
|
6930
7223
|
/** POST /admin/sessions/agents/:agentId/:chatId/suspend — suspend a session */
|
|
6931
7224
|
app.post("/agents/:agentId/:chatId/suspend", async (request, reply) => {
|
|
@@ -7532,30 +7825,40 @@ async function adminTaskRoutes(app) {
|
|
|
7532
7825
|
return getTaskHealth(app.db, request.params.taskId);
|
|
7533
7826
|
});
|
|
7534
7827
|
}
|
|
7535
|
-
|
|
7536
|
-
|
|
7537
|
-
|
|
7538
|
-
|
|
7539
|
-
|
|
7540
|
-
|
|
7541
|
-
|
|
7542
|
-
|
|
7543
|
-
|
|
7828
|
+
async function loadVisibleAgentIds(db, organizationId, memberId) {
|
|
7829
|
+
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))));
|
|
7830
|
+
return new Set(rows.map((r) => r.id));
|
|
7831
|
+
}
|
|
7832
|
+
function filterPulseAgents(agentsMap, visible) {
|
|
7833
|
+
const out = {};
|
|
7834
|
+
for (const [agentId, buckets] of Object.entries(agentsMap)) if (visible.has(agentId)) out[agentId] = buckets;
|
|
7835
|
+
return out;
|
|
7836
|
+
}
|
|
7544
7837
|
function adminWsRoutes(notifier, jwtSecret) {
|
|
7545
7838
|
const adminSockets = /* @__PURE__ */ new Map();
|
|
7546
7839
|
const secret = new TextEncoder().encode(jwtSecret);
|
|
7547
|
-
|
|
7840
|
+
function broadcastOrgScoped(payload) {
|
|
7548
7841
|
const orgId = payload.organizationId;
|
|
7549
|
-
|
|
7550
|
-
|
|
7551
|
-
|
|
7842
|
+
if (typeof orgId !== "string" || orgId.length === 0) return;
|
|
7843
|
+
const isPulseTick = payload.type === "pulse:tick" && typeof payload.agents === "object" && payload.agents !== null;
|
|
7844
|
+
const sharedData = isPulseTick ? null : JSON.stringify(payload);
|
|
7845
|
+
for (const [ws, meta] of adminSockets) {
|
|
7846
|
+
if (ws.readyState !== 1 || meta.organizationId !== orgId) continue;
|
|
7847
|
+
if (isPulseTick) {
|
|
7848
|
+
const filtered = filterPulseAgents(payload.agents, meta.visibleAgentIds);
|
|
7849
|
+
ws.send(JSON.stringify({
|
|
7850
|
+
...payload,
|
|
7851
|
+
agents: filtered
|
|
7852
|
+
}));
|
|
7853
|
+
} else ws.send(sharedData);
|
|
7854
|
+
}
|
|
7855
|
+
}
|
|
7856
|
+
registerAdminBroadcaster(broadcastOrgScoped);
|
|
7552
7857
|
notifier.onSessionStateChange((payload) => {
|
|
7553
|
-
|
|
7858
|
+
broadcastOrgScoped({
|
|
7554
7859
|
type: "session:state",
|
|
7555
7860
|
...payload
|
|
7556
7861
|
});
|
|
7557
|
-
const orgId = payload.organizationId;
|
|
7558
|
-
for (const [ws, meta] of adminSockets) if (ws.readyState === 1 && (!orgId || meta.organizationId === orgId)) ws.send(data);
|
|
7559
7862
|
});
|
|
7560
7863
|
return async (app) => {
|
|
7561
7864
|
app.get("/admin", { websocket: true }, async (socket, request) => {
|
|
@@ -7569,9 +7872,10 @@ function adminWsRoutes(notifier, jwtSecret) {
|
|
|
7569
7872
|
return;
|
|
7570
7873
|
}
|
|
7571
7874
|
let organizationId;
|
|
7875
|
+
let memberId;
|
|
7572
7876
|
try {
|
|
7573
7877
|
const { payload } = await jwtVerify(token, secret);
|
|
7574
|
-
if (payload.type !== "access" || !payload.sub ||
|
|
7878
|
+
if (payload.type !== "access" || !payload.sub || typeof payload.organizationId !== "string" || typeof payload.memberId !== "string") {
|
|
7575
7879
|
socket.send(JSON.stringify({
|
|
7576
7880
|
type: "error",
|
|
7577
7881
|
message: "Invalid token type"
|
|
@@ -7580,6 +7884,7 @@ function adminWsRoutes(notifier, jwtSecret) {
|
|
|
7580
7884
|
return;
|
|
7581
7885
|
}
|
|
7582
7886
|
organizationId = payload.organizationId;
|
|
7887
|
+
memberId = payload.memberId;
|
|
7583
7888
|
} catch {
|
|
7584
7889
|
socket.send(JSON.stringify({
|
|
7585
7890
|
type: "error",
|
|
@@ -7588,7 +7893,12 @@ function adminWsRoutes(notifier, jwtSecret) {
|
|
|
7588
7893
|
socket.close(4001, "Auth failed");
|
|
7589
7894
|
return;
|
|
7590
7895
|
}
|
|
7591
|
-
|
|
7896
|
+
const visibleAgentIds = await loadVisibleAgentIds(app.db, organizationId, memberId);
|
|
7897
|
+
adminSockets.set(socket, {
|
|
7898
|
+
organizationId,
|
|
7899
|
+
memberId,
|
|
7900
|
+
visibleAgentIds
|
|
7901
|
+
});
|
|
7592
7902
|
socket.send(JSON.stringify({ type: "admin:connected" }));
|
|
7593
7903
|
socket.on("close", () => {
|
|
7594
7904
|
adminSockets.delete(socket);
|
|
@@ -8072,7 +8382,7 @@ const wsMessageSchema = z.object({
|
|
|
8072
8382
|
* Failure ⇒ server sends `auth:rejected` and closes (code 4401).
|
|
8073
8383
|
* 2. `client:register` — bind the client_id to the authenticated user.
|
|
8074
8384
|
* 3. `agent:bind` — run Rule R-RUN (no token); populate presence.
|
|
8075
|
-
* 4. `session:state` / `runtime:state` / `session:
|
|
8385
|
+
* 4. `session:state` / `runtime:state` / `session:event` / `session:completion` / `heartbeat`.
|
|
8076
8386
|
* 5. `agent:unbind` — stop multiplexing for a specific agent.
|
|
8077
8387
|
*
|
|
8078
8388
|
* When the JWT is about to expire the server sends `auth:expired` so the
|
|
@@ -8107,6 +8417,15 @@ function clientWsRoutes(notifier, instanceId) {
|
|
|
8107
8417
|
let clientId = null;
|
|
8108
8418
|
let authExpiryTimer = null;
|
|
8109
8419
|
const boundAgents = /* @__PURE__ */ new Map();
|
|
8420
|
+
const sessionOpQueues = /* @__PURE__ */ new Map();
|
|
8421
|
+
function chainSessionOp(agentId, chatId, op) {
|
|
8422
|
+
const key = `${agentId}:${chatId}`;
|
|
8423
|
+
const next = (sessionOpQueues.get(key) ?? Promise.resolve()).then(op, op);
|
|
8424
|
+
sessionOpQueues.set(key, next.finally(() => {
|
|
8425
|
+
if (sessionOpQueues.get(key) === next) sessionOpQueues.delete(key);
|
|
8426
|
+
}));
|
|
8427
|
+
return next;
|
|
8428
|
+
}
|
|
8110
8429
|
const authTimeout = setTimeout(() => {
|
|
8111
8430
|
if (!session) {
|
|
8112
8431
|
try {
|
|
@@ -8331,7 +8650,8 @@ function clientWsRoutes(notifier, instanceId) {
|
|
|
8331
8650
|
return;
|
|
8332
8651
|
}
|
|
8333
8652
|
const payload = sessionStateMessageSchema.parse(msg);
|
|
8334
|
-
|
|
8653
|
+
if (payload.state === "evicted") chainSessionOp(agentId, payload.chatId, () => clearEvents(app.db, agentId, payload.chatId).catch(() => {}));
|
|
8654
|
+
await upsertSessionState(app.db, agentId, payload.chatId, payload.state, session.organizationId, notifier);
|
|
8335
8655
|
} else if (type === "runtime:state") {
|
|
8336
8656
|
const agentId = parsed.data.agentId;
|
|
8337
8657
|
if (!agentId || !boundAgents.has(agentId)) {
|
|
@@ -8342,10 +8662,33 @@ function clientWsRoutes(notifier, instanceId) {
|
|
|
8342
8662
|
return;
|
|
8343
8663
|
}
|
|
8344
8664
|
const payload = runtimeStateMessageSchema.parse(msg);
|
|
8345
|
-
await setRuntimeState(app.db, agentId, payload.runtimeState
|
|
8665
|
+
await setRuntimeState(app.db, agentId, payload.runtimeState, {
|
|
8666
|
+
organizationId: session.organizationId,
|
|
8667
|
+
notifier
|
|
8668
|
+
});
|
|
8346
8669
|
if (payload.runtimeState === "error" && shouldNotify(agentId, "agent_error")) notifyAgentEvent(app.db, agentId, "agent_error", "high", `Agent ${agentId} entered error state`).catch(() => {});
|
|
8347
8670
|
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:
|
|
8671
|
+
} else if (type === "session:event") {
|
|
8672
|
+
const agentId = parsed.data.agentId;
|
|
8673
|
+
if (!agentId || !boundAgents.has(agentId)) {
|
|
8674
|
+
socket.send(JSON.stringify({
|
|
8675
|
+
type: "error",
|
|
8676
|
+
message: "Agent not bound"
|
|
8677
|
+
}));
|
|
8678
|
+
return;
|
|
8679
|
+
}
|
|
8680
|
+
const payload = sessionEventMessageSchema.parse(msg);
|
|
8681
|
+
chainSessionOp(agentId, payload.chatId, async () => {
|
|
8682
|
+
try {
|
|
8683
|
+
await appendEvent(app.db, agentId, payload.chatId, payload.event);
|
|
8684
|
+
} catch (err) {
|
|
8685
|
+
socket.send(JSON.stringify({
|
|
8686
|
+
type: "error",
|
|
8687
|
+
message: `Failed to persist session event: ${err instanceof Error ? err.message : String(err)}`
|
|
8688
|
+
}));
|
|
8689
|
+
}
|
|
8690
|
+
});
|
|
8691
|
+
} else if (type === "session:completion") {
|
|
8349
8692
|
const agentId = parsed.data.agentId;
|
|
8350
8693
|
if (!agentId || !boundAgents.has(agentId)) {
|
|
8351
8694
|
socket.send(JSON.stringify({
|
|
@@ -8354,8 +8697,7 @@ function clientWsRoutes(notifier, instanceId) {
|
|
|
8354
8697
|
}));
|
|
8355
8698
|
return;
|
|
8356
8699
|
}
|
|
8357
|
-
const payload =
|
|
8358
|
-
appendOutput(app.db, agentId, payload.chatId, payload.content).catch(() => {});
|
|
8700
|
+
const payload = sessionCompletionMessageSchema.parse(msg);
|
|
8359
8701
|
if (shouldNotify(agentId, `session_completed:${payload.chatId}`)) notifyAgentEvent(app.db, agentId, "session_completed", "low", `Agent ${agentId} completed a task`, payload.chatId).catch(() => {});
|
|
8360
8702
|
} else if (type === "heartbeat") {
|
|
8361
8703
|
if (clientId) {
|
|
@@ -9267,7 +9609,7 @@ var schema_exports = /* @__PURE__ */ __exportAll({
|
|
|
9267
9609
|
notifications: () => notifications,
|
|
9268
9610
|
organizations: () => organizations,
|
|
9269
9611
|
serverInstances: () => serverInstances,
|
|
9270
|
-
|
|
9612
|
+
sessionEvents: () => sessionEvents,
|
|
9271
9613
|
systemConfigs: () => systemConfigs,
|
|
9272
9614
|
taskChats: () => taskChats,
|
|
9273
9615
|
tasks: () => tasks,
|
|
@@ -10355,6 +10697,90 @@ async function nackEntry(db, entryId) {
|
|
|
10355
10697
|
WHERE id = ${entryId}
|
|
10356
10698
|
`);
|
|
10357
10699
|
}
|
|
10700
|
+
const DEFAULT_INTERVAL_MS = 5e3;
|
|
10701
|
+
const DEFAULT_BUCKET_COUNT = 32;
|
|
10702
|
+
function createPulseAggregator(options) {
|
|
10703
|
+
const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
10704
|
+
const bucketCount = options.bucketCount ?? DEFAULT_BUCKET_COUNT;
|
|
10705
|
+
const state = /* @__PURE__ */ new Map();
|
|
10706
|
+
let currentIdx = 0;
|
|
10707
|
+
let running = false;
|
|
10708
|
+
let intervalHandle = null;
|
|
10709
|
+
function emptyBucket() {
|
|
10710
|
+
return {
|
|
10711
|
+
workingCount: 0,
|
|
10712
|
+
errorMask: false
|
|
10713
|
+
};
|
|
10714
|
+
}
|
|
10715
|
+
function initBuckets() {
|
|
10716
|
+
return Array.from({ length: bucketCount }, emptyBucket);
|
|
10717
|
+
}
|
|
10718
|
+
function ensureAgent(organizationId, agentId) {
|
|
10719
|
+
let orgMap = state.get(organizationId);
|
|
10720
|
+
if (!orgMap) {
|
|
10721
|
+
orgMap = /* @__PURE__ */ new Map();
|
|
10722
|
+
state.set(organizationId, orgMap);
|
|
10723
|
+
}
|
|
10724
|
+
let buckets = orgMap.get(agentId);
|
|
10725
|
+
if (!buckets) {
|
|
10726
|
+
buckets = initBuckets();
|
|
10727
|
+
orgMap.set(agentId, buckets);
|
|
10728
|
+
}
|
|
10729
|
+
return buckets;
|
|
10730
|
+
}
|
|
10731
|
+
const ingest = (payload) => {
|
|
10732
|
+
if (!running) return;
|
|
10733
|
+
if (!payload.organizationId || !payload.agentId) return;
|
|
10734
|
+
const bucket = ensureAgent(payload.organizationId, payload.agentId)[currentIdx];
|
|
10735
|
+
if (!bucket) return;
|
|
10736
|
+
if (payload.state === "working") bucket.workingCount += 1;
|
|
10737
|
+
else if (payload.state === "error") bucket.errorMask = true;
|
|
10738
|
+
};
|
|
10739
|
+
function snapshotBuckets(buckets) {
|
|
10740
|
+
const out = [];
|
|
10741
|
+
for (let i = 0; i < bucketCount; i++) {
|
|
10742
|
+
const src = buckets[(currentIdx + 1 + i) % bucketCount] ?? emptyBucket();
|
|
10743
|
+
out.push({
|
|
10744
|
+
workingCount: src.workingCount,
|
|
10745
|
+
errorMask: src.errorMask
|
|
10746
|
+
});
|
|
10747
|
+
}
|
|
10748
|
+
return out;
|
|
10749
|
+
}
|
|
10750
|
+
function broadcastTick() {
|
|
10751
|
+
for (const [organizationId, orgMap] of state) {
|
|
10752
|
+
const agents = {};
|
|
10753
|
+
for (const [agentId, buckets] of orgMap) agents[agentId] = snapshotBuckets(buckets);
|
|
10754
|
+
options.broadcast({
|
|
10755
|
+
type: "pulse:tick",
|
|
10756
|
+
organizationId,
|
|
10757
|
+
agents
|
|
10758
|
+
});
|
|
10759
|
+
}
|
|
10760
|
+
}
|
|
10761
|
+
function advance() {
|
|
10762
|
+
broadcastTick();
|
|
10763
|
+
currentIdx = (currentIdx + 1) % bucketCount;
|
|
10764
|
+
for (const orgMap of state.values()) for (const buckets of orgMap.values()) buckets[currentIdx] = emptyBucket();
|
|
10765
|
+
}
|
|
10766
|
+
return {
|
|
10767
|
+
start() {
|
|
10768
|
+
if (running) return;
|
|
10769
|
+
running = true;
|
|
10770
|
+
options.notifier.onRuntimeStateChange(ingest);
|
|
10771
|
+
intervalHandle = setInterval(advance, intervalMs);
|
|
10772
|
+
},
|
|
10773
|
+
stop() {
|
|
10774
|
+
if (!running) return;
|
|
10775
|
+
running = false;
|
|
10776
|
+
if (intervalHandle) {
|
|
10777
|
+
clearInterval(intervalHandle);
|
|
10778
|
+
intervalHandle = null;
|
|
10779
|
+
}
|
|
10780
|
+
},
|
|
10781
|
+
ingest
|
|
10782
|
+
};
|
|
10783
|
+
}
|
|
10358
10784
|
async function buildApp(config) {
|
|
10359
10785
|
const app = Fastify({ logger: config.logger ?? true });
|
|
10360
10786
|
const db = connectDatabase(config.database.url);
|
|
@@ -10444,7 +10870,7 @@ async function buildApp(config) {
|
|
|
10444
10870
|
await api.register(async (adminApp) => {
|
|
10445
10871
|
adminApp.addHook("onRequest", memberAuth);
|
|
10446
10872
|
await adminApp.register(adminClientRoutes);
|
|
10447
|
-
}, { prefix: "/
|
|
10873
|
+
}, { prefix: "/clients" });
|
|
10448
10874
|
await api.register(async (adminApp) => {
|
|
10449
10875
|
adminApp.addHook("onRequest", memberAuth);
|
|
10450
10876
|
await adminApp.register(adminActivityRoutes);
|
|
@@ -10513,6 +10939,10 @@ async function buildApp(config) {
|
|
|
10513
10939
|
const contextTreeDir = join(DEFAULT_DATA_DIR$1, "context-tree");
|
|
10514
10940
|
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
10941
|
const backgroundTasks = createBackgroundTasks(app, config.instanceId, adapterManager, kaelRuntime);
|
|
10942
|
+
const pulseAggregator = createPulseAggregator({
|
|
10943
|
+
notifier,
|
|
10944
|
+
broadcast: broadcastToAdmins
|
|
10945
|
+
});
|
|
10516
10946
|
notifier.onConfigChange((configType) => {
|
|
10517
10947
|
if (configType === "adapter_configs") {
|
|
10518
10948
|
adapterManager.reload().catch((err) => app.log.error(err, "Adapter hot-reload failed (PG NOTIFY)"));
|
|
@@ -10523,10 +10953,12 @@ async function buildApp(config) {
|
|
|
10523
10953
|
await ensureDefaultOrganization(db);
|
|
10524
10954
|
await notifier.start();
|
|
10525
10955
|
backgroundTasks.start();
|
|
10956
|
+
pulseAggregator.start();
|
|
10526
10957
|
await adapterManager.reload();
|
|
10527
10958
|
await kaelRuntime?.reload();
|
|
10528
10959
|
});
|
|
10529
10960
|
app.addHook("onClose", async () => {
|
|
10961
|
+
pulseAggregator.stop();
|
|
10530
10962
|
backgroundTasks.stop();
|
|
10531
10963
|
adapterManager.shutdown();
|
|
10532
10964
|
kaelRuntime?.shutdown();
|
|
@@ -10635,4 +11067,353 @@ function resolveWebDist() {
|
|
|
10635
11067
|
} catch {}
|
|
10636
11068
|
}
|
|
10637
11069
|
//#endregion
|
|
10638
|
-
|
|
11070
|
+
//#region src/core/service-install.ts
|
|
11071
|
+
const LAUNCHD_LABEL = "dev.first-tree-hub.client";
|
|
11072
|
+
const SYSTEMD_UNIT = "first-tree-hub-client.service";
|
|
11073
|
+
const LOG_DIR = join(DEFAULT_HOME_DIR$1, "logs");
|
|
11074
|
+
function whichBin(name) {
|
|
11075
|
+
try {
|
|
11076
|
+
return execFileSync(process.platform === "win32" ? "where" : "which", [name], {
|
|
11077
|
+
encoding: "utf-8",
|
|
11078
|
+
timeout: 3e3
|
|
11079
|
+
}).split(/\r?\n/).map((s) => s.trim()).filter(Boolean)[0] ?? null;
|
|
11080
|
+
} catch {
|
|
11081
|
+
return null;
|
|
11082
|
+
}
|
|
11083
|
+
}
|
|
11084
|
+
/**
|
|
11085
|
+
* Resolve how the service should launch the CLI.
|
|
11086
|
+
*
|
|
11087
|
+
* Prefers the installed `first-tree-hub` bin on PATH (usually a shim under
|
|
11088
|
+
* /usr/local/bin or ~/.npm-global/bin). Falls back to invoking the current
|
|
11089
|
+
* Node interpreter against the running script (handles `pnpm dev`, tsx, and
|
|
11090
|
+
* dev-only global installs).
|
|
11091
|
+
*/
|
|
11092
|
+
function resolveCliInvocation() {
|
|
11093
|
+
const bin = whichBin("first-tree-hub");
|
|
11094
|
+
if (bin && isAbsolute(bin)) try {
|
|
11095
|
+
return {
|
|
11096
|
+
kind: "bin",
|
|
11097
|
+
program: realpathSync(bin)
|
|
11098
|
+
};
|
|
11099
|
+
} catch {
|
|
11100
|
+
return {
|
|
11101
|
+
kind: "bin",
|
|
11102
|
+
program: bin
|
|
11103
|
+
};
|
|
11104
|
+
}
|
|
11105
|
+
const script = process.argv[1];
|
|
11106
|
+
if (!script) throw new Error("Cannot resolve CLI entry point (process.argv[1] is empty).");
|
|
11107
|
+
const scriptAbs = isAbsolute(script) ? script : join(process.cwd(), script);
|
|
11108
|
+
return {
|
|
11109
|
+
kind: "node",
|
|
11110
|
+
program: process.execPath,
|
|
11111
|
+
args: [scriptAbs]
|
|
11112
|
+
};
|
|
11113
|
+
}
|
|
11114
|
+
function ensureLogDir() {
|
|
11115
|
+
mkdirSync(LOG_DIR, {
|
|
11116
|
+
recursive: true,
|
|
11117
|
+
mode: 448
|
|
11118
|
+
});
|
|
11119
|
+
}
|
|
11120
|
+
function launchdPlistPath() {
|
|
11121
|
+
return join(homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
11122
|
+
}
|
|
11123
|
+
function renderPlist(invocation) {
|
|
11124
|
+
const argsXml = (invocation.kind === "bin" ? [
|
|
11125
|
+
invocation.program,
|
|
11126
|
+
"client",
|
|
11127
|
+
"start",
|
|
11128
|
+
"--no-interactive"
|
|
11129
|
+
] : [
|
|
11130
|
+
invocation.program,
|
|
11131
|
+
...invocation.args,
|
|
11132
|
+
"client",
|
|
11133
|
+
"start",
|
|
11134
|
+
"--no-interactive"
|
|
11135
|
+
]).map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
|
|
11136
|
+
const outLog = join(LOG_DIR, "client.out.log");
|
|
11137
|
+
const errLog = join(LOG_DIR, "client.err.log");
|
|
11138
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
11139
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTD/PropertyList-1.0.dtd">
|
|
11140
|
+
<plist version="1.0">
|
|
11141
|
+
<dict>
|
|
11142
|
+
<key>Label</key>
|
|
11143
|
+
<string>${LAUNCHD_LABEL}</string>
|
|
11144
|
+
<key>ProgramArguments</key>
|
|
11145
|
+
<array>
|
|
11146
|
+
${argsXml}
|
|
11147
|
+
</array>
|
|
11148
|
+
<key>EnvironmentVariables</key>
|
|
11149
|
+
<dict>
|
|
11150
|
+
<key>PATH</key>
|
|
11151
|
+
<string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
|
|
11152
|
+
</dict>
|
|
11153
|
+
<key>RunAtLoad</key>
|
|
11154
|
+
<true/>
|
|
11155
|
+
<key>KeepAlive</key>
|
|
11156
|
+
<dict>
|
|
11157
|
+
<key>SuccessfulExit</key>
|
|
11158
|
+
<false/>
|
|
11159
|
+
</dict>
|
|
11160
|
+
<key>ThrottleInterval</key>
|
|
11161
|
+
<integer>10</integer>
|
|
11162
|
+
<key>StandardOutPath</key>
|
|
11163
|
+
<string>${escapeXml(outLog)}</string>
|
|
11164
|
+
<key>StandardErrorPath</key>
|
|
11165
|
+
<string>${escapeXml(errLog)}</string>
|
|
11166
|
+
</dict>
|
|
11167
|
+
</plist>
|
|
11168
|
+
`;
|
|
11169
|
+
}
|
|
11170
|
+
function escapeXml(value) {
|
|
11171
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
11172
|
+
}
|
|
11173
|
+
function launchctlDomainTarget() {
|
|
11174
|
+
return `gui/${userInfo().uid}`;
|
|
11175
|
+
}
|
|
11176
|
+
function launchdState() {
|
|
11177
|
+
if (!existsSync(launchdPlistPath())) return { state: "not-installed" };
|
|
11178
|
+
try {
|
|
11179
|
+
const out = execFileSync("launchctl", ["print", `${launchctlDomainTarget()}/${LAUNCHD_LABEL}`], {
|
|
11180
|
+
encoding: "utf-8",
|
|
11181
|
+
timeout: 5e3
|
|
11182
|
+
});
|
|
11183
|
+
const stateLine = out.split(/\r?\n/).find((l) => l.trim().startsWith("state ="));
|
|
11184
|
+
const pidLine = out.split(/\r?\n/).find((l) => l.trim().startsWith("pid ="));
|
|
11185
|
+
if (stateLine?.includes("running")) {
|
|
11186
|
+
const pid = pidLine?.split("=")[1]?.trim();
|
|
11187
|
+
return {
|
|
11188
|
+
state: "active",
|
|
11189
|
+
detail: pid ? `pid ${pid}` : "running"
|
|
11190
|
+
};
|
|
11191
|
+
}
|
|
11192
|
+
return {
|
|
11193
|
+
state: "inactive",
|
|
11194
|
+
detail: stateLine?.trim() ?? "loaded"
|
|
11195
|
+
};
|
|
11196
|
+
} catch {
|
|
11197
|
+
return {
|
|
11198
|
+
state: "inactive",
|
|
11199
|
+
detail: "plist present but not loaded"
|
|
11200
|
+
};
|
|
11201
|
+
}
|
|
11202
|
+
}
|
|
11203
|
+
function installLaunchd() {
|
|
11204
|
+
const invocation = resolveCliInvocation();
|
|
11205
|
+
ensureLogDir();
|
|
11206
|
+
const plistPath = launchdPlistPath();
|
|
11207
|
+
mkdirSync(dirname(plistPath), { recursive: true });
|
|
11208
|
+
writeFileSync(plistPath, renderPlist(invocation), { mode: 420 });
|
|
11209
|
+
const target = launchctlDomainTarget();
|
|
11210
|
+
try {
|
|
11211
|
+
execFileSync("launchctl", ["bootout", `${target}/${LAUNCHD_LABEL}`], {
|
|
11212
|
+
stdio: "ignore",
|
|
11213
|
+
timeout: 5e3
|
|
11214
|
+
});
|
|
11215
|
+
} catch {}
|
|
11216
|
+
execFileSync("launchctl", [
|
|
11217
|
+
"bootstrap",
|
|
11218
|
+
target,
|
|
11219
|
+
plistPath
|
|
11220
|
+
], {
|
|
11221
|
+
stdio: "ignore",
|
|
11222
|
+
timeout: 5e3
|
|
11223
|
+
});
|
|
11224
|
+
execFileSync("launchctl", ["enable", `${target}/${LAUNCHD_LABEL}`], {
|
|
11225
|
+
stdio: "ignore",
|
|
11226
|
+
timeout: 5e3
|
|
11227
|
+
});
|
|
11228
|
+
const { state, detail } = launchdState();
|
|
11229
|
+
return {
|
|
11230
|
+
platform: "launchd",
|
|
11231
|
+
label: LAUNCHD_LABEL,
|
|
11232
|
+
unitPath: plistPath,
|
|
11233
|
+
logDir: LOG_DIR,
|
|
11234
|
+
state,
|
|
11235
|
+
detail
|
|
11236
|
+
};
|
|
11237
|
+
}
|
|
11238
|
+
function uninstallLaunchd() {
|
|
11239
|
+
const plistPath = launchdPlistPath();
|
|
11240
|
+
const target = launchctlDomainTarget();
|
|
11241
|
+
try {
|
|
11242
|
+
execFileSync("launchctl", ["bootout", `${target}/${LAUNCHD_LABEL}`], {
|
|
11243
|
+
stdio: "ignore",
|
|
11244
|
+
timeout: 5e3
|
|
11245
|
+
});
|
|
11246
|
+
} catch {}
|
|
11247
|
+
if (existsSync(plistPath)) rmSync(plistPath);
|
|
11248
|
+
return {
|
|
11249
|
+
platform: "launchd",
|
|
11250
|
+
label: LAUNCHD_LABEL,
|
|
11251
|
+
unitPath: plistPath,
|
|
11252
|
+
logDir: LOG_DIR,
|
|
11253
|
+
state: "not-installed"
|
|
11254
|
+
};
|
|
11255
|
+
}
|
|
11256
|
+
function systemdUnitPath() {
|
|
11257
|
+
return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "systemd", "user", SYSTEMD_UNIT);
|
|
11258
|
+
}
|
|
11259
|
+
function renderSystemdUnit(invocation) {
|
|
11260
|
+
return `[Unit]
|
|
11261
|
+
Description=First Tree Hub Client
|
|
11262
|
+
After=network-online.target
|
|
11263
|
+
Wants=network-online.target
|
|
11264
|
+
|
|
11265
|
+
[Service]
|
|
11266
|
+
Type=simple
|
|
11267
|
+
ExecStart=${invocation.kind === "bin" ? `${shellQuote(invocation.program)} client start --no-interactive` : `${shellQuote(invocation.program)} ${invocation.args.map(shellQuote).join(" ")} client start --no-interactive`}
|
|
11268
|
+
Restart=always
|
|
11269
|
+
RestartSec=10
|
|
11270
|
+
StandardOutput=append:${join(LOG_DIR, "client.out.log")}
|
|
11271
|
+
StandardError=append:${join(LOG_DIR, "client.err.log")}
|
|
11272
|
+
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
|
11273
|
+
|
|
11274
|
+
[Install]
|
|
11275
|
+
WantedBy=default.target
|
|
11276
|
+
`;
|
|
11277
|
+
}
|
|
11278
|
+
function shellQuote(value) {
|
|
11279
|
+
if (/^[A-Za-z0-9_\-./:=]+$/.test(value)) return value;
|
|
11280
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
11281
|
+
}
|
|
11282
|
+
function systemdState() {
|
|
11283
|
+
if (!existsSync(systemdUnitPath())) return { state: "not-installed" };
|
|
11284
|
+
try {
|
|
11285
|
+
const out = execFileSync("systemctl", [
|
|
11286
|
+
"--user",
|
|
11287
|
+
"is-active",
|
|
11288
|
+
SYSTEMD_UNIT
|
|
11289
|
+
], {
|
|
11290
|
+
encoding: "utf-8",
|
|
11291
|
+
timeout: 5e3
|
|
11292
|
+
}).trim();
|
|
11293
|
+
if (out === "active") return {
|
|
11294
|
+
state: "active",
|
|
11295
|
+
detail: "running"
|
|
11296
|
+
};
|
|
11297
|
+
return {
|
|
11298
|
+
state: "inactive",
|
|
11299
|
+
detail: out
|
|
11300
|
+
};
|
|
11301
|
+
} catch (err) {
|
|
11302
|
+
return {
|
|
11303
|
+
state: "inactive",
|
|
11304
|
+
detail: (typeof err.stdout === "string" ? (err.stdout ?? "").trim() : "") || "unit present but not active"
|
|
11305
|
+
};
|
|
11306
|
+
}
|
|
11307
|
+
}
|
|
11308
|
+
function installSystemd() {
|
|
11309
|
+
const invocation = resolveCliInvocation();
|
|
11310
|
+
ensureLogDir();
|
|
11311
|
+
const unitPath = systemdUnitPath();
|
|
11312
|
+
mkdirSync(dirname(unitPath), { recursive: true });
|
|
11313
|
+
writeFileSync(unitPath, renderSystemdUnit(invocation), { mode: 420 });
|
|
11314
|
+
execFileSync("systemctl", ["--user", "daemon-reload"], {
|
|
11315
|
+
stdio: "ignore",
|
|
11316
|
+
timeout: 5e3
|
|
11317
|
+
});
|
|
11318
|
+
execFileSync("systemctl", [
|
|
11319
|
+
"--user",
|
|
11320
|
+
"enable",
|
|
11321
|
+
"--now",
|
|
11322
|
+
SYSTEMD_UNIT
|
|
11323
|
+
], {
|
|
11324
|
+
stdio: "ignore",
|
|
11325
|
+
timeout: 1e4
|
|
11326
|
+
});
|
|
11327
|
+
const { state, detail } = systemdState();
|
|
11328
|
+
return {
|
|
11329
|
+
platform: "systemd",
|
|
11330
|
+
label: SYSTEMD_UNIT,
|
|
11331
|
+
unitPath,
|
|
11332
|
+
logDir: LOG_DIR,
|
|
11333
|
+
state,
|
|
11334
|
+
detail
|
|
11335
|
+
};
|
|
11336
|
+
}
|
|
11337
|
+
function uninstallSystemd() {
|
|
11338
|
+
const unitPath = systemdUnitPath();
|
|
11339
|
+
try {
|
|
11340
|
+
execFileSync("systemctl", [
|
|
11341
|
+
"--user",
|
|
11342
|
+
"disable",
|
|
11343
|
+
"--now",
|
|
11344
|
+
SYSTEMD_UNIT
|
|
11345
|
+
], {
|
|
11346
|
+
stdio: "ignore",
|
|
11347
|
+
timeout: 1e4
|
|
11348
|
+
});
|
|
11349
|
+
} catch {}
|
|
11350
|
+
if (existsSync(unitPath)) rmSync(unitPath);
|
|
11351
|
+
try {
|
|
11352
|
+
execFileSync("systemctl", ["--user", "daemon-reload"], {
|
|
11353
|
+
stdio: "ignore",
|
|
11354
|
+
timeout: 5e3
|
|
11355
|
+
});
|
|
11356
|
+
} catch {}
|
|
11357
|
+
return {
|
|
11358
|
+
platform: "systemd",
|
|
11359
|
+
label: SYSTEMD_UNIT,
|
|
11360
|
+
unitPath,
|
|
11361
|
+
logDir: LOG_DIR,
|
|
11362
|
+
state: "not-installed"
|
|
11363
|
+
};
|
|
11364
|
+
}
|
|
11365
|
+
/** Is background-service install supported on the current platform? */
|
|
11366
|
+
function isServiceSupported() {
|
|
11367
|
+
return process.platform === "darwin" || process.platform === "linux";
|
|
11368
|
+
}
|
|
11369
|
+
/**
|
|
11370
|
+
* Install the background service for the current platform.
|
|
11371
|
+
*
|
|
11372
|
+
* @throws {Error} if the platform is not supported or the service manager fails.
|
|
11373
|
+
*/
|
|
11374
|
+
function installClientService() {
|
|
11375
|
+
if (process.platform === "darwin") return installLaunchd();
|
|
11376
|
+
if (process.platform === "linux") return installSystemd();
|
|
11377
|
+
throw new Error(`Background service install is not supported on ${process.platform}. Run \`first-tree-hub client start\` manually to keep the computer online.`);
|
|
11378
|
+
}
|
|
11379
|
+
/** Report the current service state without modifying anything. */
|
|
11380
|
+
function getClientServiceStatus() {
|
|
11381
|
+
if (process.platform === "darwin") {
|
|
11382
|
+
const { state, detail } = launchdState();
|
|
11383
|
+
return {
|
|
11384
|
+
platform: "launchd",
|
|
11385
|
+
label: LAUNCHD_LABEL,
|
|
11386
|
+
unitPath: launchdPlistPath(),
|
|
11387
|
+
logDir: LOG_DIR,
|
|
11388
|
+
state,
|
|
11389
|
+
detail
|
|
11390
|
+
};
|
|
11391
|
+
}
|
|
11392
|
+
if (process.platform === "linux") {
|
|
11393
|
+
const { state, detail } = systemdState();
|
|
11394
|
+
return {
|
|
11395
|
+
platform: "systemd",
|
|
11396
|
+
label: SYSTEMD_UNIT,
|
|
11397
|
+
unitPath: systemdUnitPath(),
|
|
11398
|
+
logDir: LOG_DIR,
|
|
11399
|
+
state,
|
|
11400
|
+
detail
|
|
11401
|
+
};
|
|
11402
|
+
}
|
|
11403
|
+
return {
|
|
11404
|
+
platform: "unsupported",
|
|
11405
|
+
label: "",
|
|
11406
|
+
unitPath: "",
|
|
11407
|
+
logDir: LOG_DIR,
|
|
11408
|
+
state: "not-installed",
|
|
11409
|
+
detail: `platform ${process.platform} not supported`
|
|
11410
|
+
};
|
|
11411
|
+
}
|
|
11412
|
+
/** Uninstall the background service. No-op if not installed. */
|
|
11413
|
+
function uninstallClientService() {
|
|
11414
|
+
if (process.platform === "darwin") return uninstallLaunchd();
|
|
11415
|
+
if (process.platform === "linux") return uninstallSystemd();
|
|
11416
|
+
return getClientServiceStatus();
|
|
11417
|
+
}
|
|
11418
|
+
//#endregion
|
|
11419
|
+
export { stopPostgres as A, checkServerReachable as C, status as D, blank as E, SdkError as F, SessionRegistry as I, cleanWorkspaces as L, createOwner as M, hasUser as N, ensurePostgres as O, FirstTreeHubSDK as P, checkServerHealth as S, printResults as T, checkClientConfig as _, uninstallClientService as a, checkNodeVersion as b, promptAddAgent as c, loadOnboardState as d, onboardCheck as f, checkAgentConfigs as g, runMigrations as h, resolveCliInvocation as i, ClientRuntime as j, isDockerAvailable as k, promptMissingFields as l, saveOnboardState as m, installClientService as n, startServer as o, onboardCreate as p, isServiceSupported as r, isInteractive as s, getClientServiceStatus as t, formatCheckReport as u, checkDatabase as v, checkWebSocket as w, checkServerConfig as x, checkDocker as y };
|