@agent-team-foundation/first-tree-hub 0.8.1 → 0.8.3

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,15 +1,18 @@
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-8nCntTrK.mjs";
2
- import { $ as updateAgentRuntimeConfigSchema, A as createMemberSchema, B as notificationQuerySchema, C as agentTypeSchema$1, D as createAdapterMappingSchema, E as createAdapterConfigSchema, F as inboxPollQuerySchema, G as sendMessageSchema, H as refreshTokenSchema, I as isRedactedEnvValue, J as sessionEventMessageSchema, K as sendToAgentSchema, L as linkTaskChatSchema, M as createTaskSchema, N as delegateFeishuUserSchema, O as createAgentSchema, P as dryRunAgentRuntimeConfigSchema, Q as updateAdapterConfigSchema, R as loginSchema, S as agentRuntimeConfigPayloadSchema$1, T as connectTokenExchangeSchema, U as runtimeStateMessageSchema, V as paginationQuerySchema, W as selfServiceFeishuBotSchema, X as sessionStateMessageSchema, Y as sessionEventSchema$1, Z as taskListQuerySchema, _ as addParticipantSchema, a as AGENT_SELECTOR_HEADER$1, at as wsAuthFrameSchema, b as agentBindRequestSchema, c as AGENT_TYPES, d as SYSTEM_CONFIG_DEFAULTS, et as updateAgentSchema, 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 updateTaskStatusSchema, j as createOrganizationSchema, k as createChatSchema, l as AGENT_VISIBILITY, m as TASK_STATUSES, nt as updateOrganizationSchema, o as AGENT_SOURCES, p as TASK_HEALTH_SIGNALS, q as sessionCompletionMessageSchema, rt as updateSystemConfigSchema, s as AGENT_STATUSES, tt as updateMemberSchema, u as DEFAULT_AGENT_RUNTIME_CONFIG_PAYLOAD, v as adminCreateTaskSchema, w as clientRegisterSchema, x as agentPinnedMessageSchema$1, y as adminUpdateTaskSchema, z as messageSourceSchema$1 } from "./feishu-D9JkMZnU.mjs";
1
+ import { m as __toESM } from "./esm-CYu4tXXn.mjs";
2
+ 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-99vUYmLs.mjs";
3
+ import { _ as withSpan, a as endWsConnectionSpan, b as require_pino, c as messageAttrs, d as rootLogger$1, g as startWsConnectionSpan, i as currentTraceId, n as applyLoggerConfig, o as getFastifyOtelPlugin, p as setWsConnectionAttrs, r as createLogger, t as adapterAttrs, u as observabilityPlugin, v as withWsMessageSpan, y as FIRST_TREE_HUB_ATTR } from "./observability-CJzDFY_G-CmvgUuzc.mjs";
4
+ import { $ as updateAgentRuntimeConfigSchema, A as createMemberSchema, B as notificationQuerySchema, C as agentTypeSchema$1, D as createAdapterMappingSchema, E as createAdapterConfigSchema, F as inboxPollQuerySchema, G as sendMessageSchema, H as refreshTokenSchema, I as isRedactedEnvValue, J as sessionEventMessageSchema, K as sendToAgentSchema, L as linkTaskChatSchema, M as createTaskSchema, N as delegateFeishuUserSchema, O as createAgentSchema, P as dryRunAgentRuntimeConfigSchema, Q as updateAdapterConfigSchema, R as loginSchema, S as agentRuntimeConfigPayloadSchema$1, T as connectTokenExchangeSchema, U as runtimeStateMessageSchema, V as paginationQuerySchema, W as selfServiceFeishuBotSchema, X as sessionStateMessageSchema, Y as sessionEventSchema$1, Z as taskListQuerySchema, _ as addParticipantSchema, a as AGENT_SELECTOR_HEADER$1, at as wsAuthFrameSchema, b as agentBindRequestSchema, c as AGENT_TYPES, d as SYSTEM_CONFIG_DEFAULTS, et as updateAgentSchema, 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 updateTaskStatusSchema, j as createOrganizationSchema, k as createChatSchema, l as AGENT_VISIBILITY, m as TASK_STATUSES, nt as updateOrganizationSchema, o as AGENT_SOURCES, p as TASK_HEALTH_SIGNALS, q as sessionCompletionMessageSchema, rt as updateSystemConfigSchema, s as AGENT_STATUSES, tt as updateMemberSchema, u as DEFAULT_AGENT_RUNTIME_CONFIG_PAYLOAD, v as adminCreateTaskSchema, w as clientRegisterSchema, x as agentPinnedMessageSchema$1, y as adminUpdateTaskSchema, z as messageSourceSchema$1 } from "./feishu-BOISS0DK.mjs";
3
5
  import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
4
6
  import { dirname, isAbsolute, join, resolve } from "node:path";
5
7
  import { ZodError, z } from "zod";
8
+ import { Writable } from "node:stream";
6
9
  import { stringify } from "yaml";
7
10
  import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
8
11
  import { homedir, hostname, platform, userInfo } from "node:os";
9
12
  import { EventEmitter } from "node:events";
10
13
  import WebSocket from "ws";
11
14
  import { query } from "@anthropic-ai/claude-agent-sdk";
12
- import { execFileSync, execSync, spawn } from "node:child_process";
15
+ import { execFileSync, execSync, spawn, spawnSync } from "node:child_process";
13
16
  import bcrypt from "bcrypt";
14
17
  import { and, asc, count, desc, eq, gt, inArray, isNotNull, isNull, lt, ne, or, sql } from "drizzle-orm";
15
18
  import { drizzle } from "drizzle-orm/postgres-js";
@@ -25,6 +28,136 @@ import Fastify from "fastify";
25
28
  import { bigserial, boolean, index, integer, jsonb, pgTable, primaryKey, serial, text, timestamp, unique, uniqueIndex } from "drizzle-orm/pg-core";
26
29
  import { SignJWT, jwtVerify } from "jose";
27
30
  import { Client, EventDispatcher, LoggerLevel, WSClient } from "@larksuiteoapi/node-sdk";
31
+ //#region ../client/dist/observability-BUvHY6T-.mjs
32
+ var import_pino = /* @__PURE__ */ __toESM(require_pino(), 1);
33
+ /**
34
+ * Logger core — format / level primitives shared between server and client.
35
+ *
36
+ * This module intentionally has no dependency on `pino` so it can live in
37
+ * `@agent-team-foundation/first-tree-hub-shared`. Consumers construct their
38
+ * own pino instance and pass the output stream built here.
39
+ */
40
+ const LOG_LEVELS = [
41
+ "trace",
42
+ "debug",
43
+ "info",
44
+ "warn",
45
+ "error",
46
+ "fatal"
47
+ ];
48
+ const LOG_FORMATS = ["pretty", "json"];
49
+ const logLevelSchema = z.enum(LOG_LEVELS);
50
+ const logFormatSchema = z.enum(LOG_FORMATS);
51
+ /**
52
+ * Parse an env-var / config string into a LogLevel. Unknown values fall back
53
+ * to `info` so the process never fails to boot on a typo — the caller is
54
+ * responsible for emitting a warning when `fellBack` is true.
55
+ */
56
+ function parseLogLevel(raw) {
57
+ if (!raw) return {
58
+ level: "info",
59
+ fellBack: false
60
+ };
61
+ const parsed = logLevelSchema.safeParse(raw);
62
+ if (parsed.success) return {
63
+ level: parsed.data,
64
+ fellBack: false
65
+ };
66
+ return {
67
+ level: "info",
68
+ fellBack: true
69
+ };
70
+ }
71
+ const LEVEL_LABELS = {
72
+ 10: "TRACE",
73
+ 20: "DEBUG",
74
+ 30: "INFO",
75
+ 40: "WARN",
76
+ 50: "ERROR",
77
+ 60: "FATAL"
78
+ };
79
+ const LEVEL_COLORS = {
80
+ 10: "\x1B[90m",
81
+ 20: "\x1B[36m",
82
+ 30: "\x1B[32m",
83
+ 40: "\x1B[33m",
84
+ 50: "\x1B[31m",
85
+ 60: "\x1B[35m"
86
+ };
87
+ const RESET = "\x1B[0m";
88
+ const DIM = "\x1B[2m";
89
+ const SKIP_KEYS = new Set([
90
+ "level",
91
+ "time",
92
+ "msg",
93
+ "module",
94
+ "pid",
95
+ "hostname",
96
+ "v"
97
+ ]);
98
+ function formatPrettyEntry(json) {
99
+ const obj = JSON.parse(json);
100
+ const level = obj.level;
101
+ const label = LEVEL_LABELS[level] ?? "???";
102
+ const color = LEVEL_COLORS[level] ?? "";
103
+ const time = obj.time ?? (/* @__PURE__ */ new Date()).toISOString();
104
+ const module = obj.module ? `[${String(obj.module)}] ` : "";
105
+ const msg = obj.msg ?? "";
106
+ const extras = [];
107
+ let errStack = "";
108
+ for (const [k, v] of Object.entries(obj)) {
109
+ if (SKIP_KEYS.has(k)) continue;
110
+ if (k === "err" && v && typeof v === "object") {
111
+ const e = v;
112
+ if (e.message) extras.push(`err.message=${String(e.message)}`);
113
+ if (typeof e.stack === "string") errStack = `\n${DIM}${e.stack}${RESET}`;
114
+ } else extras.push(`${k}=${typeof v === "string" ? v : JSON.stringify(v)}`);
115
+ }
116
+ const extraStr = extras.length > 0 ? ` ${DIM}${extras.join(" ")}${RESET}` : "";
117
+ return `${DIM}${time}${RESET} ${color}${label.padEnd(5)}${RESET} ${module}${msg}${extraStr}${errStack}\n`;
118
+ }
119
+ function formatLocalTime() {
120
+ const d = /* @__PURE__ */ new Date();
121
+ return `${d.toLocaleDateString("sv-SE")} ${d.toLocaleTimeString("en-GB", { hour12: false })}`;
122
+ }
123
+ function createLoggerOutputStream(options) {
124
+ return new Writable({ write(chunk, _, callback) {
125
+ const text = chunk.toString();
126
+ try {
127
+ if (options.getFormat() === "pretty") process.stdout.write(formatPrettyEntry(text));
128
+ else process.stdout.write(text);
129
+ if (options.onJsonEntry) try {
130
+ const obj = JSON.parse(text);
131
+ options.onJsonEntry(obj);
132
+ } catch {}
133
+ } catch {
134
+ process.stdout.write(text);
135
+ }
136
+ callback();
137
+ } });
138
+ }
139
+ /**
140
+ * Client-side logger. Same pretty / NDJSON formats as the server logger, but
141
+ * intentionally lightweight — the client is deployed to agent user machines,
142
+ * so we skip tracing, context propagation, and error sinks.
143
+ */
144
+ const initialLevel = parseLogLevel(process.env.FIRST_TREE_HUB_LOG_LEVEL);
145
+ let _format = process.env.NODE_ENV === "production" ? "json" : "pretty";
146
+ let _level = initialLevel.level;
147
+ function applyClientLoggerConfig(options = {}) {
148
+ if (options.level) {
149
+ _level = options.level;
150
+ rootLogger.level = options.level;
151
+ }
152
+ if (options.format) _format = options.format;
153
+ }
154
+ const outputStream = createLoggerOutputStream({ getFormat: () => _format });
155
+ const rootLogger = (0, import_pino.default)({
156
+ level: _level,
157
+ timestamp: () => `,"time":"${formatLocalTime()}"`
158
+ }, outputStream);
159
+ if (initialLevel.fellBack) rootLogger.warn({ envValue: process.env.FIRST_TREE_HUB_LOG_LEVEL }, "invalid FIRST_TREE_HUB_LOG_LEVEL; falling back to info");
160
+ //#endregion
28
161
  //#region ../client/dist/index.mjs
