@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.
@@ -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-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";
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
- 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() {
@@ -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
- 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)}`));
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
- } 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}`);
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
- appendOutput: (content) => {
2709
- this.config.onSessionOutput?.(chatId, content);
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
- onSessionOutput: (chatId, content) => this.reportSessionOutput(chatId, content)
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
- reportSessionOutput(chatId, content) {
2888
- this.clientConnection.reportSessionOutput(this.config.agentId, chatId, content);
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-BoMJHlOv.mjs").then((n) => n.r);
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-BjIHPMKY.mjs
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
- return (await app.db.select().from(adapterAgentMappings).orderBy(desc(adapterAgentMappings.createdAt))).map((r) => ({
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
- const orgId = data.organizationId ?? await resolveDefaultOrgId(db);
4907
- const managerId = data.managerId ?? await resolveFallbackManagerId(db, orgId);
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
- async function setRuntimeState(db, agentId, runtimeState) {
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 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));
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
- if (firstSep > 0 && secondSep > firstSep) {
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 and update materialized aggregates on agent_presence. */
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
- if (!adminWsBroadcast) return;
6627
- try {
6628
- adminWsBroadcast({
6629
- type: "notification",
6630
- data: notification
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 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.
7070
+ * Session eventsstructured 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 sessionOutputs = pgTable("session_outputs", {
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
- 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
- });
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
- /** 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;
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
- content: row.content,
6871
- updatedAt: row.updatedAt.toISOString()
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
- await assertAgentVisible(app.db, memberScope(request), request.params.agentId);
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/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
- };
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
- * 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
- */
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
- setAdminWsBroadcast((payload) => {
7840
+ function broadcastOrgScoped(payload) {
7548
7841
  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
- });
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
- const data = JSON.stringify({
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 || !payload.organizationId) {
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
- adminSockets.set(socket, { organizationId });
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:output` / `heartbeat`.
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
- await upsertSessionState(app.db, agentId, payload.chatId, payload.state, notifier);
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:output") {
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 = sessionOutputMessageSchema.parse(msg);
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
- sessionOutputs: () => sessionOutputs,
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: "/admin/clients" });
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
- export { SdkError as A, ensurePostgres as C, createOwner as D, ClientRuntime as E, cleanWorkspaces as M, hasUser as O, status as S, stopPostgres as T, checkServerHealth as _, formatCheckReport as a, printResults as b, onboardCreate as c, checkAgentConfigs as d, checkClientConfig as f, checkServerConfig as g, checkNodeVersion as h, promptMissingFields as i, SessionRegistry as j, FirstTreeHubSDK as k, saveOnboardState as l, checkDocker as m, isInteractive as n, loadOnboardState as o, checkDatabase as p, promptAddAgent as r, onboardCheck as s, startServer as t, runMigrations as u, checkServerReachable as v, isDockerAvailable as w, blank as x, checkWebSocket as y };
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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 };