@agent-team-foundation/first-tree-hub 0.6.3 → 0.7.0

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