29
162
  const adapterPlatformSchema = z.enum([
30
163
  "feishu",
@@ -1350,12 +1483,7 @@ defineConfig({
1350
1483
  auto: "client-id",
1351
1484
  env: "FIRST_TREE_HUB_CLIENT_ID"
1352
1485
  }) },
1353
- logLevel: field(z.enum([
1354
- "debug",
1355
- "info",
1356
- "warn",
1357
- "error"
1358
- ]).default("info"), { env: "FIRST_TREE_HUB_LOG_LEVEL" })
1486
+ logLevel: field(logLevelSchema.default("info"), { env: "FIRST_TREE_HUB_LOG_LEVEL" })
1359
1487
  });
1360
1488
  const DEFAULT_HOME_DIR = process.env.FIRST_TREE_HUB_HOME ?? join(homedir(), ".first-tree-hub");
1361
1489
  join(DEFAULT_HOME_DIR, "config");
@@ -1422,7 +1550,29 @@ defineConfig({
1422
1550
  secret: true
1423
1551
  }),
1424
1552
  hubPublicUrl: field(z.string(), { env: "FIRST_TREE_HUB_PUBLIC_URL" })
1425
- })
1553
+ }),
1554
+ observability: {
1555
+ logging: {
1556
+ level: field(logLevelSchema.default("info"), { env: "FIRST_TREE_HUB_LOG_LEVEL" }),
1557
+ format: field(logFormatSchema.default(process.env.NODE_ENV === "production" ? "json" : "pretty")),
1558
+ bridgeToSpanLevel: field(z.enum([
1559
+ "error",
1560
+ "warn",
1561
+ "off"
1562
+ ]).default("error"))
1563
+ },
1564
+ tracing: optional({
1565
+ endpoint: field(z.string(), { env: "FIRST_TREE_HUB_OTEL_ENDPOINT" }),
1566
+ headers: field(z.string().default(""), {
1567
+ env: "FIRST_TREE_HUB_OTEL_HEADERS",
1568
+ secret: true
1569
+ }),
1570
+ exporter: field(z.enum(["otlp-http", "otlp-grpc"]).default("otlp-http")),
1571
+ serviceName: field(z.string().default("first-tree-hub")),
1572
+ environment: field(z.string().default("development"), { env: "FIRST_TREE_HUB_OTEL_ENVIRONMENT" }),
1573
+ sampleRate: field(z.number().min(0).max(1).default(1))
1574
+ })
1575
+ }
1426
1576
  });
1427
1577
  join(DEFAULT_DATA_DIR, "context-tree");
1428
1578
  /**
@@ -4371,7 +4521,7 @@ async function onboardCreate(args) {
4371
4521
  }
4372
4522
  const runtimeAgent = args.type === "human" ? args.assistant : args.id;
4373
4523
  if (args.feishuBotAppId && args.feishuBotAppSecret) {
4374
- const { bindFeishuBot } = await import("./feishu-D9JkMZnU.mjs").then((n) => n.r);
4524
+ const { bindFeishuBot } = await import("./feishu-BOISS0DK.mjs").then((n) => n.r);
4375
4525
  const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
4376
4526
  if (!targetAgentUuid) process.stderr.write(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
4377
4527
  else {
@@ -4512,7 +4662,7 @@ function setNestedByDot(obj, dotPath, value) {
4512
4662
  if (lastKey !== void 0) current[lastKey] = value;
4513
4663
  }
4514
4664
  //#endregion
4515
- //#region ../server/dist/app-TMhTLXuz.mjs
4665
+ //#region ../server/dist/app-DJEePmWL.mjs
4516
4666
  var __defProp = Object.defineProperty;
4517
4667
  var __exportAll = (all, no_symbols) => {
4518
4668
  let target = {};
@@ -5275,6 +5425,7 @@ async function deleteAdapterConfig(db, id) {
5275
5425
  const [row] = await db.delete(adapterConfigs).where(eq(adapterConfigs.id, id)).returning();
5276
5426
  if (!row) throw new NotFoundError(`Adapter config "${id}" not found`);
5277
5427
  }
5428
+ const log$4 = createLogger("AdminAdapters");
5278
5429
  function parseId(raw) {
5279
5430
  const id = Number(raw);
5280
5431
  if (!Number.isInteger(id) || id <= 0) throw new BadRequestError(`Invalid adapter ID: "${raw}"`);
@@ -5293,7 +5444,7 @@ async function adminAdapterRoutes(app) {
5293
5444
  const scope = memberScope(request);
5294
5445
  await assertCanManage(app.db, scope, body.agentId);
5295
5446
  const config = await createAdapterConfig(app.db, body, app.config.secrets.encryptionKey);
5296
- app.adapterManager.reload().catch((err) => app.log.error(err, "Adapter reload failed after create"));
5447
+ app.adapterManager.reload().catch((err) => log$4.error({ err }, "adapter reload failed after create"));
5297
5448
  app.notifier.notifyConfigChange("adapter_configs").catch(() => {});
5298
5449
  return reply.status(201).send({
5299
5450
  ...config,
@@ -5317,7 +5468,7 @@ async function adminAdapterRoutes(app) {
5317
5468
  const existing = await getAdapterConfig(app.db, id);
5318
5469
  await assertCanManage(app.db, scope, existing.agentId);
5319
5470
  const config = await updateAdapterConfig(app.db, id, body, app.config.secrets.encryptionKey);
5320
- app.adapterManager.reload().catch((err) => app.log.error(err, "Adapter reload failed after update"));
5471
+ app.adapterManager.reload().catch((err) => log$4.error({ err }, "adapter reload failed after update"));
5321
5472
  app.notifier.notifyConfigChange("adapter_configs").catch(() => {});
5322
5473
  return {
5323
5474
  ...config,
@@ -5331,7 +5482,7 @@ async function adminAdapterRoutes(app) {
5331
5482
  const existing = await getAdapterConfig(app.db, id);
5332
5483
  await assertCanManage(app.db, scope, existing.agentId);
5333
5484
  await deleteAdapterConfig(app.db, id);
5334
- app.adapterManager.reload().catch((err) => app.log.error(err, "Adapter reload failed after delete"));
5485
+ app.adapterManager.reload().catch((err) => log$4.error({ err }, "adapter reload failed after delete"));
5335
5486
  app.notifier.notifyConfigChange("adapter_configs").catch(() => {});
5336
5487
  return reply.status(204).send();
5337
5488
  });
@@ -6310,6 +6461,13 @@ const inboxEntries = pgTable("inbox_entries", {
6310
6461
  ackedAt: timestamp("acked_at", { withTimezone: true })
6311
6462
  }, (table) => [unique("uq_inbox_delivery").on(table.inboxId, table.messageId, table.chatId), index("idx_inbox_pending").on(table.inboxId, table.createdAt)]);
6312
6463
  async function sendMessage(db, chatId, senderId, data) {
6464
+ return withSpan("inbox.enqueue", messageAttrs({
6465
+ chatId,
6466
+ senderAgentId: senderId,
6467
+ source: data.source ?? void 0
6468
+ }), () => sendMessageInner(db, chatId, senderId, data));
6469
+ }
6470
+ async function sendMessageInner(db, chatId, senderId, data) {
6313
6471
  return db.transaction(async (tx) => {
6314
6472
  const messageId = randomUUID();
6315
6473
  const [msg] = await tx.insert(messages).values({
@@ -8315,7 +8473,11 @@ function adminWsRoutes(notifier, jwtSecret) {
8315
8473
  });
8316
8474
  });
8317
8475
  return async (app) => {
8318
- app.get("/admin", { websocket: true }, async (socket, request) => {
8476
+ app.get("/admin", {
8477
+ websocket: true,
8478
+ config: { otel: false }
8479
+ }, async (socket, request) => {
8480
+ startWsConnectionSpan(socket, { remoteIp: request.ip });
8319
8481
  const token = request.query.token;
8320
8482
  if (!token) {
8321
8483
  socket.send(JSON.stringify({
@@ -8323,6 +8485,7 @@ function adminWsRoutes(notifier, jwtSecret) {
8323
8485
  message: "Missing token query parameter"
8324
8486
  }));
8325
8487
  socket.close(4001, "Missing token");
8488
+ endWsConnectionSpan(socket, 4001);
8326
8489
  return;
8327
8490
  }
8328
8491
  let organizationId;
@@ -8335,6 +8498,7 @@ function adminWsRoutes(notifier, jwtSecret) {
8335
8498
  message: "Invalid token type"
8336
8499
  }));
8337
8500
  socket.close(4001, "Invalid token");
8501
+ endWsConnectionSpan(socket, 4001);
8338
8502
  return;
8339
8503
  }
8340
8504
  organizationId = payload.organizationId;
@@ -8345,8 +8509,13 @@ function adminWsRoutes(notifier, jwtSecret) {
8345
8509
  message: "Invalid or expired token"
8346
8510
  }));
8347
8511
  socket.close(4001, "Auth failed");
8512
+ endWsConnectionSpan(socket, 4001);
8348
8513
  return;
8349
8514
  }
8515
+ setWsConnectionAttrs(socket, {
8516
+ organizationId,
8517
+ memberId
8518
+ });
8350
8519
  const visibleAgentIds = await loadVisibleAgentIds(app.db, organizationId, memberId);
8351
8520
  adminSockets.set(socket, {
8352
8521
  organizationId,
@@ -8354,8 +8523,9 @@ function adminWsRoutes(notifier, jwtSecret) {
8354
8523
  visibleAgentIds
8355
8524
  });
8356
8525
  socket.send(JSON.stringify({ type: "admin:connected" }));
8357
- socket.on("close", () => {
8526
+ socket.on("close", (code) => {
8358
8527
  adminSockets.delete(socket);
8528
+ endWsConnectionSpan(socket, code);
8359
8529
  });
8360
8530
  });
8361
8531
  };
@@ -8430,6 +8600,7 @@ async function agentConfigRoutes(app) {
8430
8600
  return await app.configService.getDecrypted(identity.uuid);
8431
8601
  });
8432
8602
  }
8603
+ const log$3 = createLogger("AgentFeishuBot");
8433
8604
  async function agentFeishuBotRoutes(app) {
8434
8605
  /**
8435
8606
  * PUT /agent/me/feishu-bot
@@ -8457,7 +8628,7 @@ async function agentFeishuBotRoutes(app) {
8457
8628
  },
8458
8629
  status: "active"
8459
8630
  }, app.config.secrets.encryptionKey);
8460
- app.adapterManager.reload().catch((err) => app.log.error(err, "Adapter reload failed after self-service bind"));
8631
+ app.adapterManager.reload().catch((err) => log$3.error({ err }, "adapter reload failed after self-service bind"));
8461
8632
  app.notifier.notifyConfigChange("adapter_configs").catch(() => {});
8462
8633
  return reply.status(current ? 200 : 201).send({
8463
8634
  ...config,
@@ -8474,7 +8645,7 @@ async function agentFeishuBotRoutes(app) {
8474
8645
  const current = (await listAdapterConfigs(app.db)).find((c) => c.agentId === identity.uuid && c.platform === "feishu");
8475
8646
  if (!current) return reply.status(204).send();
8476
8647
  await deleteAdapterConfig(app.db, current.id);
8477
- app.adapterManager.reload().catch((err) => app.log.error(err, "Adapter reload failed after self-service unbind"));
8648
+ app.adapterManager.reload().catch((err) => log$3.error({ err }, "adapter reload failed after self-service unbind"));
8478
8649
  app.notifier.notifyConfigChange("adapter_configs").catch(() => {});
8479
8650
  return reply.status(204).send();
8480
8651
  });
@@ -8561,6 +8732,12 @@ async function resolveAgentId(db, source) {
8561
8732
  const DEFAULT_INBOX_TIMEOUT_SECONDS = 300;
8562
8733
  const DEFAULT_MAX_RETRY_COUNT = 3;
8563
8734
  async function pollInbox(db, inboxId, limit) {
8735
+ return withSpan("inbox.deliver", {
8736
+ "inbox.id": inboxId,
8737
+ "inbox.poll.limit": limit
8738
+ }, () => pollInboxInner(db, inboxId, limit));
8739
+ }
8740
+ async function pollInboxInner(db, inboxId, limit) {
8564
8741
  return await db.transaction(async (tx) => {
8565
8742
  const claimed = await tx.execute(sql`
8566
8743
  UPDATE inbox_entries
@@ -8615,12 +8792,17 @@ async function pollInbox(db, inboxId, limit) {
8615
8792
  });
8616
8793
  }
8617
8794
  async function ackEntry$2(db, entryId, inboxId) {
8618
- const [entry] = await db.update(inboxEntries).set({
8619
- status: "acked",
8620
- ackedAt: /* @__PURE__ */ new Date()
8621
- }).where(and(eq(inboxEntries.id, entryId), eq(inboxEntries.inboxId, inboxId), eq(inboxEntries.status, "delivered"))).returning();
8622
- if (!entry) throw new NotFoundError("Inbox entry not found or not in delivered status");
8623
- return entry;
8795
+ return withSpan("inbox.ack", {
8796
+ [FIRST_TREE_HUB_ATTR.INBOX_ENTRY_ID]: String(entryId),
8797
+ "inbox.id": inboxId
8798
+ }, async () => {
8799
+ const [entry] = await db.update(inboxEntries).set({
8800
+ status: "acked",
8801
+ ackedAt: /* @__PURE__ */ new Date()
8802
+ }).where(and(eq(inboxEntries.id, entryId), eq(inboxEntries.inboxId, inboxId), eq(inboxEntries.status, "delivered"))).returning();
8803
+ if (!entry) throw new NotFoundError("Inbox entry not found or not in delivered status");
8804
+ return entry;
8805
+ });
8624
8806
  }
8625
8807
  async function renewEntry(db, entryId, inboxId) {
8626
8808
  const [entry] = await db.update(inboxEntries).set({ deliveredAt: /* @__PURE__ */ new Date() }).where(and(eq(inboxEntries.id, entryId), eq(inboxEntries.inboxId, inboxId), eq(inboxEntries.status, "delivered"))).returning();
@@ -8683,6 +8865,7 @@ async function agentMeRoutes(app) {
8683
8865
  };
8684
8866
  });
8685
8867
  }
8868
+ const log$2 = createLogger("AgentMessages");
8686
8869
  const editMessageSchema = z.object({
8687
8870
  format: z.string().optional(),
8688
8871
  content: z.unknown()
@@ -8704,10 +8887,10 @@ async function agentMessageRoutes(app) {
8704
8887
  await assertParticipant(app.db, request.params.chatId, identity.uuid);
8705
8888
  const body = editMessageSchema.parse(request.body);
8706
8889
  const msg = await editMessage(app.db, request.params.chatId, request.params.messageId, identity.uuid, body);
8707
- app.adapterManager.editOutboundMessage(msg.id, msg.format, msg.content).catch((err) => app.log.error({
8890
+ app.adapterManager.editOutboundMessage(msg.id, msg.format, msg.content).catch((err) => log$2.error({
8708
8891
  err,
8709
8892
  messageId: msg.id
8710
- }, "Failed to edit outbound message"));
8893
+ }, "failed to edit outbound message"));
8711
8894
  return {
8712
8895
  ...msg,
8713
8896
  createdAt: msg.createdAt.toISOString()
@@ -8866,7 +9049,11 @@ function sendRejected(socket, ref, reason) {
8866
9049
  function clientWsRoutes(notifier, instanceId) {
8867
9050
  return async (app) => {
8868
9051
  const jwtSecretBytes = new TextEncoder().encode(app.config.secrets.jwtSecret);
8869
- app.get("/client", { websocket: true }, async (socket) => {
9052
+ app.get("/client", {
9053
+ websocket: true,
9054
+ config: { otel: false }
9055
+ }, async (socket) => {
9056
+ startWsConnectionSpan(socket);
8870
9057
  let session = null;
8871
9058
  let clientId = null;
8872
9059
  let authExpiryTimer = null;
@@ -8965,6 +9152,10 @@ function clientWsRoutes(notifier, instanceId) {
8965
9152
  organizationId: member.organizationId,
8966
9153
  role: member.role
8967
9154
  };
9155
+ setWsConnectionAttrs(socket, {
9156
+ "organization.id": member.organizationId,
9157
+ "member.id": member.id
9158
+ });
8968
9159
  clearTimeout(authTimeout);
8969
9160
  scheduleAuthExpiry(claims.exp);
8970
9161
  socket.send(JSON.stringify({ type: "auth:ok" }));
@@ -8978,223 +9169,228 @@ function clientWsRoutes(notifier, instanceId) {
8978
9169
  }
8979
9170
  return;
8980
9171
  }
8981
- try {
8982
- if (type === "client:register") {
8983
- const data = clientRegisterSchema.parse(msg);
8984
- try {
8985
- await registerClient(app.db, {
8986
- clientId: data.clientId,
8987
- userId: session.userId,
8988
- instanceId,
8989
- hostname: data.hostname,
8990
- os: data.os,
8991
- sdkVersion: data.sdkVersion
8992
- });
8993
- } catch (err) {
8994
- const message = err instanceof Error ? err.message : "client register failed";
9172
+ await withWsMessageSpan(socket, type, ref !== void 0 ? { "ws.message.ref": String(ref) } : {}, async () => {
9173
+ if (!session) return;
9174
+ try {
9175
+ if (type === "client:register") {
9176
+ const data = clientRegisterSchema.parse(msg);
9177
+ try {
9178
+ await registerClient(app.db, {
9179
+ clientId: data.clientId,
9180
+ userId: session.userId,
9181
+ instanceId,
9182
+ hostname: data.hostname,
9183
+ os: data.os,
9184
+ sdkVersion: data.sdkVersion
9185
+ });
9186
+ } catch (err) {
9187
+ const message = err instanceof Error ? err.message : "client register failed";
9188
+ socket.send(JSON.stringify({
9189
+ type: "client:register:rejected",
9190
+ message
9191
+ }));
9192
+ socket.close(4403, "client register rejected");
9193
+ return;
9194
+ }
9195
+ clientId = data.clientId;
9196
+ setWsConnectionAttrs(socket, { "client.id": data.clientId });
9197
+ setClientConnection(data.clientId, socket);
8995
9198
  socket.send(JSON.stringify({
8996
- type: "client:register:rejected",
8997
- message
9199
+ type: "client:registered",
9200
+ clientId: data.clientId
8998
9201
  }));
8999
- socket.close(4403, "client register rejected");
9000
- return;
9001
- }
9002
- clientId = data.clientId;
9003
- setClientConnection(data.clientId, socket);
9004
- socket.send(JSON.stringify({
9005
- type: "client:registered",
9006
- clientId: data.clientId
9007
- }));
9008
- try {
9009
- const pinned = await listActiveAgentsPinnedToClient(app.db, data.clientId);
9010
- for (const agent of pinned) {
9011
- const parsed = agentPinnedMessageSchema$1.safeParse({
9012
- type: "agent:pinned",
9013
- agentId: agent.uuid,
9014
- name: agent.name,
9015
- displayName: agent.displayName,
9016
- agentType: agent.type
9017
- });
9018
- if (!parsed.success) {
9019
- app.log.warn({
9020
- err: parsed.error.flatten(),
9202
+ try {
9203
+ const pinned = await listActiveAgentsPinnedToClient(app.db, data.clientId);
9204
+ for (const agent of pinned) {
9205
+ const parsed = agentPinnedMessageSchema$1.safeParse({
9206
+ type: "agent:pinned",
9021
9207
  agentId: agent.uuid,
9022
- clientId: data.clientId
9023
- }, "agent:pinned backfill frame failed schema validation — skipping");
9024
- continue;
9208
+ name: agent.name,
9209
+ displayName: agent.displayName,
9210
+ agentType: agent.type
9211
+ });
9212
+ if (!parsed.success) {
9213
+ app.log.warn({
9214
+ err: parsed.error.flatten(),
9215
+ agentId: agent.uuid,
9216
+ clientId: data.clientId
9217
+ }, "agent:pinned backfill frame failed schema validation — skipping");
9218
+ continue;
9219
+ }
9220
+ socket.send(JSON.stringify(parsed.data));
9025
9221
  }
9026
- socket.send(JSON.stringify(parsed.data));
9222
+ } catch (err) {
9223
+ app.log.error({
9224
+ err,
9225
+ clientId: data.clientId
9226
+ }, "agent:pinned backfill on client:register failed — client may need manual `agent add`");
9027
9227
  }
9028
- } catch (err) {
9029
- app.log.error({
9030
- err,
9031
- clientId: data.clientId
9032
- }, "agent:pinned backfill on client:register failed — client may need manual `agent add`");
9033
- }
9034
- } else if (type === "agent:bind") {
9035
- if (!clientId) {
9036
- socket.send(JSON.stringify({
9037
- type: "error",
9038
- ref,
9039
- message: "Must register client first"
9040
- }));
9041
- return;
9042
- }
9043
- const bindRequest = agentBindRequestSchema.parse(msg);
9044
- const [agent] = await app.db.select({
9045
- id: agents.uuid,
9046
- displayName: agents.displayName,
9047
- type: agents.type,
9048
- organizationId: agents.organizationId,
9049
- inboxId: agents.inboxId,
9050
- status: agents.status,
9051
- clientId: agents.clientId,
9052
- clientUserId: clients.userId,
9053
- managerUserId: members.userId
9054
- }).from(agents).leftJoin(clients, eq(agents.clientId, clients.id)).leftJoin(members, eq(agents.managerId, members.id)).where(and(eq(agents.uuid, bindRequest.agentId))).limit(1);
9055
- if (!agent) {
9056
- sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.UNKNOWN_AGENT);
9057
- return;
9058
- }
9059
- if (agent.organizationId !== session.organizationId) {
9060
- sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.WRONG_ORG);
9061
- return;
9062
- }
9063
- if (agent.status !== "active") {
9064
- sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.AGENT_SUSPENDED);
9065
- return;
9066
- }
9067
- if (agent.clientId === null) {
9068
- if (!agent.managerUserId || agent.managerUserId !== session.userId) {
9069
- sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.NOT_OWNED);
9228
+ } else if (type === "agent:bind") {
9229
+ if (!clientId) {
9230
+ socket.send(JSON.stringify({
9231
+ type: "error",
9232
+ ref,
9233
+ message: "Must register client first"
9234
+ }));
9070
9235
  return;
9071
9236
  }
9072
- if ((await app.db.update(agents).set({
9073
- clientId,
9074
- updatedAt: /* @__PURE__ */ new Date()
9075
- }).where(and(eq(agents.uuid, agent.id), isNull(agents.clientId))).returning({ uuid: agents.uuid })).length === 0) {
9237
+ const bindRequest = agentBindRequestSchema.parse(msg);
9238
+ const [agent] = await app.db.select({
9239
+ id: agents.uuid,
9240
+ displayName: agents.displayName,
9241
+ type: agents.type,
9242
+ organizationId: agents.organizationId,
9243
+ inboxId: agents.inboxId,
9244
+ status: agents.status,
9245
+ clientId: agents.clientId,
9246
+ clientUserId: clients.userId,
9247
+ managerUserId: members.userId
9248
+ }).from(agents).leftJoin(clients, eq(agents.clientId, clients.id)).leftJoin(members, eq(agents.managerId, members.id)).where(and(eq(agents.uuid, bindRequest.agentId))).limit(1);
9249
+ if (!agent) {
9250
+ sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.UNKNOWN_AGENT);
9251
+ return;
9252
+ }
9253
+ if (agent.organizationId !== session.organizationId) {
9254
+ sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.WRONG_ORG);
9255
+ return;
9256
+ }
9257
+ if (agent.status !== "active") {
9258
+ sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.AGENT_SUSPENDED);
9259
+ return;
9260
+ }
9261
+ if (agent.clientId === null) {
9262
+ if (!agent.managerUserId || agent.managerUserId !== session.userId) {
9263
+ sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.NOT_OWNED);
9264
+ return;
9265
+ }
9266
+ if ((await app.db.update(agents).set({
9267
+ clientId,
9268
+ updatedAt: /* @__PURE__ */ new Date()
9269
+ }).where(and(eq(agents.uuid, agent.id), isNull(agents.clientId))).returning({ uuid: agents.uuid })).length === 0) {
9270
+ sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.WRONG_CLIENT);
9271
+ return;
9272
+ }
9273
+ } else if (agent.clientId !== clientId) {
9076
9274
  sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.WRONG_CLIENT);
9077
9275
  return;
9276
+ } else if (!agent.clientUserId || agent.clientUserId !== session.userId) {
9277
+ sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.NOT_OWNED);
9278
+ return;
9078
9279
  }
9079
- } else if (agent.clientId !== clientId) {
9080
- sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.WRONG_CLIENT);
9081
- return;
9082
- } else if (!agent.clientUserId || agent.clientUserId !== session.userId) {
9083
- sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.NOT_OWNED);
9084
- return;
9085
- }
9086
- await bindAgent(app.db, agent.id, {
9087
- clientId,
9088
- instanceId,
9089
- runtimeType: bindRequest.runtimeType,
9090
- runtimeVersion: bindRequest.runtimeVersion
9091
- });
9092
- bindAgentToClient(clientId, agent.id);
9093
- boundAgents.set(agent.id, {
9094
- agentId: agent.id,
9095
- inboxId: agent.inboxId
9096
- });
9097
- notifier.subscribe(agent.inboxId, socket);
9098
- socket.send(JSON.stringify({
9099
- type: "agent:bound",
9100
- ref,
9101
- agentId: agent.id,
9102
- displayName: agent.displayName,
9103
- agentType: agent.type
9104
- }));
9105
- } else if (type === "agent:unbind") {
9106
- const agentId = parsed.data.agentId;
9107
- if (!agentId || !boundAgents.has(agentId)) {
9108
- socket.send(JSON.stringify({
9109
- type: "error",
9110
- message: "Agent not bound"
9111
- }));
9112
- return;
9113
- }
9114
- const info = boundAgents.get(agentId);
9115
- if (info) notifier.unsubscribe(info.inboxId, socket);
9116
- await unbindAgent(app.db, agentId);
9117
- unbindAgentFromClient(agentId);
9118
- boundAgents.delete(agentId);
9119
- socket.send(JSON.stringify({
9120
- type: "agent:unbound",
9121
- agentId
9122
- }));
9123
- } else if (type === "session:state") {
9124
- const agentId = parsed.data.agentId;
9125
- if (!agentId || !boundAgents.has(agentId)) {
9126
- socket.send(JSON.stringify({
9127
- type: "error",
9128
- message: "Agent not bound"
9129
- }));
9130
- return;
9131
- }
9132
- const payload = sessionStateMessageSchema.parse(msg);
9133
- if (payload.state === "evicted") chainSessionOp(agentId, payload.chatId, () => clearEvents(app.db, agentId, payload.chatId).catch(() => {}));
9134
- await upsertSessionState(app.db, agentId, payload.chatId, payload.state, session.organizationId, notifier);
9135
- } else if (type === "runtime:state") {
9136
- const agentId = parsed.data.agentId;
9137
- if (!agentId || !boundAgents.has(agentId)) {
9138
- socket.send(JSON.stringify({
9139
- type: "error",
9140
- message: "Agent not bound"
9141
- }));
9142
- return;
9143
- }
9144
- const payload = runtimeStateMessageSchema.parse(msg);
9145
- await setRuntimeState(app.db, agentId, payload.runtimeState, {
9146
- organizationId: session.organizationId,
9147
- notifier
9148
- });
9149
- if (payload.runtimeState === "error" && shouldNotify(agentId, "agent_error")) notifyAgentEvent(app.db, agentId, "agent_error", "high", `Agent ${agentId} entered error state`).catch(() => {});
9150
- else if (payload.runtimeState === "blocked" && shouldNotify(agentId, "agent_blocked")) notifyAgentEvent(app.db, agentId, "agent_blocked", "medium", `Agent ${agentId} is blocked`).catch(() => {});
9151
- } else if (type === "session:event") {
9152
- const agentId = parsed.data.agentId;
9153
- if (!agentId || !boundAgents.has(agentId)) {
9280
+ await bindAgent(app.db, agent.id, {
9281
+ clientId,
9282
+ instanceId,
9283
+ runtimeType: bindRequest.runtimeType,
9284
+ runtimeVersion: bindRequest.runtimeVersion
9285
+ });
9286
+ bindAgentToClient(clientId, agent.id);
9287
+ boundAgents.set(agent.id, {
9288
+ agentId: agent.id,
9289
+ inboxId: agent.inboxId
9290
+ });
9291
+ notifier.subscribe(agent.inboxId, socket);
9154
9292
  socket.send(JSON.stringify({
9155
- type: "error",
9156
- message: "Agent not bound"
9293
+ type: "agent:bound",
9294
+ ref,
9295
+ agentId: agent.id,
9296
+ displayName: agent.displayName,
9297
+ agentType: agent.type
9157
9298
  }));
9158
- return;
9159
- }
9160
- const payload = sessionEventMessageSchema.parse(msg);
9161
- chainSessionOp(agentId, payload.chatId, async () => {
9162
- try {
9163
- await appendEvent(app.db, agentId, payload.chatId, payload.event);
9164
- } catch (err) {
9299
+ } else if (type === "agent:unbind") {
9300
+ const agentId = parsed.data.agentId;
9301
+ if (!agentId || !boundAgents.has(agentId)) {
9165
9302
  socket.send(JSON.stringify({
9166
9303
  type: "error",
9167
- message: `Failed to persist session event: ${err instanceof Error ? err.message : String(err)}`
9304
+ message: "Agent not bound"
9168
9305
  }));
9306
+ return;
9169
9307
  }
9170
- });
9171
- } else if (type === "session:completion") {
9172
- const agentId = parsed.data.agentId;
9173
- if (!agentId || !boundAgents.has(agentId)) {
9308
+ const info = boundAgents.get(agentId);
9309
+ if (info) notifier.unsubscribe(info.inboxId, socket);
9310
+ await unbindAgent(app.db, agentId);
9311
+ unbindAgentFromClient(agentId);
9312
+ boundAgents.delete(agentId);
9174
9313
  socket.send(JSON.stringify({
9175
- type: "error",
9176
- message: "Agent not bound"
9314
+ type: "agent:unbound",
9315
+ agentId
9177
9316
  }));
9178
- return;
9179
- }
9180
- const payload = sessionCompletionMessageSchema.parse(msg);
9181
- if (shouldNotify(agentId, `session_completed:${payload.chatId}`)) notifyAgentEvent(app.db, agentId, "session_completed", "low", `Agent ${agentId} completed a task`, payload.chatId).catch(() => {});
9182
- } else if (type === "heartbeat") {
9183
- if (clientId) {
9184
- await heartbeatClient(app.db, clientId);
9185
- await Promise.all([...boundAgents.keys()].map((id) => touchAgent(app.db, id)));
9317
+ } else if (type === "session:state") {
9318
+ const agentId = parsed.data.agentId;
9319
+ if (!agentId || !boundAgents.has(agentId)) {
9320
+ socket.send(JSON.stringify({
9321
+ type: "error",
9322
+ message: "Agent not bound"
9323
+ }));
9324
+ return;
9325
+ }
9326
+ const payload = sessionStateMessageSchema.parse(msg);
9327
+ if (payload.state === "evicted") chainSessionOp(agentId, payload.chatId, () => clearEvents(app.db, agentId, payload.chatId).catch(() => {}));
9328
+ await upsertSessionState(app.db, agentId, payload.chatId, payload.state, session.organizationId, notifier);
9329
+ } else if (type === "runtime:state") {
9330
+ const agentId = parsed.data.agentId;
9331
+ if (!agentId || !boundAgents.has(agentId)) {
9332
+ socket.send(JSON.stringify({
9333
+ type: "error",
9334
+ message: "Agent not bound"
9335
+ }));
9336
+ return;
9337
+ }
9338
+ const payload = runtimeStateMessageSchema.parse(msg);
9339
+ await setRuntimeState(app.db, agentId, payload.runtimeState, {
9340
+ organizationId: session.organizationId,
9341
+ notifier
9342
+ });
9343
+ if (payload.runtimeState === "error" && shouldNotify(agentId, "agent_error")) notifyAgentEvent(app.db, agentId, "agent_error", "high", `Agent ${agentId} entered error state`).catch(() => {});
9344
+ else if (payload.runtimeState === "blocked" && shouldNotify(agentId, "agent_blocked")) notifyAgentEvent(app.db, agentId, "agent_blocked", "medium", `Agent ${agentId} is blocked`).catch(() => {});
9345
+ } else if (type === "session:event") {
9346
+ const agentId = parsed.data.agentId;
9347
+ if (!agentId || !boundAgents.has(agentId)) {
9348
+ socket.send(JSON.stringify({
9349
+ type: "error",
9350
+ message: "Agent not bound"
9351
+ }));
9352
+ return;
9353
+ }
9354
+ const payload = sessionEventMessageSchema.parse(msg);
9355
+ chainSessionOp(agentId, payload.chatId, async () => {
9356
+ try {
9357
+ await appendEvent(app.db, agentId, payload.chatId, payload.event);
9358
+ } catch (err) {
9359
+ socket.send(JSON.stringify({
9360
+ type: "error",
9361
+ message: `Failed to persist session event: ${err instanceof Error ? err.message : String(err)}`
9362
+ }));
9363
+ }
9364
+ });
9365
+ } else if (type === "session:completion") {
9366
+ const agentId = parsed.data.agentId;
9367
+ if (!agentId || !boundAgents.has(agentId)) {
9368
+ socket.send(JSON.stringify({
9369
+ type: "error",
9370
+ message: "Agent not bound"
9371
+ }));
9372
+ return;
9373
+ }
9374
+ const payload = sessionCompletionMessageSchema.parse(msg);
9375
+ if (shouldNotify(agentId, `session_completed:${payload.chatId}`)) notifyAgentEvent(app.db, agentId, "session_completed", "low", `Agent ${agentId} completed a task`, payload.chatId).catch(() => {});
9376
+ } else if (type === "heartbeat") {
9377
+ if (clientId) {
9378
+ await heartbeatClient(app.db, clientId);
9379
+ await Promise.all([...boundAgents.keys()].map((id) => touchAgent(app.db, id)));
9380
+ }
9381
+ socket.send(JSON.stringify({ type: "heartbeat:ack" }));
9186
9382
  }
9187
- socket.send(JSON.stringify({ type: "heartbeat:ack" }));
9383
+ } catch (err) {
9384
+ const message = err instanceof Error ? err.message : "Internal error";
9385
+ socket.send(JSON.stringify({
9386
+ type: "error",
9387
+ message
9388
+ }));
9188
9389
  }
9189
- } catch (err) {
9190
- const message = err instanceof Error ? err.message : "Internal error";
9191
- socket.send(JSON.stringify({
9192
- type: "error",
9193
- message
9194
- }));
9195
- }
9390
+ });
9196
9391
  });
9197
- socket.on("close", async () => {
9392
+ socket.on("close", async (closeCode) => {
9393
+ endWsConnectionSpan(socket, closeCode);
9198
9394
  clearTimeout(authTimeout);
9199
9395
  if (authExpiryTimer) clearTimeout(authExpiryTimer);
9200
9396
  for (const [agentId, info] of boundAgents) {
@@ -9601,6 +9797,7 @@ async function memberRoutes(app) {
9601
9797
  return reply.status(204).send();
9602
9798
  });
9603
9799
  }
9800
+ const log$1 = createLogger("GithubWebhook");
9604
9801
  const GITHUB_ADAPTER_ID = "github-adapter";
9605
9802
  function verifySignature(secret, rawBody, signatureHeader) {
9606
9803
  const expected = `sha256=${createHmac("sha256", secret).update(rawBody).digest("hex")}`;
@@ -9687,7 +9884,10 @@ async function routeMentionDelegations(app, mentionedNames, ctx) {
9687
9884
  status: agents.status
9688
9885
  }).from(agents).where(eq(agents.uuid, agent.delegateMention)).limit(1);
9689
9886
  if (!target || target.status !== "active") {
9690
- app.log.warn(`delegate_mention target "${agent.delegateMention}" for "${agent.name}" is not active, skipping`);
9887
+ log$1.warn({
9888
+ targetAgent: agent.delegateMention,
9889
+ sourceAgent: agent.name
9890
+ }, "delegate_mention target not active, skipping");
9691
9891
  continue;
9692
9892
  }
9693
9893
  try {
@@ -9713,7 +9913,11 @@ async function routeMentionDelegations(app, mentionedNames, ctx) {
9713
9913
  notifyRecipients(app.notifier, recipients, msg.id);
9714
9914
  routed++;
9715
9915
  } catch (err) {
9716
- app.log.error(err, `Failed to route mention delegation from "${agent.name}" to "${agent.delegateMention}"`);
9916
+ log$1.error({
9917
+ err,
9918
+ sourceAgent: agent.name,
9919
+ targetAgent: agent.delegateMention
9920
+ }, "failed to route mention delegation");
9717
9921
  }
9718
9922
  }
9719
9923
  return routed;
@@ -9974,7 +10178,10 @@ async function handleIssuesEvent(app, eventType, payload, reply) {
9974
10178
  });
9975
10179
  const [senderId, targetAgentId] = await Promise.all([ensureGitHubAdapterAgent(app.db), findTargetAgent(app.db, data.repository.full_name)]);
9976
10180
  if (!targetAgentId) {
9977
- app.log.warn(`No target agent found for GitHub issue event on ${data.repository.full_name}`);
10181
+ log$1.warn({
10182
+ repo: data.repository.full_name,
10183
+ event: "issue"
10184
+ }, "no target agent found for GitHub event");
9978
10185
  return reply.status(200).send({
9979
10186
  ok: true,
9980
10187
  event: "issues",
@@ -10026,7 +10233,10 @@ async function handleIssueCommentEvent(app, eventType, payload, reply) {
10026
10233
  });
10027
10234
  const [senderId, targetAgentId] = await Promise.all([ensureGitHubAdapterAgent(app.db), findTargetAgent(app.db, data.repository.full_name)]);
10028
10235
  if (!targetAgentId) {
10029
- app.log.warn(`No target agent found for GitHub issue_comment event on ${data.repository.full_name}`);
10236
+ log$1.warn({
10237
+ repo: data.repository.full_name,
10238
+ event: "issue_comment"
10239
+ }, "no target agent found for GitHub event");
10030
10240
  return reply.status(200).send({
10031
10241
  ok: true,
10032
10242
  event: "issue_comment",
@@ -10202,7 +10412,11 @@ function createAdapterManager(db, encryptionKey, log, notifier) {
10202
10412
  const bot = bots.get(appId);
10203
10413
  if (!bot) return;
10204
10414
  try {
10205
- await processInboundMessage(db, event, bot, log, notifier);
10415
+ await withSpan("adapter.inbound feishu", adapterAttrs({
10416
+ platform: "feishu",
10417
+ externalChatId: event.externalChannelId,
10418
+ agentId: bot.agentId
10419
+ }), () => processInboundMessage(db, event, bot, log, notifier));
10206
10420
  bot.lastActiveAt = /* @__PURE__ */ new Date();
10207
10421
  } catch (err) {
10208
10422
  await unclaimEvent(db, event.eventId, "feishu");
@@ -10290,15 +10504,17 @@ function createAdapterManager(db, encryptionKey, log, notifier) {
10290
10504
  sent: 0,
10291
10505
  errors: 0
10292
10506
  };
10293
- try {
10294
- return await processFeishuOutbound(db, findBotByAgentId, log);
10295
- } catch (err) {
10296
- log.error({ err }, "Feishu outbound processing error");
10297
- return {
10298
- sent: 0,
10299
- errors: 1
10300
- };
10301
- }
10507
+ return withSpan("adapter.outbound feishu", adapterAttrs({ platform: "feishu" }), async () => {
10508
+ try {
10509
+ return await processFeishuOutbound(db, findBotByAgentId, log);
10510
+ } catch (err) {
10511
+ log.error({ err }, "Feishu outbound processing error");
10512
+ return {
10513
+ sent: 0,
10514
+ errors: 1
10515
+ };
10516
+ }
10517
+ });
10302
10518
  },
10303
10519
  async editOutboundMessage(messageId, format, content) {
10304
10520
  const ref = await findExternalMessageByInternalId(db, "feishu", messageId);
@@ -10648,6 +10864,7 @@ function formatForFeishu(format, content) {
10648
10864
  content: JSON.stringify({ text })
10649
10865
  };
10650
10866
  }
10867
+ const log = createLogger("BackgroundTasks");
10651
10868
  function createBackgroundTasks(app, instanceId, adapterManager, kaelRuntime) {
10652
10869
  let inboxTimer = null;
10653
10870
  let heartbeatTimer = null;
@@ -10663,7 +10880,7 @@ function createBackgroundTasks(app, instanceId, adapterManager, kaelRuntime) {
10663
10880
  const maxRetries = configs.max_retry_count ?? 3;
10664
10881
  await resetTimedOutEntries(app.db, timeoutSeconds, maxRetries);
10665
10882
  } catch (err) {
10666
- app.log.error(err, "Failed to reset timed-out inbox entries");
10883
+ log.error({ err }, "failed to reset timed-out inbox entries");
10667
10884
  }
10668
10885
  }, 6e4);
10669
10886
  heartbeatTimer = setInterval(async () => {
@@ -10674,37 +10891,40 @@ function createBackgroundTasks(app, instanceId, adapterManager, kaelRuntime) {
10674
10891
  await cleanupStaleClients(app.db, staleSeconds);
10675
10892
  const staleAgents = await markStaleAgents(app.db, staleSeconds);
10676
10893
  if (staleAgents.length > 0) {
10677
- app.log.info(`Marked ${staleAgents.length} agent(s) as stale: ${staleAgents.join(", ")}`);
10894
+ log.info({
10895
+ count: staleAgents.length,
10896
+ agentIds: staleAgents
10897
+ }, "marked agents as stale");
10678
10898
  for (const agentId of staleAgents) notifyAgentEvent(app.db, agentId, "agent_stale", "medium", `Agent ${agentId} is unresponsive`).catch(() => {});
10679
10899
  }
10680
10900
  } catch (err) {
10681
- app.log.error(err, "Failed to heartbeat / cleanup presence");
10901
+ log.error({ err }, "failed to heartbeat / cleanup presence");
10682
10902
  }
10683
10903
  }, 3e4);
10684
10904
  adapterOutboundTimer = setInterval(async () => {
10685
10905
  try {
10686
10906
  await adapterManager.processOutbound();
10687
10907
  } catch (err) {
10688
- app.log.error(err, "Adapter outbound processing failed");
10908
+ log.error({ err }, "adapter outbound processing failed");
10689
10909
  }
10690
10910
  }, 5e3);
10691
10911
  if (kaelRuntime) kaelOutboundTimer = setInterval(async () => {
10692
10912
  try {
10693
10913
  await kaelRuntime.processOutbound();
10694
10914
  } catch (err) {
10695
- app.log.error(err, "Kael outbound processing failed");
10915
+ log.error({ err }, "kael outbound processing failed");
10696
10916
  }
10697
10917
  }, 5e3);
10698
10918
  sessionCleanupTimer = setInterval(async () => {
10699
10919
  try {
10700
10920
  const deleted = await cleanupStaleSessions(app.db);
10701
- if (deleted > 0) app.log.info(`Cleaned up ${deleted} stale session(s)`);
10921
+ if (deleted > 0) log.info({ count: deleted }, "cleaned up stale sessions");
10702
10922
  } catch (err) {
10703
- app.log.error(err, "Failed to clean up stale sessions");
10923
+ log.error({ err }, "failed to clean up stale sessions");
10704
10924
  }
10705
10925
  }, 36e5);
10706
10926
  heartbeatInstance(app.db, instanceId).catch((err) => {
10707
- app.log.error(err, "Failed initial heartbeat");
10927
+ log.error({ err }, "failed initial heartbeat");
10708
10928
  });
10709
10929
  },
10710
10930
  stop() {
@@ -11064,11 +11284,15 @@ function createKaelRuntime(db, encryptionKey, kaelEndpoint, kaelApiKey, serverUr
11064
11284
  sent: 0,
11065
11285
  errors: 0
11066
11286
  };
11067
- let sent = 0;
11068
- let errorCount = 0;
11069
- try {
11070
- const agentIds = [...agentConfigs.keys()];
11071
- const claimed = await db.execute(sql`
11287
+ return withSpan("kael.forward", {
11288
+ "kael.endpoint": kaelEndpoint,
11289
+ "kael.agent_count": agentConfigs.size
11290
+ }, async () => {
11291
+ let sent = 0;
11292
+ let errorCount = 0;
11293
+ try {
11294
+ const agentIds = [...agentConfigs.keys()];
11295
+ const claimed = await db.execute(sql`
11072
11296
  UPDATE inbox_entries
11073
11297
  SET status = 'delivered', delivered_at = NOW()
11074
11298
  WHERE id IN (
@@ -11084,75 +11308,76 @@ function createKaelRuntime(db, encryptionKey, kaelEndpoint, kaelApiKey, serverUr
11084
11308
  )
11085
11309
  RETURNING id, inbox_id, message_id, chat_id
11086
11310
  `);
11087
- for (const entry of claimed) try {
11088
- const [msg] = await db.select().from(messages).where(eq(messages.id, entry.message_id)).limit(1);
11089
- if (!msg) {
11090
- await ackEntry(db, entry.id);
11091
- continue;
11092
- }
11093
- const config = inboxToConfig.get(entry.inbox_id);
11094
- if (!config) {
11311
+ for (const entry of claimed) try {
11312
+ const [msg] = await db.select().from(messages).where(eq(messages.id, entry.message_id)).limit(1);
11313
+ if (!msg) {
11314
+ await ackEntry(db, entry.id);
11315
+ continue;
11316
+ }
11317
+ const config = inboxToConfig.get(entry.inbox_id);
11318
+ if (!config) {
11319
+ await ackEntry(db, entry.id);
11320
+ continue;
11321
+ }
11322
+ const messageContent = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
11323
+ const payload = {
11324
+ hub_chat_id: entry.chat_id ?? msg.chatId,
11325
+ hub_agent_id: config.agentId,
11326
+ hub_server_url: serverUrl,
11327
+ hub_agent_token: config.agentToken,
11328
+ user_id: config.kaelUserId,
11329
+ project_id: config.kaelProjectId,
11330
+ message: messageContent,
11331
+ sender_id: msg.senderId,
11332
+ format: msg.format
11333
+ };
11334
+ if (agentsMd) payload.agents_md = agentsMd;
11335
+ const response = await fetch(`${kaelEndpoint}/api/v1/hub/messages`, {
11336
+ method: "POST",
11337
+ headers: {
11338
+ "Content-Type": "application/json",
11339
+ ...kaelApiKey ? { "X-Internal-API-Key": kaelApiKey } : {}
11340
+ },
11341
+ body: JSON.stringify(payload)
11342
+ });
11343
+ if (!response.ok) {
11344
+ const body = await response.text().catch(() => "");
11345
+ log.error({
11346
+ entryId: entry.id,
11347
+ status: response.status,
11348
+ body
11349
+ }, "Kael API rejected outbound message");
11350
+ await nackEntry(db, entry.id);
11351
+ errorCount++;
11352
+ continue;
11353
+ }
11095
11354
  await ackEntry(db, entry.id);
11096
- continue;
11097
- }
11098
- const messageContent = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
11099
- const payload = {
11100
- hub_chat_id: entry.chat_id ?? msg.chatId,
11101
- hub_agent_id: config.agentId,
11102
- hub_server_url: serverUrl,
11103
- hub_agent_token: config.agentToken,
11104
- user_id: config.kaelUserId,
11105
- project_id: config.kaelProjectId,
11106
- message: messageContent,
11107
- sender_id: msg.senderId,
11108
- format: msg.format
11109
- };
11110
- if (agentsMd) payload.agents_md = agentsMd;
11111
- const response = await fetch(`${kaelEndpoint}/api/v1/hub/messages`, {
11112
- method: "POST",
11113
- headers: {
11114
- "Content-Type": "application/json",
11115
- ...kaelApiKey ? { "X-Internal-API-Key": kaelApiKey } : {}
11116
- },
11117
- body: JSON.stringify(payload)
11118
- });
11119
- if (!response.ok) {
11120
- const body = await response.text().catch(() => "");
11355
+ sent++;
11356
+ } catch (err) {
11121
11357
  log.error({
11122
11358
  entryId: entry.id,
11123
- status: response.status,
11124
- body
11125
- }, "Kael API rejected outbound message");
11126
- await nackEntry(db, entry.id);
11359
+ err
11360
+ }, "Failed to send outbound Kael message");
11361
+ await nackEntry(db, entry.id).catch((nackErr) => {
11362
+ log.error({
11363
+ entryId: entry.id,
11364
+ err: nackErr
11365
+ }, "Failed to NACK entry");
11366
+ });
11127
11367
  errorCount++;
11128
- continue;
11129
11368
  }
11130
- await ackEntry(db, entry.id);
11131
- sent++;
11132
11369
  } catch (err) {
11133
- log.error({
11134
- entryId: entry.id,
11135
- err
11136
- }, "Failed to send outbound Kael message");
11137
- await nackEntry(db, entry.id).catch((nackErr) => {
11138
- log.error({
11139
- entryId: entry.id,
11140
- err: nackErr
11141
- }, "Failed to NACK entry");
11142
- });
11143
- errorCount++;
11370
+ log.error({ err }, "Kael outbound processing error");
11371
+ return {
11372
+ sent: 0,
11373
+ errors: 1
11374
+ };
11144
11375
  }
11145
- } catch (err) {
11146
- log.error({ err }, "Kael outbound processing error");
11147
11376
  return {
11148
- sent: 0,
11149
- errors: 1
11377
+ sent,
11378
+ errors: errorCount
11150
11379
  };
11151
- }
11152
- return {
11153
- sent,
11154
- errors: errorCount
11155
- };
11380
+ });
11156
11381
  },
11157
11382
  shutdown() {
11158
11383
  aborted = true;
@@ -11262,7 +11487,15 @@ function createPulseAggregator(options) {
11262
11487
  };
11263
11488
  }
11264
11489
  async function buildApp(config) {
11265
- const app = Fastify({ logger: config.logger ?? true });
11490
+ applyLoggerConfig({
11491
+ level: config.observability.logging.level,
11492
+ format: config.observability.logging.format,
11493
+ bridgeToSpanLevel: config.observability.logging.bridgeToSpanLevel
11494
+ });
11495
+ const app = Fastify({ loggerInstance: rootLogger$1 });
11496
+ const otelPlugin = getFastifyOtelPlugin();
11497
+ if (otelPlugin) await app.register(otelPlugin);
11498
+ await app.register(observabilityPlugin);
11266
11499
  const db = connectDatabase(config.database.url);
11267
11500
  app.decorate("db", db);
11268
11501
  app.decorate("config", config);
@@ -11282,14 +11515,23 @@ async function buildApp(config) {
11282
11515
  const memberAuth = memberAuthHook(db, config.secrets.jwtSecret);
11283
11516
  const adminOnly = requireAdminRoleHook();
11284
11517
  const agentSelector = agentSelectorHook(db);
11285
- app.setErrorHandler((error, _request, reply) => {
11286
- if (error instanceof AppError) return reply.status(error.statusCode).send({ error: error.message });
11518
+ app.setErrorHandler((error, request, reply) => {
11519
+ const traceId = currentTraceId();
11520
+ const traceField = traceId ? { traceId } : {};
11521
+ if (error instanceof AppError) return reply.status(error.statusCode).send({
11522
+ error: error.message,
11523
+ ...traceField
11524
+ });
11287
11525
  if (error instanceof ZodError) return reply.status(400).send({
11288
11526
  error: "Validation error",
11289
- details: error.issues
11527
+ details: error.issues,
11528
+ ...traceField
11529
+ });
11530
+ request.log.error({ err: error }, "unhandled request error");
11531
+ return reply.status(500).send({
11532
+ error: "Internal server error",
11533
+ ...traceField
11290
11534
  });
11291
- app.log.error(error);
11292
- return reply.status(500).send({ error: "Internal server error" });
11293
11535
  });
11294
11536
  await app.register(healthzRoutes);
11295
11537
  await app.register(async (api) => {
@@ -11423,10 +11665,11 @@ async function buildApp(config) {
11423
11665
  notifier,
11424
11666
  broadcast: broadcastToAdmins
11425
11667
  });
11668
+ const hotReloadLog = createLogger("HotReload");
11426
11669
  notifier.onConfigChange((configType) => {
11427
11670
  if (configType === "adapter_configs") {
11428
- adapterManager.reload().catch((err) => app.log.error(err, "Adapter hot-reload failed (PG NOTIFY)"));
11429
- kaelRuntime?.reload().catch((err) => app.log.error(err, "Kael hot-reload failed (PG NOTIFY)"));
11671
+ adapterManager.reload().catch((err) => hotReloadLog.error({ err }, "adapter hot-reload failed (PG NOTIFY)"));
11672
+ kaelRuntime?.reload().catch((err) => hotReloadLog.error({ err }, "kael hot-reload failed (PG NOTIFY)"));
11430
11673
  }
11431
11674
  });
11432
11675
  app.addHook("onReady", async () => {
@@ -11500,13 +11743,18 @@ async function startServer(options) {
11500
11743
  const config = {
11501
11744
  ...serverConfig,
11502
11745
  webDistPath: webDistPath ?? void 0,
11503
- instanceId: `srv_${randomUUID().slice(0, 8)}`,
11504
- logger: true
11746
+ instanceId: `srv_${randomUUID().slice(0, 8)}`
11505
11747
  };
11748
+ const { initTelemetry, shutdownTelemetry } = await import("./observability-Xi-sEZI7.mjs");
11749
+ await initTelemetry(serverConfig.observability.tracing, config.instanceId);
11506
11750
  const app = await buildApp(config);
11507
11751
  const shutdown = async () => {
11508
11752
  process.stderr.write("\n Shutting down...\n");
11509
- await app.close();
11753
+ try {
11754
+ await app.close();
11755
+ } finally {
11756
+ await shutdownTelemetry();
11757
+ }
11510
11758
  process.exit(0);
11511
11759
  };
11512
11760
  process.on("SIGINT", () => void shutdown());
@@ -11548,6 +11796,32 @@ function resolveWebDist() {
11548
11796
  }
11549
11797
  //#endregion
11550
11798
  //#region src/core/service-install.ts
11799
+ /**
11800
+ * Run a subprocess capturing stderr so failures surface a meaningful error
11801
+ * instead of Node's opaque "Command failed". Used for launchctl/systemctl —
11802
+ * anywhere the stderr message is diagnostically crucial.
11803
+ */
11804
+ function runCapture(program, args, timeoutMs) {
11805
+ const res = spawnSync(program, args, {
11806
+ encoding: "utf-8",
11807
+ timeout: timeoutMs,
11808
+ stdio: [
11809
+ "ignore",
11810
+ "pipe",
11811
+ "pipe"
11812
+ ]
11813
+ });
11814
+ if (res.status === 0) return { ok: true };
11815
+ return {
11816
+ ok: false,
11817
+ stderr: (res.stderr ?? "").trim(),
11818
+ code: res.status
11819
+ };
11820
+ }
11821
+ function sleepSync(ms) {
11822
+ const shared = new Int32Array(new SharedArrayBuffer(4));
11823
+ Atomics.wait(shared, 0, 0, ms);
11824
+ }
11551
11825
  const LAUNCHD_LABEL = "dev.first-tree-hub.client";
11552
11826
  const SYSTEMD_UNIT = "first-tree-hub-client.service";
11553
11827
  const LOG_DIR = join(DEFAULT_HOME_DIR$1, "logs");
@@ -11655,30 +11929,56 @@ function launchctlDomainTarget() {
11655
11929
  }
11656
11930
  function launchdState() {
11657
11931
  if (!existsSync(launchdPlistPath())) return { state: "not-installed" };
11658
- try {
11659
- const out = execFileSync("launchctl", ["print", `${launchctlDomainTarget()}/${LAUNCHD_LABEL}`], {
11660
- encoding: "utf-8",
11661
- timeout: 5e3
11662
- });
11663
- const stateLine = out.split(/\r?\n/).find((l) => l.trim().startsWith("state ="));
11664
- const pidLine = out.split(/\r?\n/).find((l) => l.trim().startsWith("pid ="));
11665
- if (stateLine?.includes("running")) {
11666
- const pid = pidLine?.split("=")[1]?.trim();
11667
- return {
11668
- state: "active",
11669
- detail: pid ? `pid ${pid}` : "running"
11670
- };
11671
- }
11672
- return {
11673
- state: "inactive",
11674
- detail: stateLine?.trim() ?? "loaded"
11675
- };
11676
- } catch {
11932
+ const res = spawnSync("launchctl", ["print", `${launchctlDomainTarget()}/${LAUNCHD_LABEL}`], {
11933
+ encoding: "utf-8",
11934
+ timeout: 5e3,
11935
+ stdio: [
11936
+ "ignore",
11937
+ "pipe",
11938
+ "pipe"
11939
+ ]
11940
+ });
11941
+ if (res.status !== 0) return {
11942
+ state: "inactive",
11943
+ detail: "plist present but not loaded"
11944
+ };
11945
+ const out = res.stdout ?? "";
11946
+ const stateLine = out.split(/\r?\n/).find((l) => l.trim().startsWith("state ="));
11947
+ const pidLine = out.split(/\r?\n/).find((l) => l.trim().startsWith("pid ="));
11948
+ if (stateLine?.includes("running")) {
11949
+ const pid = pidLine?.split("=")[1]?.trim();
11677
11950
  return {
11678
- state: "inactive",
11679
- detail: "plist present but not loaded"
11951
+ state: "active",
11952
+ detail: pid ? `pid ${pid}` : "running"
11680
11953
  };
11681
11954
  }
11955
+ return {
11956
+ state: "inactive",
11957
+ detail: stateLine?.trim() ?? "loaded"
11958
+ };
11959
+ }
11960
+ /**
11961
+ * Poll `launchctl print` until the label disappears, confirming launchd has
11962
+ * finished the async eviction kicked off by `bootout`. Required because
11963
+ * `bootout` returns before the actual unload completes when the service has
11964
+ * active WebSocket connections — a follow-up `bootstrap` against a still-
11965
+ * registered label fails with `Bootstrap failed: 5: Input/output error`.
11966
+ */
11967
+ function waitForLabelEvicted(target, label, timeoutMs) {
11968
+ const deadline = Date.now() + timeoutMs;
11969
+ while (Date.now() < deadline) {
11970
+ if (spawnSync("launchctl", ["print", `${target}/${label}`], {
11971
+ encoding: "utf-8",
11972
+ timeout: 2e3,
11973
+ stdio: [
11974
+ "ignore",
11975
+ "ignore",
11976
+ "pipe"
11977
+ ]
11978
+ }).status !== 0) return true;
11979
+ sleepSync(200);
11980
+ }
11981
+ return false;
11682
11982
  }
11683
11983
  function installLaunchd() {
11684
11984
  const invocation = resolveCliInvocation();
@@ -11687,24 +11987,28 @@ function installLaunchd() {
11687
11987
  mkdirSync(dirname(plistPath), { recursive: true });
11688
11988
  writeFileSync(plistPath, renderPlist(invocation), { mode: 420 });
11689
11989
  const target = launchctlDomainTarget();
11690
- try {
11691
- execFileSync("launchctl", ["bootout", `${target}/${LAUNCHD_LABEL}`], {
11692
- stdio: "ignore",
11693
- timeout: 5e3
11694
- });
11695
- } catch {}
11696
- execFileSync("launchctl", [
11697
- "bootstrap",
11698
- target,
11699
- plistPath
11700
- ], {
11701
- stdio: "ignore",
11702
- timeout: 5e3
11703
- });
11704
- execFileSync("launchctl", ["enable", `${target}/${LAUNCHD_LABEL}`], {
11705
- stdio: "ignore",
11706
- timeout: 5e3
11707
- });
11990
+ const bootoutRes = runCapture("launchctl", ["bootout", `${target}/${LAUNCHD_LABEL}`], 15e3);
11991
+ if (!bootoutRes.ok) {
11992
+ if (!/not find|no such|not loaded/i.test(bootoutRes.stderr)) process.stderr.write(` warning: launchctl bootout: ${bootoutRes.stderr || `exit ${bootoutRes.code ?? "unknown"}`}\n`);
11993
+ }
11994
+ waitForLabelEvicted(target, LAUNCHD_LABEL, 1e4);
11995
+ let lastBootstrapErr = null;
11996
+ for (let attempt = 1; attempt <= 2; attempt++) {
11997
+ const res = runCapture("launchctl", [
11998
+ "bootstrap",
11999
+ target,
12000
+ plistPath
12001
+ ], 1e4);
12002
+ if (res.ok) {
12003
+ lastBootstrapErr = null;
12004
+ break;
12005
+ }
12006
+ lastBootstrapErr = res;
12007
+ if (attempt < 2) sleepSync(1e3);
12008
+ }
12009
+ if (lastBootstrapErr) throw new Error(`launchctl bootstrap failed: ${lastBootstrapErr.stderr || `exit ${lastBootstrapErr.code ?? "unknown"}`}\n Command: launchctl bootstrap ${target} ${plistPath}\n Recovery: \`launchctl bootout ${target}/${LAUNCHD_LABEL}\` then \`first-tree-hub service install\`.`);
12010
+ const enableRes = runCapture("launchctl", ["enable", `${target}/${LAUNCHD_LABEL}`], 5e3);
12011
+ if (!enableRes.ok) process.stderr.write(` warning: launchctl enable: ${enableRes.stderr || `exit ${enableRes.code ?? "unknown"}`}\n`);
11708
12012
  const { state, detail } = launchdState();
11709
12013
  return {
11710
12014
  platform: "launchd",
@@ -11717,13 +12021,8 @@ function installLaunchd() {
11717
12021
  }
11718
12022
  function uninstallLaunchd() {
11719
12023
  const plistPath = launchdPlistPath();
11720
- const target = launchctlDomainTarget();
11721
- try {
11722
- execFileSync("launchctl", ["bootout", `${target}/${LAUNCHD_LABEL}`], {
11723
- stdio: "ignore",
11724
- timeout: 5e3
11725
- });
11726
- } catch {}
12024
+ const res = runCapture("launchctl", ["bootout", `${launchctlDomainTarget()}/${LAUNCHD_LABEL}`], 15e3);
12025
+ if (!res.ok && !/not find|no such|not loaded/i.test(res.stderr)) process.stderr.write(` warning: bootout during uninstall: ${res.stderr || `exit ${res.code ?? "unknown"}`}\n`);
11727
12026
  if (existsSync(plistPath)) rmSync(plistPath);
11728
12027
  return {
11729
12028
  platform: "launchd",
@@ -11761,29 +12060,28 @@ function shellQuote(value) {
11761
12060
  }
11762
12061
  function systemdState() {
11763
12062
  if (!existsSync(systemdUnitPath())) return { state: "not-installed" };
11764
- try {
11765
- const out = execFileSync("systemctl", [
11766
- "--user",
11767
- "is-active",
11768
- SYSTEMD_UNIT
11769
- ], {
11770
- encoding: "utf-8",
11771
- timeout: 5e3
11772
- }).trim();
11773
- if (out === "active") return {
11774
- state: "active",
11775
- detail: "running"
11776
- };
11777
- return {
11778
- state: "inactive",
11779
- detail: out
11780
- };
11781
- } catch (err) {
11782
- return {
11783
- state: "inactive",
11784
- detail: (typeof err.stdout === "string" ? (err.stdout ?? "").trim() : "") || "unit present but not active"
11785
- };
11786
- }
12063
+ const res = spawnSync("systemctl", [
12064
+ "--user",
12065
+ "is-active",
12066
+ SYSTEMD_UNIT
12067
+ ], {
12068
+ encoding: "utf-8",
12069
+ timeout: 5e3,
12070
+ stdio: [
12071
+ "ignore",
12072
+ "pipe",
12073
+ "pipe"
12074
+ ]
12075
+ });
12076
+ const out = (res.stdout ?? "").trim();
12077
+ if (res.status === 0 && out === "active") return {
12078
+ state: "active",
12079
+ detail: "running"
12080
+ };
12081
+ return {
12082
+ state: "inactive",
12083
+ detail: out || "unit present but not active"
12084
+ };
11787
12085
  }
11788
12086
  function installSystemd() {
11789
12087
  const invocation = resolveCliInvocation();
@@ -11791,19 +12089,15 @@ function installSystemd() {
11791
12089
  const unitPath = systemdUnitPath();
11792
12090
  mkdirSync(dirname(unitPath), { recursive: true });
11793
12091
  writeFileSync(unitPath, renderSystemdUnit(invocation), { mode: 420 });
11794
- execFileSync("systemctl", ["--user", "daemon-reload"], {
11795
- stdio: "ignore",
11796
- timeout: 5e3
11797
- });
11798
- execFileSync("systemctl", [
12092
+ const reloadRes = runCapture("systemctl", ["--user", "daemon-reload"], 5e3);
12093
+ if (!reloadRes.ok) throw new Error(`systemctl --user daemon-reload failed: ${reloadRes.stderr || `exit ${reloadRes.code ?? "unknown"}`}`);
12094
+ const enableRes = runCapture("systemctl", [
11799
12095
  "--user",
11800
12096
  "enable",
11801
12097
  "--now",
11802
12098
  SYSTEMD_UNIT
11803
- ], {
11804
- stdio: "ignore",
11805
- timeout: 1e4
11806
- });
12099
+ ], 1e4);
12100
+ if (!enableRes.ok) throw new Error(`systemctl --user enable --now ${SYSTEMD_UNIT} failed: ${enableRes.stderr || `exit ${enableRes.code ?? "unknown"}`}\n Recovery: \`systemctl --user stop ${SYSTEMD_UNIT}\` then \`first-tree-hub service install\`.`);
11807
12101
  const { state, detail } = systemdState();
11808
12102
  return {
11809
12103
  platform: "systemd",
@@ -11816,24 +12110,16 @@ function installSystemd() {
11816
12110
  }
11817
12111
  function uninstallSystemd() {
11818
12112
  const unitPath = systemdUnitPath();
11819
- try {
11820
- execFileSync("systemctl", [
11821
- "--user",
11822
- "disable",
11823
- "--now",
11824
- SYSTEMD_UNIT
11825
- ], {
11826
- stdio: "ignore",
11827
- timeout: 1e4
11828
- });
11829
- } catch {}
12113
+ const disableRes = runCapture("systemctl", [
12114
+ "--user",
12115
+ "disable",
12116
+ "--now",
12117
+ SYSTEMD_UNIT
12118
+ ], 1e4);
12119
+ if (!disableRes.ok && !/not found|no such|not loaded/i.test(disableRes.stderr)) process.stderr.write(` warning: systemctl disable during uninstall: ${disableRes.stderr || `exit ${disableRes.code ?? "unknown"}`}\n`);
11830
12120
  if (existsSync(unitPath)) rmSync(unitPath);
11831
- try {
11832
- execFileSync("systemctl", ["--user", "daemon-reload"], {
11833
- stdio: "ignore",
11834
- timeout: 5e3
11835
- });
11836
- } catch {}
12121
+ const reloadRes = runCapture("systemctl", ["--user", "daemon-reload"], 5e3);
12122
+ if (!reloadRes.ok) process.stderr.write(` warning: systemctl daemon-reload during uninstall: ${reloadRes.stderr || `exit ${reloadRes.code ?? "unknown"}`}\n`);
11837
12123
  return {
11838
12124
  platform: "systemd",
11839
12125
  label: SYSTEMD_UNIT,
@@ -11896,4 +12182,4 @@ function uninstallClientService() {
11896
12182
  return getClientServiceStatus();
11897
12183
  }
11898
12184
  //#endregion
11899
- 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 };
12185
+ 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, applyClientLoggerConfig as R, 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 };